|
81617
|
2833
|
25
|
2026-05-28T08:32:41.372636+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957161372_m2.jpg...
|
PhpStorm
|
faVsco.js – TextRelayService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
remote: Resolving deltas: 100% (10/10), completed remote: Resolving deltas: 100% (10/10), completed with 10 local objects.
2 files committed
JY-20915 fix missing header
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
1
15
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
103
Previous Highlighted Error
Next Highlighted Error
[2026-05-28 08:28:04] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.883} {"correlation_id":"73199e23-a066-42ba-b189-2802e0869a44","trace_id":"e53672c2-0f50-409d-af25-84ab485c505d"}
[2026-05-28 08:28:04] local.INFO: [ScheduleBotCommand] Dispatched activities to capture {"count":0} {"correlation_id":"73199e23-a066-42ba-b189-2802e0869a44","trace_id":"e53672c2-0f50-409d-af25-84ab485c505d"}
[2026-05-28 08:28:04] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.883,"memoryPeakAfterCommandInMB":99.883} {"correlation_id":"73199e23-a066-42ba-b189-2802e0869a44","trace_id":"e53672c2-0f50-409d-af25-84ab485c505d"}
[2026-05-28 08:28:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.883} {"correlation_id":"741b2c4e-6a44-4c46-b9fb-fce017aef225","trace_id":"804dc547-eb25-4b32-8d0f-c87019773085"}
[2026-05-28 08:28:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.883,"memoryPeakAfterCommandInMB":99.883} {"correlation_id":"741b2c4e-6a44-4c46-b9fb-fce017aef225","trace_id":"804dc547-eb25-4b32-8d0f-c87019773085"}
[2026-05-28 08:28:09] local.NOTICE: Monitoring start {"correlation_id":"8ae5c131-edd9-463d-8161-875f4d34320f","trace_id":"1b6420c8-ef4e-4566-81b2-688696064648"}
[2026-05-28 08:28:09] local.NOTICE: Monitoring end {"correlation_id":"8ae5c131-edd9-463d-8161-875f4d34320f","trace_id":"1b6420c8-ef4e-4566-81b2-688696064648"}
[2026-05-28 08:28:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.883} {"correlation_id":"6f5390d3-abe7-4e56-a662-aeed1098941b","trace_id":"b57dc2df-2f4d-4d3b-b09a-aa6e61205d5e"}
[2026-05-28 08:28:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.883,"memoryPeakAfterCommandInMB":99.883} {"correlation_id":"6f5390d3-abe7-4e56-a662-aeed1098941b","trace_id":"b57dc2df-2f4d-4d3b-b09a-aa6e61205d5e"}
[2026-05-28 08:28:12] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.883} {"correlation_id":"ba694158-ea28-4ac1-b134-83ac485146e0","trace_id":"83a26b74-baa5-407e-9fc2-2b49d65b1c77"}
[2026-05-28 08:28:12] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"ba694158-ea28-4ac1-b134-83ac485146e0","trace_id":"83a26b74-baa5-407e-9fc2-2b49d65b1c77"}
[2026-05-28 08:28:12] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"ba694158-ea28-4ac1-b134-83ac485146e0","trace_id":"83a26b74-baa5-407e-9fc2-2b49d65b1c77"}
[2026-05-28 08:28:12] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.883,"memoryPeakAfterCommandInMB":99.883} {"correlation_id":"ba694158-ea28-4ac1-b134-83ac485146e0","trace_id":"83a26b74-baa5-407e-9fc2-2b49d65b1c77"}
[2026-05-28 08:28:14] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"conference:monitor:count","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.883} {"correlation_id":"b79833c7-3eab-4eb7-98bb-42359039b8fb","trace_id":"a8e159bc-867d-40af-bf7d-1228cf6d4f16"}
[2026-05-28 08:28:14] local.INFO: Running conference:monitor:count command for activities in (2026-05-28 08:26:00, 2026-05-28 08:28:00] {"correlation_id":"b79833c7-3eab-4eb7-98bb-42359039b8fb","trace_id":"a8e159bc-867d-40af-bf7d-1228cf6d4f16"}
[2026-05-28 08:28:14] local.INFO: [conference:monitor:count] No activities found in (2026-05-28 08:26:00, 2026-05-28 08:28:00] {"correlation_id":"b79833c7-3eab-4eb7-98bb-42359039b8fb","trace_id":"a8e159bc-867d-40af-bf7d-1228cf6d4f16"}
[2026-05-28 08:28:14] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"conference:monitor:count","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.883,"memoryPeakAfterCommandInMB":99.883} {"correlation_id":"b79833c7-3eab-4eb7-98bb-42359039b8fb","trace_id":"a8e159bc-867d-40af-bf7d-1228cf6d4f16"}
[2026-05-28 08:28:16] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:retry-failed","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.883} {"correlation_id":"d51071d5-ead0-45f0-a07e-e3234074f884","trace_id":"77173d9f-596f-405f-ae55-ba117b66455d"}
[2026-05-28 08:28:16] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"calendar:sync","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.883} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:16] local.NOTICE: Calendar sync start {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:16] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:retry-failed","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.883,"memoryPeakAfterCommandInMB":99.883} {"correlation_id":"d51071d5-ead0-45f0-a07e-e3234074f884","trace_id":"77173d9f-596f-405f-ae55-ba117b66455d"}
[2026-05-28 08:28:16] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1393,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:16] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1393,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:16] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:16] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1393,"provider":"google","refreshToken":"5aa7e2d96b53201cd16fca5d2e4ef3ad03320971fc064781d18aee3ae7b99fbf","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1393,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1393,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1387,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1387,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1387,"provider":"google","refreshToken":"8157ac6de94842937194009e9c50e459253600f799dacf6a40755ffdbeb5bba6","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1387,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1387,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1348,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1348,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1348,"provider":"google","refreshToken":"9e7d13d3032d0cb1b79d8e95aef01383e8e91eb52ff8ee960c8a0b6b95cd8c73","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1348,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1348,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1361,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1361,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1361,"provider":"google","refreshToken":"6c843da199c2b9907445329304fcc4ec5057a4ee748d8299641764395c08e1fd","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1361,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1361,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1310,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1310,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1310,"provider":"google","refreshToken":"e34818922c2830a660813a63f6169a4a9a992ae2cccd7dc8dd7796cfdb470ef1","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1310,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1310,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1333,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1333,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1333,"provider":"google","refreshToken":"6c902986546d8e8da1dc539b046cdc1d458f519acc972e5b5f1d6a1a295165e0","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1333,"provider":"google","responseBody":{"error":"unauthorized_client","error_description":"Unauthorized"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1333,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1368,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1368,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1368,"provider":"google","refreshToken":"d2f128898ff8543bd16b69cfae37896ab85119b0f5ed2b431d739593bb600333","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1368,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1368,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1365,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1365,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1365,"provider":"google","refreshToken":"7676e4a9afcd082b413248ab5ec6e487021fec6a9bdf315860a59cefad9caad8","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1365,"provider":"google","responseBody":{"error":"unauthorized_client","error_description":"Unauthorized"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1365,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1364,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1364,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1364,"provider":"google","refreshToken":"dd5882ebce76e645292ce33ae74238abbb77c0a4ecc6a2bfe723cad82e72ba8e","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1364,"provider":"google","responseBody":{"error":"unauthorized_client","error_description":"Unauthorized"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1364,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1370,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1370,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1370,"provider":"office","refreshToken":"b7ee8035306d0043cea6e00e7c4fe14f745e44074a1194db62a31cdf8b70af3e","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1370,"provider":"office","responseBody":"{\"error\":\"invalid_client\",\"error_description\":\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 41d8db5c-a91f-41a6-9b5c-22a6c99b9100 Correlation ID: 70bc0a6f-1565-4e74-94dc-419d94108fa9 Timestamp: 2026-05-28 08:28:18Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-28 08:28:18Z\",\"trace_id\":\"41d8db5c-a91f-41a6-9b5c-22a6c99b9100\",\"correlation_id\":\"70bc0a6f-1565-4e74-94dc-419d94108fa9\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1370,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1202,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1202,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1202,"provider":"office","refreshToken":"b458799ccc29b21a6e2eb5260fdb63e49ccba21bf942a3973fb63799bd7f0afe","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1202,"provider":"office","responseBody":"{\"error\":\"invalid_client\",\"error_description\":\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: d3d14dfc-1b70-40c4-b9db-fab12f748300 Correlation ID: 4c5f746c-8366-44c1-8733-6877607826db Timestamp: 2026-05-28 08:28:19Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-28 08:28:19Z\",\"trace_id\":\"d3d14dfc-1b70-40c4-b9db-fab12f748300\",\"correlation_id\":\"4c5f746c-8366-44c1-8733-6877607826db\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1202,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1502,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1502,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: Calendar sync job dispatched {"calendar_id":501} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1300,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1300,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1300,"provider":"google","refreshToken":"4b811db0725fd9602a95943519a7da935e2a5065da7d9ebfcb170752e3e1ddb8","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1300,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1300,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1409,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1409,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1409,"provider":"google","refreshToken":"e2a3f2d06894894eed1ee87d9db1ace77d4d42ee6e1288a8940ad2c10333b0c4","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1409,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1409,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1352,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1352,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1352,"provider":"google","refreshToken":"dd4b16b00fdc1216da6b717c02338c073636e29162826b2de6db3f064fc029eb","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1352,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1352,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1296,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1296,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1296,"provider":"office","refreshToken":"011ae723c9d800c674e0b4be76f49fc046dac7d501b66c59ef0d9549cfa56ae5","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1502,"provider":"google"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1502,"provider":"google"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [Calendar] Processing sync {"calendarId":"a33076c1-8d97-431a-99f0-85c9524e118b","from":null,"to":null,"delta":"CIiFh8TP44kDEIiFh8TP44kDGAUgkZvkzgIokZvkzgI=","last_sync":"2024-12-09 07:12:53","dateMode":"daily"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"integration-app","crm_owner":1695,"team_id":3143} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1502,"provider":"google"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1502,"provider":"google"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1296,"provider":"office","responseBody":"{\"error\":\"invalid_client\",\"error_description\":\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: dd83a147-95bc-407c-8c9b-3c14e7cb7d00 Correlation ID: bb16bae3-b3c4-467e-bd95-4e7ab85364c9 Timestamp: 2026-05-28 08:28:20Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-28 08:28:20Z\",\"trace_id\":\"dd83a147-95bc-407c-8c9b-3c14e7cb7d00\",\"correlation_id\":\"bb16bae3-b3c4-467e-bd95-4e7ab85364c9\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1296,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":391,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":391,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":391,"provider":"office","refreshToken":"00045eebae0f39b34887c6d53f92ae78064f7145e1f4b67754aebd03cfb2d881","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [Google Calendar] Failed to watch channel for calendar {"calendarId":"a33076c1-8d97-431a-99f0-85c9524e118b","code":400,"reason":"{
\"error\": {
\"errors\": [
{
\"domain\": \"global\",
\"reason\": \"push.webhookUrlNotHttps\",
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
],
\"code\": 400,
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
}"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.WARNING: [Calendar] Sync failed {"calendarId":"a33076c1-8d97-431a-99f0-85c9524e118b","code":400,"reason":"{
\"error\": {
\"errors\": [
{
\"domain\": \"global\",
\"reason\": \"push.webhookUrlNotHttps\",
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
],
\"code\": 400,
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
}"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":391,"provider":"office","responseBody":"{\"error\":\"invalid_client\",\"error_description\":\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 587224ad-f597-4371-b2ba-00fcd2da1b00 Correlation ID: 4e752610-95b1-4606-847f-68e00f90be0a Timestamp: 2026-05-28 08:28:21Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-28 08:28:21Z\",\"trace_id\":\"587224ad-f597-4371-b2ba-00fcd2da1b00\",\"correlation_id\":\"4e752610-95b1-4606-847f-68e00f90be0a\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:21] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":391,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1271,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1271,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:21] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Re...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"","depth":2,"bounds":{"left":0.6625665,"top":0.44134077,"width":0.06948138,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"remote: Resolving deltas: 100% (10/10), completed with 10 local objects.","depth":2,"bounds":{"left":0.6625665,"top":0.47326416,"width":0.06948138,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2 files committed","depth":2,"bounds":{"left":0.8753325,"top":0.91300875,"width":0.100398935,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"JY-20915 fix missing header","depth":3,"bounds":{"left":0.8753325,"top":0.92897046,"width":0.11037234,"height":0.013567438},"on_screen":true,"value":"JY-20915 fix missing header","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"bounds":{"left":0.8753325,"top":0.92897046,"width":0.05817819,"height":0.013567438},"on_screen":true,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":4,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Edit Commit Message…","depth":2,"bounds":{"left":0.8753325,"top":0.9481245,"width":0.048204787,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20915-fix-missing-header-text-relay, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09740692,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20915-fix-missing-header-text-relay","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8374335,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.37632978,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"15","depth":4,"bounds":{"left":0.3856383,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39694148,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40425533,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"103","depth":4,"bounds":{"left":0.72539896,"top":0.10055866,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7390292,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.7463431,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-28 08:28:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"73199e23-a066-42ba-b189-2802e0869a44\",\"trace_id\":\"e53672c2-0f50-409d-af25-84ab485c505d\"}\n[2026-05-28 08:28:04] local.INFO: [ScheduleBotCommand] Dispatched activities to capture {\"count\":0} {\"correlation_id\":\"73199e23-a066-42ba-b189-2802e0869a44\",\"trace_id\":\"e53672c2-0f50-409d-af25-84ab485c505d\"}\n[2026-05-28 08:28:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"73199e23-a066-42ba-b189-2802e0869a44\",\"trace_id\":\"e53672c2-0f50-409d-af25-84ab485c505d\"}\n[2026-05-28 08:28:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"741b2c4e-6a44-4c46-b9fb-fce017aef225\",\"trace_id\":\"804dc547-eb25-4b32-8d0f-c87019773085\"}\n[2026-05-28 08:28:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"741b2c4e-6a44-4c46-b9fb-fce017aef225\",\"trace_id\":\"804dc547-eb25-4b32-8d0f-c87019773085\"}\n[2026-05-28 08:28:09] local.NOTICE: Monitoring start {\"correlation_id\":\"8ae5c131-edd9-463d-8161-875f4d34320f\",\"trace_id\":\"1b6420c8-ef4e-4566-81b2-688696064648\"}\n[2026-05-28 08:28:09] local.NOTICE: Monitoring end {\"correlation_id\":\"8ae5c131-edd9-463d-8161-875f4d34320f\",\"trace_id\":\"1b6420c8-ef4e-4566-81b2-688696064648\"}\n[2026-05-28 08:28:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"6f5390d3-abe7-4e56-a662-aeed1098941b\",\"trace_id\":\"b57dc2df-2f4d-4d3b-b09a-aa6e61205d5e\"}\n[2026-05-28 08:28:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"6f5390d3-abe7-4e56-a662-aeed1098941b\",\"trace_id\":\"b57dc2df-2f4d-4d3b-b09a-aa6e61205d5e\"}\n[2026-05-28 08:28:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"ba694158-ea28-4ac1-b134-83ac485146e0\",\"trace_id\":\"83a26b74-baa5-407e-9fc2-2b49d65b1c77\"}\n[2026-05-28 08:28:12] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"ba694158-ea28-4ac1-b134-83ac485146e0\",\"trace_id\":\"83a26b74-baa5-407e-9fc2-2b49d65b1c77\"}\n[2026-05-28 08:28:12] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"ba694158-ea28-4ac1-b134-83ac485146e0\",\"trace_id\":\"83a26b74-baa5-407e-9fc2-2b49d65b1c77\"}\n[2026-05-28 08:28:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"ba694158-ea28-4ac1-b134-83ac485146e0\",\"trace_id\":\"83a26b74-baa5-407e-9fc2-2b49d65b1c77\"}\n[2026-05-28 08:28:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"b79833c7-3eab-4eb7-98bb-42359039b8fb\",\"trace_id\":\"a8e159bc-867d-40af-bf7d-1228cf6d4f16\"}\n[2026-05-28 08:28:14] local.INFO: Running conference:monitor:count command for activities in (2026-05-28 08:26:00, 2026-05-28 08:28:00] {\"correlation_id\":\"b79833c7-3eab-4eb7-98bb-42359039b8fb\",\"trace_id\":\"a8e159bc-867d-40af-bf7d-1228cf6d4f16\"}\n[2026-05-28 08:28:14] local.INFO: [conference:monitor:count] No activities found in (2026-05-28 08:26:00, 2026-05-28 08:28:00] {\"correlation_id\":\"b79833c7-3eab-4eb7-98bb-42359039b8fb\",\"trace_id\":\"a8e159bc-867d-40af-bf7d-1228cf6d4f16\"}\n[2026-05-28 08:28:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"b79833c7-3eab-4eb7-98bb-42359039b8fb\",\"trace_id\":\"a8e159bc-867d-40af-bf7d-1228cf6d4f16\"}\n[2026-05-28 08:28:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"d51071d5-ead0-45f0-a07e-e3234074f884\",\"trace_id\":\"77173d9f-596f-405f-ae55-ba117b66455d\"}\n[2026-05-28 08:28:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:16] local.NOTICE: Calendar sync start {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"d51071d5-ead0-45f0-a07e-e3234074f884\",\"trace_id\":\"77173d9f-596f-405f-ae55-ba117b66455d\"}\n[2026-05-28 08:28:16] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:16] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:16] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:16] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1393,\"provider\":\"google\",\"refreshToken\":\"5aa7e2d96b53201cd16fca5d2e4ef3ad03320971fc064781d18aee3ae7b99fbf\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1387,\"provider\":\"google\",\"refreshToken\":\"8157ac6de94842937194009e9c50e459253600f799dacf6a40755ffdbeb5bba6\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1348,\"provider\":\"google\",\"refreshToken\":\"9e7d13d3032d0cb1b79d8e95aef01383e8e91eb52ff8ee960c8a0b6b95cd8c73\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1361,\"provider\":\"google\",\"refreshToken\":\"6c843da199c2b9907445329304fcc4ec5057a4ee748d8299641764395c08e1fd\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1310,\"provider\":\"google\",\"refreshToken\":\"e34818922c2830a660813a63f6169a4a9a992ae2cccd7dc8dd7796cfdb470ef1\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1333,\"provider\":\"google\",\"refreshToken\":\"6c902986546d8e8da1dc539b046cdc1d458f519acc972e5b5f1d6a1a295165e0\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1368,\"provider\":\"google\",\"refreshToken\":\"d2f128898ff8543bd16b69cfae37896ab85119b0f5ed2b431d739593bb600333\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1365,\"provider\":\"google\",\"refreshToken\":\"7676e4a9afcd082b413248ab5ec6e487021fec6a9bdf315860a59cefad9caad8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1364,\"provider\":\"google\",\"refreshToken\":\"dd5882ebce76e645292ce33ae74238abbb77c0a4ecc6a2bfe723cad82e72ba8e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1370,\"provider\":\"office\",\"refreshToken\":\"b7ee8035306d0043cea6e00e7c4fe14f745e44074a1194db62a31cdf8b70af3e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1370,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 41d8db5c-a91f-41a6-9b5c-22a6c99b9100 Correlation ID: 70bc0a6f-1565-4e74-94dc-419d94108fa9 Timestamp: 2026-05-28 08:28:18Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-28 08:28:18Z\\\",\\\"trace_id\\\":\\\"41d8db5c-a91f-41a6-9b5c-22a6c99b9100\\\",\\\"correlation_id\\\":\\\"70bc0a6f-1565-4e74-94dc-419d94108fa9\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1370,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1202,\"provider\":\"office\",\"refreshToken\":\"b458799ccc29b21a6e2eb5260fdb63e49ccba21bf942a3973fb63799bd7f0afe\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1202,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: d3d14dfc-1b70-40c4-b9db-fab12f748300 Correlation ID: 4c5f746c-8366-44c1-8733-6877607826db Timestamp: 2026-05-28 08:28:19Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-28 08:28:19Z\\\",\\\"trace_id\\\":\\\"d3d14dfc-1b70-40c4-b9db-fab12f748300\\\",\\\"correlation_id\\\":\\\"4c5f746c-8366-44c1-8733-6877607826db\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1202,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: Calendar sync job dispatched {\"calendar_id\":501} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1300,\"provider\":\"google\",\"refreshToken\":\"4b811db0725fd9602a95943519a7da935e2a5065da7d9ebfcb170752e3e1ddb8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1409,\"provider\":\"google\",\"refreshToken\":\"e2a3f2d06894894eed1ee87d9db1ace77d4d42ee6e1288a8940ad2c10333b0c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1352,\"provider\":\"google\",\"refreshToken\":\"dd4b16b00fdc1216da6b717c02338c073636e29162826b2de6db3f064fc029eb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1296,\"provider\":\"office\",\"refreshToken\":\"011ae723c9d800c674e0b4be76f49fc046dac7d501b66c59ef0d9549cfa56ae5\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [Calendar] Processing sync {\"calendarId\":\"a33076c1-8d97-431a-99f0-85c9524e118b\",\"from\":null,\"to\":null,\"delta\":\"CIiFh8TP44kDEIiFh8TP44kDGAUgkZvkzgIokZvkzgI=\",\"last_sync\":\"2024-12-09 07:12:53\",\"dateMode\":\"daily\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"integration-app\",\"crm_owner\":1695,\"team_id\":3143} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1296,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: dd83a147-95bc-407c-8c9b-3c14e7cb7d00 Correlation ID: bb16bae3-b3c4-467e-bd95-4e7ab85364c9 Timestamp: 2026-05-28 08:28:20Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-28 08:28:20Z\\\",\\\"trace_id\\\":\\\"dd83a147-95bc-407c-8c9b-3c14e7cb7d00\\\",\\\"correlation_id\\\":\\\"bb16bae3-b3c4-467e-bd95-4e7ab85364c9\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1296,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":391,\"provider\":\"office\",\"refreshToken\":\"00045eebae0f39b34887c6d53f92ae78064f7145e1f4b67754aebd03cfb2d881\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [Google Calendar] Failed to watch channel for calendar {\"calendarId\":\"a33076c1-8d97-431a-99f0-85c9524e118b\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.WARNING: [Calendar] Sync failed {\"calendarId\":\"a33076c1-8d97-431a-99f0-85c9524e118b\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":391,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 587224ad-f597-4371-b2ba-00fcd2da1b00 Correlation ID: 4e752610-95b1-4606-847f-68e00f90be0a Timestamp: 2026-05-28 08:28:21Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-28 08:28:21Z\\\",\\\"trace_id\\\":\\\"587224ad-f597-4371-b2ba-00fcd2da1b00\\\",\\\"correlation_id\\\":\\\"4e752610-95b1-4606-847f-68e00f90be0a\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":391,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1271,\"provider\":\"office\",\"refreshToken\":\"118cde2c06993147b07ccaec4cbcd5026a819dea6c71081166a492933e392afb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1271,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: dd8bf2a7-bc49-43d7-a487-892c51e27600 Correlation ID: 51e3eb6c-c47e-4dd7-91aa-e734fe5b65cf Timestamp: 2026-05-28 08:28:21Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-28 08:28:21Z\\\",\\\"trace_id\\\":\\\"dd8bf2a7-bc49-43d7-a487-892c51e27600\\\",\\\"correlation_id\\\":\\\"51e3eb6c-c47e-4dd7-91aa-e734fe5b65cf\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1271,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1351,\"provider\":\"google\",\"refreshToken\":\"4271d15b9e60a606439caddc68337f783e472c85b03dacff14d1b6dfded9051c\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1366,\"provider\":\"google\",\"refreshToken\":\"ae21385059b2eebfd43f68aecd56eccd702a1aabb6598f1f7ab594ed8af491b4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: Calendar sync job dispatched {\"calendar_id\":378} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: Calendar sync job dispatched {\"calendar_id\":504} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.NOTICE: Calendar sync end {\"retrieved_calendars\":31,\"processed_calendars\":3} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [Calendar] Processing sync {\"calendarId\":\"2676cb6d-f86c-427e-bf78-591e388e3c1e\",\"from\":null,\"to\":null,\"delta\":\"CJ_x49O3jpIDEJ_x49O3jpIDGAUgw67KlwMow67KlwM=\",\"last_sync\":\"2026-01-19 07:48:40\",\"dateMode\":\"daily\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.WARNING: [Pipedrive] Account not connected for user {\"userId\":\"e6538737-e7b4-455f-a37a-3e79b665a220\",\"account\":{\"Jiminny\\\\Models\\\\SocialAccount\":{\"id\":1116,\"sociable_id\":241,\"provider_user_id\":\"19555731\",\"expires\":1779091997,\"refresh_token_expires\":null,\"provider\":\"pipedrive\",\"state\":\"full-refresh\",\"auth_scope\":\"base,deals:full,activities:full,contacts:full,search:read\",\"retry_after\":null,\"created_at\":\"2023-09-08 09:44:29\",\"updated_at\":\"2026-05-18 08:13:32\"}}} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"pipedrive\",\"crm_owner\":241,\"team_id\":19} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.WARNING: [Calendar] CRM disconnected for user so events will not be matched {\"provider\":\"pipedrive\",\"user_id\":241,\"message\":\"Your Pipedrive account has become disconnected. Please login to Jiminny to reconnect.\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [Google Calendar] Failed to watch channel for calendar {\"calendarId\":\"2676cb6d-f86c-427e-bf78-591e388e3c1e\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.WARNING: [Calendar] Sync failed {\"calendarId\":\"2676cb6d-f86c-427e-bf78-591e388e3c1e\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [Calendar] Processing sync {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\",\"from\":null,\"to\":null,\"delta\":\"R0usmcdvmMuZCBYV0hguCBhf4uKqyU9_gdNd-le_nqqau95XAUmZwlIAy2uwtCf5sA67w6NIiDvawUSmVjpTzZO-RsxLdeS-g8Kxy6ODZQxAxafq8AJNx0hJV7T291NUvfaPYqLMv-3je3IdhAPUtXQXkHtYaBAlX_DDBxwc3zYoVVQ8iM9mQkBykdA3sh-KVYPyzi7nK4cZg5Jzpu9tHA.UA7urx3pJyhUhdo_16STIbSNXUrF-qeQVIis9stVUeY\",\"last_sync\":\"2026-05-28 07:44:06\",\"dateMode\":\"daily\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [MS Office Calendar] Skipping delta sync for daily mode {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:29:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"86a65904-a9f8-43c9-b0c3-1e9c8b4b9541\",\"trace_id\":\"800b02ff-3553-46a3-a8c6-c12324a1ce53\"}\n[2026-05-28 08:29:05] local.INFO: [ScheduleBotCommand] Dispatched activities to capture {\"count\":0} {\"correlation_id\":\"86a65904-a9f8-43c9-b0c3-1e9c8b4b9541\",\"trace_id\":\"800b02ff-3553-46a3-a8c6-c12324a1ce53\"}\n[2026-05-28 08:29:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"86a65904-a9f8-43c9-b0c3-1e9c8b4b9541\",\"trace_id\":\"800b02ff-3553-46a3-a8c6-c12324a1ce53\"}\n[2026-05-28 08:29:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"e5162656-5b49-4857-9917-f0cf3ddffe27\",\"trace_id\":\"a3a53cc5-596d-474c-b9c4-f6c5f7ebdfd0\"}\n[2026-05-28 08:29:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"e5162656-5b49-4857-9917-f0cf3ddffe27\",\"trace_id\":\"a3a53cc5-596d-474c-b9c4-f6c5f7ebdfd0\"}\n[2026-05-28 08:29:09] local.NOTICE: Monitoring start {\"correlation_id\":\"f559463e-0c12-46df-8b53-0c50cf4aa3eb\",\"trace_id\":\"5a09aee9-5f75-4bfa-93e4-df0ff76f2326\"}\n[2026-05-28 08:29:09] local.NOTICE: Monitoring end {\"correlation_id\":\"f559463e-0c12-46df-8b53-0c50cf4aa3eb\",\"trace_id\":\"5a09aee9-5f75-4bfa-93e4-df0ff76f2326\"}\n[2026-05-28 08:29:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"9b1cc2f1-0c8a-4aa0-a9e8-57ac879839e4\",\"trace_id\":\"d69dd133-b5f0-4568-81ef-9cce3d1c5c9e\"}\n[2026-05-28 08:29:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"9b1cc2f1-0c8a-4aa0-a9e8-57ac879839e4\",\"trace_id\":\"d69dd133-b5f0-4568-81ef-9cce3d1c5c9e\"}\n[2026-05-28 08:29:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"87e10603-9e6f-4d71-97ab-5ba67103e5fd\",\"trace_id\":\"0aae0afb-779f-4999-892d-0188e14b3dcb\"}\n[2026-05-28 08:29:12] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"87e10603-9e6f-4d71-97ab-5ba67103e5fd\",\"trace_id\":\"0aae0afb-779f-4999-892d-0188e14b3dcb\"}\n[2026-05-28 08:29:12] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"87e10603-9e6f-4d71-97ab-5ba67103e5fd\",\"trace_id\":\"0aae0afb-779f-4999-892d-0188e14b3dcb\"}\n[2026-05-28 08:29:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"87e10603-9e6f-4d71-97ab-5ba67103e5fd\",\"trace_id\":\"0aae0afb-779f-4999-892d-0188e14b3dcb\"}\n[2026-05-28 08:30:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"200b132b-d8db-4010-b968-fe5e3fcc42f1\",\"trace_id\":\"7457715a-3b9d-4c35-a499-455c83e925ab\"}\n[2026-05-28 08:30:04] local.INFO: [ScheduleBotCommand] Dispatched activities to capture {\"count\":0} {\"correlation_id\":\"200b132b-d8db-4010-b968-fe5e3fcc42f1\",\"trace_id\":\"7457715a-3b9d-4c35-a499-455c83e925ab\"}\n[2026-05-28 08:30:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"200b132b-d8db-4010-b968-fe5e3fcc42f1\",\"trace_id\":\"7457715a-3b9d-4c35-a499-455c83e925ab\"}\n[2026-05-28 08:30:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"a94ef973-dee7-4bdf-99aa-9ff06f5a5561\",\"trace_id\":\"4e37013f-2993-4c71-87d9-560f8f429d27\"}\n[2026-05-28 08:30:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"a94ef973-dee7-4bdf-99aa-9ff06f5a5561\",\"trace_id\":\"4e37013f-2993-4c71-87d9-560f8f429d27\"}\n[2026-05-28 08:30:07] local.NOTICE: Monitoring start {\"correlation_id\":\"691e7103-784d-4b4a-8036-f3c624a186db\",\"trace_id\":\"991dd947-ed80-4080-8769-8ee7efc4e6a4\"}\n[2026-05-28 08:30:07] local.NOTICE: Monitoring end {\"correlation_id\":\"691e7103-784d-4b4a-8036-f3c624a186db\",\"trace_id\":\"991dd947-ed80-4080-8769-8ee7efc4e6a4\"}\n[2026-05-28 08:30:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"00cf8914-d440-4826-a079-ee4586e76c62\",\"trace_id\":\"d77009ee-8d7b-44ae-bb21-2080b6d48c6c\"}\n[2026-05-28 08:30:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"00cf8914-d440-4826-a079-ee4586e76c62\",\"trace_id\":\"d77009ee-8d7b-44ae-bb21-2080b6d48c6c\"}\n[2026-05-28 08:30:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"7c9550d0-42ed-4b1d-b5b3-7dca67f743ec\",\"trace_id\":\"84177853-9530-4b1e-bde6-3dc0c71883ee\"}\n[2026-05-28 08:30:10] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"7c9550d0-42ed-4b1d-b5b3-7dca67f743ec\",\"trace_id\":\"84177853-9530-4b1e-bde6-3dc0c71883ee\"}\n[2026-05-28 08:30:10] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"7c9550d0-42ed-4b1d-b5b3-7dca67f743ec\",\"trace_id\":\"84177853-9530-4b1e-bde6-3dc0c71883ee\"}\n[2026-05-28 08:30:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"7c9550d0-42ed-4b1d-b5b3-7dca67f743ec\",\"trace_id\":\"84177853-9530-4b1e-bde6-3dc0c71883ee\"}\n[2026-05-28 08:30:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"85cc6c92-262b-45c5-bbcc-4a084137fde3\",\"trace_id\":\"058bc9a3-67af-49b6-a87c-8a0a0bc0c47a\"}\n[2026-05-28 08:30:12] local.INFO: Running conference:monitor:count command for activities in (2026-05-28 08:28:00, 2026-05-28 08:30:00] {\"correlation_id\":\"85cc6c92-262b-45c5-bbcc-4a084137fde3\",\"trace_id\":\"058bc9a3-67af-49b6-a87c-8a0a0bc0c47a\"}\n[2026-05-28 08:30:12] local.INFO: [conference:monitor:count] No activities found in (2026-05-28 08:28:00, 2026-05-28 08:30:00] {\"correlation_id\":\"85cc6c92-262b-45c5-bbcc-4a084137fde3\",\"trace_id\":\"058bc9a3-67af-49b6-a87c-8a0a0bc0c47a\"}\n[2026-05-28 08:30:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"85cc6c92-262b-45c5-bbcc-4a084137fde3\",\"trace_id\":\"058bc9a3-67af-49b6-a87c-8a0a0bc0c47a\"}\n[2026-05-28 08:30:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"5083d024-d669-4c6a-bde3-ce084d6a628c\",\"trace_id\":\"83110e1f-4309-4f4a-b8bb-eab5c0ec081e\"}\n[2026-05-28 08:30:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"5083d024-d669-4c6a-bde3-ce084d6a628c\",\"trace_id\":\"83110e1f-4309-4f4a-b8bb-eab5c0ec081e\"}\n[2026-05-28 08:30:15] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"bc529b16-9502-4df3-b2c0-c63fcf977ed5\",\"trace_id\":\"9dd5d6d0-4cc5-454c-a0c8-0e72b69eeb29\"}\n[2026-05-28 08:30:15] local.INFO: [TextRelayService] Starting sync {\"mailbox\":\"catch-all@txt.staging.jiminny.com\",\"expected_alias\":\"catch-all\",\"expected_host\":\"txt.staging.jiminny.com\"} {\"correlation_id\":\"bc529b16-9502-4df3-b2c0-c63fcf977ed5\",\"trace_id\":\"9dd5d6d0-4cc5-454c-a0c8-0e72b69eeb29\"}\n[2026-05-28 08:30:15] local.INFO: [TextRelayService] Sync completed {\"mailbox\":\"catch-all@txt.staging.jiminny.com\",\"messages_processed\":0,\"message_ids\":[]} {\"correlation_id\":\"bc529b16-9502-4df3-b2c0-c63fcf977ed5\",\"trace_id\":\"9dd5d6d0-4cc5-454c-a0c8-0e72b69eeb29\"}\n[2026-05-28 08:30:15] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"bc529b16-9502-4df3-b2c0-c63fcf977ed5\",\"trace_id\":\"9dd5d6d0-4cc5-454c-a0c8-0e72b69eeb29\"}\n[2026-05-28 08:30:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:pre-meeting-notification\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"b655bca8-effa-420b-8686-95ba96f27085\",\"trace_id\":\"b1f43a19-c7ad-428e-9933-4e509b5f0055\"}\n[2026-05-28 08:30:18] local.INFO: Running pre-meeting notification command {\"correlation_id\":\"b655bca8-effa-420b-8686-95ba96f27085\",\"trace_id\":\"b1f43a19-c7ad-428e-9933-4e509b5f0055\"}\n[2026-05-28 08:30:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:pre-meeting-notification\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"b655bca8-effa-420b-8686-95ba96f27085\",\"trace_id\":\"b1f43a19-c7ad-428e-9933-4e509b5f0055\"}\n[2026-05-28 08:30:20] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:start\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"5a8493c4-3ef7-4453-98b7-f32225d453fe\",\"trace_id\":\"056f1aea-8825-4e2c-8239-7804086e504e\"}\n[2026-05-28 08:30:20] local.INFO: Running conference:monitor:start command for activities in (2026-05-28 08:20:00, 2026-05-28 08:25:00] {\"correlation_id\":\"5a8493c4-3ef7-4453-98b7-f32225d453fe\",\"trace_id\":\"056f1aea-8825-4e2c-8239-7804086e504e\"}\n[2026-05-28 08:30:20] local.INFO: [conference:monitor:start] No activities found in (2026-05-28 08:20:00, 2026-05-28 08:25:00] {\"correlation_id\":\"5a8493c4-3ef7-4453-98b7-f32225d453fe\",\"trace_id\":\"056f1aea-8825-4e2c-8239-7804086e504e\"}\n[2026-05-28 08:30:20] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:start\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"5a8493c4-3ef7-4453-98b7-f32225d453fe\",\"trace_id\":\"056f1aea-8825-4e2c-8239-7804086e504e\"}\n[2026-05-28 08:30:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:end\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"18a2aed9-1aae-4b10-a7fa-f75a147db499\",\"trace_id\":\"fff0c9b2-1c60-4402-b30a-2f41d9b45db3\"}\n[2026-05-28 08:30:22] local.INFO: conference:monitor:end:Jiminny\\Console\\Commands\\Activities\\MonitorMeetingEndCommand::logActivitiesEnded {\"from\":\"08:25\",\"to\":\"08:30\"} {\"correlation_id\":\"18a2aed9-1aae-4b10-a7fa-f75a147db499\",\"trace_id\":\"fff0c9b2-1c60-4402-b30a-2f41d9b45db3\"}\n[2026-05-28 08:30:22] local.INFO: conference:monitor:end:Jiminny\\Console\\Commands\\Activities\\MonitorMeetingEndCommand::logActivitiesWithUnfinishedSession {\"from\":\"22:20\",\"to\":\"22:25\"} {\"correlation_id\":\"18a2aed9-1aae-4b10-a7fa-f75a147db499\",\"trace_id\":\"fff0c9b2-1c60-4402-b30a-2f41d9b45db3\"}\n[2026-05-28 08:30:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:end\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"18a2aed9-1aae-4b10-a7fa-f75a147db499\",\"trace_id\":\"fff0c9b2-1c60-4402-b30a-2f41d9b45db3\"}\n[2026-05-28 08:30:23] local.NOTICE: Repairing HubSpot tokens start {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:23] local.INFO: Trying to refresh HubSpot token {\"account_id\":59,\"updated_at\":\"2025-10-03 09:32:05\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:23] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:23] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":59,\"provider\":\"hubspot\",\"refreshToken\":\"97b78f6e2cc49965c00c2492b602b02708b1392551e6b3f113fbaa48992af90b\",\"state\":\"full-refresh\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:26] local.ERROR: Failed to refresh HubSpot token {\"account_id\":59,\"updated_at\":\"2025-10-03 09:32:05\",\"reason\":\"missing or invalid refresh token\",\"previous\":\"\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:26] local.INFO: Trying to refresh HubSpot token {\"account_id\":306,\"updated_at\":\"2023-11-27 09:30:03\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:26] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":306,\"provider\":\"hubspot\",\"refreshToken\":\"6fa6aa8cc641d131231acc3470f5c03cb3b07b2e580fb18f8acb3b1dbb72549b\",\"state\":\"full-refresh\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:27] local.ERROR: Failed to refresh HubSpot token {\"account_id\":306,\"updated_at\":\"2023-11-27 09:30:03\",\"reason\":\"missing or invalid refresh token\",\"previous\":\"\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:27] local.INFO: Trying to refresh HubSpot token {\"account_id\":1372,\"updated_at\":\"2025-10-02 14:47:06\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:27] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:27] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1372,\"provider\":\"hubspot\",\"refreshToken\":\"9aa73948c761da29dce46c177cf9aee1fde483a44169ca38723f9f0597d7a8c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:27] local.ERROR: Failed to refresh HubSpot token {\"account_id\":1372,\"updated_at\":\"2025-10-02 14:47:06\",\"reason\":\"missing or invalid refresh token\",\"previous\":\"\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:27] local.NOTICE: Repairing HubSpot tokens end {\"total\":3,\"fixed\":0,\"failed\":3} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:33] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"jiminny:transcription:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"f73071c7-8696-459f-8aff-e61a7c6b30e6\",\"trace_id\":\"d72fc2ce-5064-4877-b401-10891378e1bc\"}\n[2026-05-28 08:30:33] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:pre-meeting-reminder\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"720d5c87-3d13-4193-87c9-efb8dc6df7c8\",\"trace_id\":\"a9d9c9fe-db29-44a1-a5f6-1905d48dde90\"}\n[2026-05-28 08:30:33] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"jiminny:transcription:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"f73071c7-8696-459f-8aff-e61a7c6b30e6\",\"trace_id\":\"d72fc2ce-5064-4877-b401-10891378e1bc\"}\n[2026-05-28 08:30:33] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:33] local.INFO: [HubSpot Journal Command] Starting polling service {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:33] local.INFO: [HubSpot Journal Polling] Service starting {\"memory_limit\":\"256M\",\"max_execution_time\":\"0\",\"initial_memory_mb\":62.0} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:33] local.INFO: [HubSpot Journal Polling] Acquired polling lock {\"expires_at\":\"2026-05-28T08:32:33.444809Z\"} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:33] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:33] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:33] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:pre-meeting-reminder\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"720d5c87-3d13-4193-87c9-efb8dc6df7c8\",\"trace_id\":\"a9d9c9fe-db29-44a1-a5f6-1905d48dde90\"}\n[2026-05-28 08:30:33] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:35] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:reset-governor\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"c692a62b-3b1a-4ff3-b965-388a7d12f619\",\"trace_id\":\"d1bdfdec-73bd-499f-a31e-b25a864da976\"}\n[2026-05-28 08:30:35] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:reset-governor\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"c692a62b-3b1a-4ff3-b965-388a7d12f619\",\"trace_id\":\"d1bdfdec-73bd-499f-a31e-b25a864da976\"}\n[2026-05-28 08:30:38] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:38] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:38] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:39] local.INFO: Dispatching activity sync job {\"import_id\":814238,\"provider\":\"twilio-flex\",\"team\":\"jiminny\"} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:39] local.INFO: Dispatching activity sync job {\"import_id\":814239,\"provider\":\"xant\",\"team\":\"jiminny\"} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:39] local.INFO: Dispatching activity sync job {\"import_id\":814240,\"provider\":\"apollo\",\"team\":\"jiminny\"} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:39] local.INFO: Dispatching activity sync job {\"import_id\":814241,\"provider\":\"groove\",\"team\":\"jiminny\"} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:39] local.INFO: Dispatching activity sync job {\"import_id\":814242,\"provider\":\"twilio-video\",\"team\":\"jiminny\"} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:39] local.INFO: Dispatching activity sync job {\"import_id\":814243,\"provider\":\"hubspot\",\"team\":\"hubspot\"} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:fail-stalled\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"f6d797df-5275-4cf1-8ddd-300c125df8da\",\"trace_id\":\"2b46c31f-aced-4324-a70a-6395f0a72618\"}\n[2026-05-28 08:30:42] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:fail-stalled\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"f6d797df-5275-4cf1-8ddd-300c125df8da\",\"trace_id\":\"2b46c31f-aced-4324-a70a-6395f0a72618\"}\n[2026-05-28 08:30:42] local.WARNING: [Salesforce] Account not connected for user {\"userId\":\"cdf8b554-d951-4758-bc2b-c1b85d1cd0b9\",\"account\":null} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"salesforce\",\"crm_owner\":3,\"team_id\":1} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [CrmOwnerResolver] TeamMember found with active crm connection {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1194,\"provider\":\"twilio-flex\"} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1194,\"provider\":\"twilio-flex\"} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [SyncActivity] Start {\"import_id\":814238,\"provider\":\"twilio-flex\",\"provider_id\":317,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.NOTICE: [TwilioFlex] Calls import start {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.ALERT: [SyncActivity] Failed {\"import_id\":814238,\"provider\":\"twilio-flex\",\"provider_id\":317,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"[HTTP 401] Unable to fetch page: Authenticate\",\"file\":\"/home/jiminny/vendor/twilio/sdk/src/Twilio/Page.php\",\"line\":60} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [SyncActivity] Start {\"import_id\":814239,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [Salesforce] Performing query {\"query\":\"\n SELECT Playbooks_Call_Date__c,Playbooks_Call_Recording__c,CreatedDate,TaskSubtype,CallType,CallDurationInSeconds,Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type\n FROM Task\n WHERE IsDeleted = false\n AND LastModifiedDate >= :from\n AND LastModifiedDate <= :to\n ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-05-28T08:14:00Z\",\"to\":\"2026-05-28T08:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+Playbooks_Call_Date__c%2CPlaybooks_Call_Recording__c%2CCreatedDate%2CTaskSubtype%2CCallType%2CCallDurationInSeconds%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-05-28T08%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-05-28T08%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:44] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:44] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:44] local.INFO: [Xant (InsideSales)] No calls found. {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SyncActivity] End {\"import_id\":814239,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SyncActivity] Memory usage {\"import_id\":814239,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1,\"memory_usage\":24080216,\"memory_real_usage\":65011712,\"pid\":21708} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SyncActivity] Start {\"import_id\":814240,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [Salesforce] Performing query {\"query\":\"\n SELECT AccountId,CreatedDate,TaskSubtype,CallType,Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type\n FROM Task\n WHERE IsDeleted = false\n AND LastModifiedDate >= :from\n AND LastModifiedDate <= :to\n ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-05-28T08:14:00Z\",\"to\":\"2026-05-28T08:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+AccountId%2CCreatedDate%2CTaskSubtype%2CCallType%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-05-28T08%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-05-28T08%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [Apollo] No calls found. {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SyncActivity] End {\"import_id\":814240,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SyncActivity] Memory usage {\"import_id\":814240,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1,\"memory_usage\":24242184,\"memory_real_usage\":65011712,\"pid\":21708} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SyncActivity] Start {\"import_id\":814241,\"provider\":\"groove\",\"provider_id\":228,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [Salesforce] Performing query {\"query\":\"\n SELECT call_recording_url__c,TaskSubtype,CreatedDate,CallType,CallDurationInSeconds,Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type\n FROM Task\n WHERE IsDeleted = false\n AND LastModifiedDate >= :from\n AND LastModifiedDate <= :to\n ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-05-28T08:14:00Z\",\"to\":\"2026-05-28T08:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+call_recording_url__c%2CTaskSubtype%2CCreatedDate%2CCallType%2CCallDurationInSeconds%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-05-28T08%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-05-28T08%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:bullhorn:ping\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"b2b37795-cfa8-472a-b681-322ce27c0e08\",\"trace_id\":\"605e54f2-d4f9-4683-b3fd-6c1c23e691b7\"}\n[2026-05-28 08:30:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:bullhorn:ping\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"b2b37795-cfa8-472a-b681-322ce27c0e08\",\"trace_id\":\"605e54f2-d4f9-4683-b3fd-6c1c23e691b7\"}\n[2026-05-28 08:30:44] local.ERROR: [Salesforce] Request exception [400] \nSELECT call_recording_url__c,TaskSubtype\n ^\nERROR at Row:1:Column:8\nNo such column 'call_recording_url__c' on entity 'Task'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names. {\"url\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+call_recording_url__c%2CTaskSubtype%2CCreatedDate%2CCallType%2CCallDurationInSeconds%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-05-28T08%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-05-28T08%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000\",\"data\":{\"headers\":{\"Authorization\":\"Bearer 00D2g0000008hH4!AQEAQHWUK6nshi8dSCX9CJphxkVivcv_TK98TLOckVy4i0Z1gQfAoRRuH2LVwDXDkbTPs80wvQjczjAlOz0UEtbHoORBLi2u\"}},\"response\":{\"GuzzleHttp\\\\Psr7\\\\Stream\":\"[{\\\"message\\\":\\\"\\\\nSELECT call_recording_url__c,TaskSubtype\\\\n ^\\\\nERROR at Row:1:Column:8\\\\nNo such column 'call_recording_url__c' on entity 'Task'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names.\\\",\\\"errorCode\\\":\\\"INVALID_FIELD\\\"}]\"},\"fields\":[]} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.ALERT: [SyncActivity] Failed {\"import_id\":814241,\"provider\":\"groove\",\"provider_id\":228,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"\nSELECT call_recording_url__c,TaskSubtype\n ^\nERROR at Row:1:Column:8\nNo such column 'call_recording_url__c' on entity 'Task'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names.\",\"file\":\"/home/jiminny/app/Services/Crm/Salesforce/Client.php\",\"line\":564} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SyncActivity] Start {\"import_id\":814242,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [Salesforce] Performing query {\"query\":\"SELECT Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type,twilio_call_sid__c,Lead_UUID__c,Opportunity__c\n FROM Task\n WHERE Type = 'Video'\n AND isClosed = true\n AND IsDeleted = false\n AND LastModifiedDate >= :from\n AND twilio_call_sid__c != NULL AND LastModifiedDate <= :to ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-05-28T08:14:00Z\",\"to\":\"2026-05-28T08:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=SELECT+Id%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%2Ctwilio_call_sid__c%2CLead_UUID__c%2COpportunity__c%0A++++++++++++++FROM+Task%0A++++++++++++WHERE+Type+%3D+%27Video%27%0A++++++++++++++AND+isClosed+%3D+true%0A++++++++++++++AND+IsDeleted+%3D+false%0A++++++++++++++AND+LastModifiedDate+%3E%3D+2026-05-28T08%3A14%3A00Z%0A++++++++++++++AND+twilio_call_sid__c+%21%3D+NULL+AND+LastModifiedDate+%3C%3D+2026-05-28T08%3A30%3A00Z+ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [Twilio Video] No calls found. {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SyncActivity] End {\"import_id\":814242,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SyncActivity] Memory usage {\"import_id\":814242,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1,\"memory_usage\":24634032,\"memory_real_usage\":65011712,\"pid\":21708} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":408,\"provider\":\"hubspot\"} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":408,\"provider\":\"hubspot\"} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SyncActivity] Start {\"import_id\":814243,\"provider\":\"hubspot\",\"provider_id\":31,\"team\":\"hubspot\",\"team_id\":2} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [HubSpot] Search calls for period {\"from\":\"2026-05-28 08:14:00\",\"to\":\"2026-05-28 08:30:00\"} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SyncActivity] End {\"import_id\":814243,\"provider\":\"hubspot\",\"provider_id\":31,\"team\":\"hubspot\",\"team_id\":2} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SyncActivity] Memory usage {\"import_id\":814243,\"provider\":\"hubspot\",\"provider_id\":31,\"team\":\"hubspot\",\"team_id\":2,\"memory_usage\":24947696,\"memory_real_usage\":65011712,\"pid\":21708} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:46] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"nudges:send\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"f3e257b6-82bb-440c-bf76-855ac528e4e8\",\"trace_id\":\"f8a31218-7290-45ac-8fe1-ffb1b8527521\"}\n[2026-05-28 08:30:46] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"nudges:send\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"f3e257b6-82bb-440c-bf76-855ac528e4e8\",\"trace_id\":\"f8a31218-7290-45ac-8fe1-ffb1b8527521\"}\n[2026-05-28 08:30:50] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"jiminny:playlists:normalize-sort\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"a64b1836-79a1-41da-a4b0-bc0a9703a94b\",\"trace_id\":\"c53faedc-a7a7-41b7-8e51-e84dc2755d79\"}\n[2026-05-28 08:30:50] local.INFO: [Jiminny\\Component\\Playlist\\Command\\NormalizeSortCommand::handle] starting. {\"playlists\":[]} {\"correlation_id\":\"a64b1836-79a1-41da-a4b0-bc0a9703a94b\",\"trace_id\":\"c53faedc-a7a7-41b7-8e51-e84dc2755d79\"}\n[2026-05-28 08:30:50] local.INFO: [Jiminny\\Component\\Playlist\\Command\\NormalizeSortCommand::handle] finished. {\"normalizedPlaylists\":[],\"deletedPlaylists\":[]} {\"correlation_id\":\"a64b1836-79a1-41da-a4b0-bc0a9703a94b\",\"trace_id\":\"c53faedc-a7a7-41b7-8e51-e84dc2755d79\"}\n[2026-05-28 08:30:50] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"jiminny:playlists:normalize-sort\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"a64b1836-79a1-41da-a4b0-bc0a9703a94b\",\"trace_id\":\"c53faedc-a7a7-41b7-8e51-e84dc2755d79\"}\n[2026-05-28 08:30:59] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:59] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:59] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"41db30ba-33c5-46f1-b8bd-5d9e86a70d12\",\"trace_id\":\"a5e2028d-ca41-4ba1-9ddd-a246f83ef4cc\"}\n[2026-05-28 08:31:07] local.INFO: [ScheduleBotCommand] Dispatched activities to capture {\"count\":0} {\"correlation_id\":\"41db30ba-33c5-46f1-b8bd-5d9e86a70d12\",\"trace_id\":\"a5e2028d-ca41-4ba1-9ddd-a246f83ef4cc\"}\n[2026-05-28 08:31:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"41db30ba-33c5-46f1-b8bd-5d9e86a70d12\",\"trace_id\":\"a5e2028d-ca41-4ba1-9ddd-a246f83ef4cc\"}\n[2026-05-28 08:31:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"f6ed0a05-caa1-4cb6-ae83-63f44ae4e0d6\",\"trace_id\":\"16cd898d-57a1-4b74-90d1-f9223bbd8d7a\"}\n[2026-05-28 08:31:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"f6ed0a05-caa1-4cb6-ae83-63f44ae4e0d6\",\"trace_id\":\"16cd898d-57a1-4b74-90d1-f9223bbd8d7a\"}\n[2026-05-28 08:31:15] local.NOTICE: Monitoring start {\"correlation_id\":\"cdf384ea-4f59-4411-8ebf-efc299e4a1c8\",\"trace_id\":\"b069d3c8-2f77-4698-bd53-97de373652e8\"}\n[2026-05-28 08:31:15] local.NOTICE: Monitoring end {\"correlation_id\":\"cdf384ea-4f59-4411-8ebf-efc299e4a1c8\",\"trace_id\":\"b069d3c8-2f77-4698-bd53-97de373652e8\"}\n[2026-05-28 08:31:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"940a264b-4aee-40a0-a527-e101ea555a38\",\"trace_id\":\"cd9169ad-4c1e-452d-b7cb-66005aae4dc8\"}\n[2026-05-28 08:31:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"940a264b-4aee-40a0-a527-e101ea555a38\",\"trace_id\":\"cd9169ad-4c1e-452d-b7cb-66005aae4dc8\"}\n[2026-05-28 08:31:23] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"b798f381-71f2-4c51-a135-f91f09330cfd\",\"trace_id\":\"b44ed325-23f7-4f8b-a447-4c8d0ee99d01\"}\n[2026-05-28 08:31:23] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"b798f381-71f2-4c51-a135-f91f09330cfd\",\"trace_id\":\"b44ed325-23f7-4f8b-a447-4c8d0ee99d01\"}\n[2026-05-28 08:31:24] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"b798f381-71f2-4c51-a135-f91f09330cfd\",\"trace_id\":\"b44ed325-23f7-4f8b-a447-4c8d0ee99d01\"}\n[2026-05-28 08:31:24] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"b798f381-71f2-4c51-a135-f91f09330cfd\",\"trace_id\":\"b44ed325-23f7-4f8b-a447-4c8d0ee99d01\"}\n[2026-05-28 08:31:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:sync-hubspot-objects\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"2d631328-f2b9-44f3-83d3-613b7b9b9ddc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:sync-hubspot-objects\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"2d631328-f2b9-44f3-83d3-613b7b9b9ddc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"usage\":22563136,\"real_usage\":65011712,\"pid\":21703} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [HubSpot] Syncing opportunities using strategy: lastModified {\"team\":2} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.4,\"average_seconds_per_request\":0.4} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [HubSpot] Synced opportunities {\"team\":2,\"strategies\":\"lastModified\",\"sync_count\":0,\"total\":0,\"last_synced_id\":null,\"duration_ms\":432.6} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"provider\":\"hubspot\",\"status\":\"completed\",\"duration_ms\":543.02,\"usage\":22983640,\"real_usage\":65011712,\"pid\":21703} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"usage\":22961848,\"real_usage\":65011712,\"pid\":21703} {\"correlation_id\":\"32799697-dd65-4ad4-b1d1-cd553c4510f8\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"2ac0447f-3c8c-4ce0-baeb-b63ddb76fa9b\",\"account\":null} {\"correlation_id\":\"32799697-dd65-4ad4-b1d1-cd553c4510f8\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":130,\"team_id\":42} {\"correlation_id\":\"32799697-dd65-4ad4-b1d1-cd553c4510f8\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"32799697-dd65-4ad4-b1d1-cd553c4510f8\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"32799697-dd65-4ad4-b1d1-cd553c4510f8\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":23.85,\"usage\":23024000,\"real_usage\":65011712,\"pid\":21703,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"32799697-dd65-4ad4-b1d1-cd553c4510f8\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"usage\":22984760,\"real_usage\":65011712,\"pid\":21703} {\"correlation_id\":\"0732b08b-2bf7-4f5b-93b6-9b2889269535\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"33e34a7a-1c02-4f04-87ac-22c3a385e6e3\",\"account\":{\"Jiminny\\\\Models\\\\SocialAccount\":{\"id\":306,\"sociable_id\":109,\"provider_user_id\":\"11348452\",\"expires\":1701077403,\"refresh_token_expires\":null,\"provider\":\"hubspot\",\"state\":\"full-refresh\",\"auth_scope\":null,\"retry_after\":null,\"created_at\":\"2020-09-01 16:59:04\",\"updated_at\":\"2023-11-27 09:30:03\"}}} {\"correlation_id\":\"0732b08b-2bf7-4f5b-93b6-9b2889269535\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":109,\"team_id\":29} {\"correlation_id\":\"0732b08b-2bf7-4f5b-93b6-9b2889269535\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"0732b08b-2bf7-4f5b-93b6-9b2889269535\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"0732b08b-2bf7-4f5b-93b6-9b2889269535\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":14.98,\"usage\":23047624,\"real_usage\":65011712,\"pid\":21703,\"reason\":\"Your HubSpot account has become disconnected. Please login to Jiminny to reconnect.\"} {\"correlation_id\":\"0732b08b-2bf7-4f5b-93b6-9b2889269535\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:29] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:29] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:29] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:29] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:29] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:29] local.INFO: [HubSpot Journal Polling] Service ending {\"runtime_seconds\":56,\"total_cycles\":5,\"files_downloaded\":0,\"empty_files\":0,\"other_portal_skipped\":0,\"total_events\":0,\"events_per_file\":0,\"avg_api_ms\":192.1,\"avg_download_ms\":0.0,\"avg_transform_ms\":0.0,\"avg_process_ms\":0.0,\"peak_memory_mb\":99.88} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:29] local.INFO: [HubSpot Journal Polling] Released polling lock {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:29] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"usage\":23005136,\"real_usage\":65011712,\"pid\":21703} {\"correlation_id\":\"4f83a1ac-418f-4f49-8377-328b614306fc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:29] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"71e3aac5-fb66-47c5-a236-2d051ae3e319\",\"account\":null} {\"correlation_id\":\"4f83a1ac-418f-4f49-8377-328b614306fc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:29] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":256,\"team_id\":49} {\"correlation_id\":\"4f83a1ac-418f-4f49-8377-328b614306fc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:29] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"4f83a1ac-418f-4f49-8377-328b614306fc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:29] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"4f83a1ac-418f-4f49-8377-328b614306fc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:29] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":34.51,\"usage\":23021248,\"real_usage\":65011712,\"pid\":21703,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"4f83a1ac-418f-4f49-8377-328b614306fc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:32:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"35f98b1b-7b16-4e79-a420-7e9818097d24\",\"trace_id\":\"d66deb58-fa13-486f-a866-2cac402dc589\"}\n[2026-05-28 08:32:06] local.INFO: [ScheduleBotCommand] Dispatched activities to capture {\"count\":0} {\"correlation_id\":\"35f98b1b-7b16-4e79-a420-7e9818097d24\",\"trace_id\":\"d66deb58-fa13-486f-a866-2cac402dc589\"}\n[2026-05-28 08:32:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"35f98b1b-7b16-4e79-a420-7e9818097d24\",\"trace_id\":\"d66deb58-fa13-486f-a866-2cac402dc589\"}\n[2026-05-28 08:32:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"bc6784c1-c46f-4295-be09-87334f4baa39\",\"trace_id\":\"74b00236-4ed0-4464-8b07-99c328ab6461\"}\n[2026-05-28 08:32:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"bc6784c1-c46f-4295-be09-87334f4baa39\",\"trace_id\":\"74b00236-4ed0-4464-8b07-99c328ab6461\"}\n[2026-05-28 08:32:11] local.NOTICE: Monitoring start {\"correlation_id\":\"caf4d1dd-2a52-40f5-834a-f9148f642e83\",\"trace_id\":\"1312bdcc-7e8b-480a-a935-2433fb79dc70\"}\n[2026-05-28 08:32:11] local.NOTICE: Monitoring end {\"correlation_id\":\"caf4d1dd-2a52-40f5-834a-f9148f642e83\",\"trace_id\":\"1312bdcc-7e8b-480a-a935-2433fb79dc70\"}\n[2026-05-28 08:32:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"64d86889-1f0d-4f7d-a1ac-6f56e58e775e\",\"trace_id\":\"b184d66b-49f8-40ec-afbe-2e76e9c7d952\"}\n[2026-05-28 08:32:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"64d86889-1f0d-4f7d-a1ac-6f56e58e775e\",\"trace_id\":\"b184d66b-49f8-40ec-afbe-2e76e9c7d952\"}\n[2026-05-28 08:32:20] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"8e6dd4bc-4f44-4fb2-b2ba-9e2f385996ef\",\"trace_id\":\"7767e0ca-37a3-43a5-972e-ea9a1bedea2f\"}\n[2026-05-28 08:32:20] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"8e6dd4bc-4f44-4fb2-b2ba-9e2f385996ef\",\"trace_id\":\"7767e0ca-37a3-43a5-972e-ea9a1bedea2f\"}\n[2026-05-28 08:32:20] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"8e6dd4bc-4f44-4fb2-b2ba-9e2f385996ef\",\"trace_id\":\"7767e0ca-37a3-43a5-972e-ea9a1bedea2f\"}\n[2026-05-28 08:32:20] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"8e6dd4bc-4f44-4fb2-b2ba-9e2f385996ef\",\"trace_id\":\"7767e0ca-37a3-43a5-972e-ea9a1bedea2f\"}\n[2026-05-28 08:32:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"6750ab25-6dff-4e7b-9f6e-bc7d1fc4221a\",\"trace_id\":\"3db6ce9b-167d-4214-a32c-67c611aa2bbe\"}\n[2026-05-28 08:32:22] local.INFO: Running conference:monitor:count command for activities in (2026-05-28 08:30:00, 2026-05-28 08:32:00] {\"correlation_id\":\"6750ab25-6dff-4e7b-9f6e-bc7d1fc4221a\",\"trace_id\":\"3db6ce9b-167d-4214-a32c-67c611aa2bbe\"}\n[2026-05-28 08:32:22] local.INFO: [conference:monitor:count] No activities found in (2026-05-28 08:30:00, 2026-05-28 08:32:00] {\"correlation_id\":\"6750ab25-6dff-4e7b-9f6e-bc7d1fc4221a\",\"trace_id\":\"3db6ce9b-167d-4214-a32c-67c611aa2bbe\"}\n[2026-05-28 08:32:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"6750ab25-6dff-4e7b-9f6e-bc7d1fc4221a\",\"trace_id\":\"3db6ce9b-167d-4214-a32c-67c611aa2bbe\"}\n[2026-05-28 08:32:23] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"04510c2c-638c-4687-81d7-b12e76f41df3\",\"trace_id\":\"87f0bb69-8fd1-47f1-9c9e-3b0ac3d6bd60\"}\n[2026-05-28 08:32:23] local.INFO: [EmailSchedule] STARTING batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"04510c2c-638c-4687-81d7-b12e76f41df3\",\"trace_id\":\"87f0bb69-8fd1-47f1-9c9e-3b0ac3d6bd60\"}\n[2026-05-28 08:32:23] local.INFO: [EmailSchedule] FINISHED batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"04510c2c-638c-4687-81d7-b12e76f41df3\",\"trace_id\":\"87f0bb69-8fd1-47f1-9c9e-3b0ac3d6bd60\"}\n[2026-05-28 08:32:23] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"04510c2c-638c-4687-81d7-b12e76f41df3\",\"trace_id\":\"87f0bb69-8fd1-47f1-9c9e-3b0ac3d6bd60\"}\n[2026-05-28 08:32:25] local.INFO: [Jiminny\\Jobs\\Mailbox\\CreateBatches] processed 1 inboxes and created 0 batches {\"userId\":null,\"batchSize\":30,\"maxBatches\":1000} {\"correlation_id\":\"e60597ef-aca8-4bda-bb90-18f01e96874d\",\"trace_id\":\"87f0bb69-8fd1-47f1-9c9e-3b0ac3d6bd60\"}","depth":4,"bounds":{"left":0.4331782,"top":0.09736632,"width":0.5668218,"height":0.90263367},"on_screen":true,"value":"[2026-05-28 08:28:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"73199e23-a066-42ba-b189-2802e0869a44\",\"trace_id\":\"e53672c2-0f50-409d-af25-84ab485c505d\"}\n[2026-05-28 08:28:04] local.INFO: [ScheduleBotCommand] Dispatched activities to capture {\"count\":0} {\"correlation_id\":\"73199e23-a066-42ba-b189-2802e0869a44\",\"trace_id\":\"e53672c2-0f50-409d-af25-84ab485c505d\"}\n[2026-05-28 08:28:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"73199e23-a066-42ba-b189-2802e0869a44\",\"trace_id\":\"e53672c2-0f50-409d-af25-84ab485c505d\"}\n[2026-05-28 08:28:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"741b2c4e-6a44-4c46-b9fb-fce017aef225\",\"trace_id\":\"804dc547-eb25-4b32-8d0f-c87019773085\"}\n[2026-05-28 08:28:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"741b2c4e-6a44-4c46-b9fb-fce017aef225\",\"trace_id\":\"804dc547-eb25-4b32-8d0f-c87019773085\"}\n[2026-05-28 08:28:09] local.NOTICE: Monitoring start {\"correlation_id\":\"8ae5c131-edd9-463d-8161-875f4d34320f\",\"trace_id\":\"1b6420c8-ef4e-4566-81b2-688696064648\"}\n[2026-05-28 08:28:09] local.NOTICE: Monitoring end {\"correlation_id\":\"8ae5c131-edd9-463d-8161-875f4d34320f\",\"trace_id\":\"1b6420c8-ef4e-4566-81b2-688696064648\"}\n[2026-05-28 08:28:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"6f5390d3-abe7-4e56-a662-aeed1098941b\",\"trace_id\":\"b57dc2df-2f4d-4d3b-b09a-aa6e61205d5e\"}\n[2026-05-28 08:28:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"6f5390d3-abe7-4e56-a662-aeed1098941b\",\"trace_id\":\"b57dc2df-2f4d-4d3b-b09a-aa6e61205d5e\"}\n[2026-05-28 08:28:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"ba694158-ea28-4ac1-b134-83ac485146e0\",\"trace_id\":\"83a26b74-baa5-407e-9fc2-2b49d65b1c77\"}\n[2026-05-28 08:28:12] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"ba694158-ea28-4ac1-b134-83ac485146e0\",\"trace_id\":\"83a26b74-baa5-407e-9fc2-2b49d65b1c77\"}\n[2026-05-28 08:28:12] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"ba694158-ea28-4ac1-b134-83ac485146e0\",\"trace_id\":\"83a26b74-baa5-407e-9fc2-2b49d65b1c77\"}\n[2026-05-28 08:28:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"ba694158-ea28-4ac1-b134-83ac485146e0\",\"trace_id\":\"83a26b74-baa5-407e-9fc2-2b49d65b1c77\"}\n[2026-05-28 08:28:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"b79833c7-3eab-4eb7-98bb-42359039b8fb\",\"trace_id\":\"a8e159bc-867d-40af-bf7d-1228cf6d4f16\"}\n[2026-05-28 08:28:14] local.INFO: Running conference:monitor:count command for activities in (2026-05-28 08:26:00, 2026-05-28 08:28:00] {\"correlation_id\":\"b79833c7-3eab-4eb7-98bb-42359039b8fb\",\"trace_id\":\"a8e159bc-867d-40af-bf7d-1228cf6d4f16\"}\n[2026-05-28 08:28:14] local.INFO: [conference:monitor:count] No activities found in (2026-05-28 08:26:00, 2026-05-28 08:28:00] {\"correlation_id\":\"b79833c7-3eab-4eb7-98bb-42359039b8fb\",\"trace_id\":\"a8e159bc-867d-40af-bf7d-1228cf6d4f16\"}\n[2026-05-28 08:28:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"b79833c7-3eab-4eb7-98bb-42359039b8fb\",\"trace_id\":\"a8e159bc-867d-40af-bf7d-1228cf6d4f16\"}\n[2026-05-28 08:28:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"d51071d5-ead0-45f0-a07e-e3234074f884\",\"trace_id\":\"77173d9f-596f-405f-ae55-ba117b66455d\"}\n[2026-05-28 08:28:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:16] local.NOTICE: Calendar sync start {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"d51071d5-ead0-45f0-a07e-e3234074f884\",\"trace_id\":\"77173d9f-596f-405f-ae55-ba117b66455d\"}\n[2026-05-28 08:28:16] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:16] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:16] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:16] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1393,\"provider\":\"google\",\"refreshToken\":\"5aa7e2d96b53201cd16fca5d2e4ef3ad03320971fc064781d18aee3ae7b99fbf\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1387,\"provider\":\"google\",\"refreshToken\":\"8157ac6de94842937194009e9c50e459253600f799dacf6a40755ffdbeb5bba6\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1348,\"provider\":\"google\",\"refreshToken\":\"9e7d13d3032d0cb1b79d8e95aef01383e8e91eb52ff8ee960c8a0b6b95cd8c73\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1361,\"provider\":\"google\",\"refreshToken\":\"6c843da199c2b9907445329304fcc4ec5057a4ee748d8299641764395c08e1fd\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1310,\"provider\":\"google\",\"refreshToken\":\"e34818922c2830a660813a63f6169a4a9a992ae2cccd7dc8dd7796cfdb470ef1\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1333,\"provider\":\"google\",\"refreshToken\":\"6c902986546d8e8da1dc539b046cdc1d458f519acc972e5b5f1d6a1a295165e0\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1368,\"provider\":\"google\",\"refreshToken\":\"d2f128898ff8543bd16b69cfae37896ab85119b0f5ed2b431d739593bb600333\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1365,\"provider\":\"google\",\"refreshToken\":\"7676e4a9afcd082b413248ab5ec6e487021fec6a9bdf315860a59cefad9caad8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1364,\"provider\":\"google\",\"refreshToken\":\"dd5882ebce76e645292ce33ae74238abbb77c0a4ecc6a2bfe723cad82e72ba8e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1370,\"provider\":\"office\",\"refreshToken\":\"b7ee8035306d0043cea6e00e7c4fe14f745e44074a1194db62a31cdf8b70af3e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1370,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 41d8db5c-a91f-41a6-9b5c-22a6c99b9100 Correlation ID: 70bc0a6f-1565-4e74-94dc-419d94108fa9 Timestamp: 2026-05-28 08:28:18Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-28 08:28:18Z\\\",\\\"trace_id\\\":\\\"41d8db5c-a91f-41a6-9b5c-22a6c99b9100\\\",\\\"correlation_id\\\":\\\"70bc0a6f-1565-4e74-94dc-419d94108fa9\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1370,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1202,\"provider\":\"office\",\"refreshToken\":\"b458799ccc29b21a6e2eb5260fdb63e49ccba21bf942a3973fb63799bd7f0afe\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1202,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: d3d14dfc-1b70-40c4-b9db-fab12f748300 Correlation ID: 4c5f746c-8366-44c1-8733-6877607826db Timestamp: 2026-05-28 08:28:19Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-28 08:28:19Z\\\",\\\"trace_id\\\":\\\"d3d14dfc-1b70-40c4-b9db-fab12f748300\\\",\\\"correlation_id\\\":\\\"4c5f746c-8366-44c1-8733-6877607826db\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1202,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: Calendar sync job dispatched {\"calendar_id\":501} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1300,\"provider\":\"google\",\"refreshToken\":\"4b811db0725fd9602a95943519a7da935e2a5065da7d9ebfcb170752e3e1ddb8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1409,\"provider\":\"google\",\"refreshToken\":\"e2a3f2d06894894eed1ee87d9db1ace77d4d42ee6e1288a8940ad2c10333b0c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1352,\"provider\":\"google\",\"refreshToken\":\"dd4b16b00fdc1216da6b717c02338c073636e29162826b2de6db3f064fc029eb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1296,\"provider\":\"office\",\"refreshToken\":\"011ae723c9d800c674e0b4be76f49fc046dac7d501b66c59ef0d9549cfa56ae5\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [Calendar] Processing sync {\"calendarId\":\"a33076c1-8d97-431a-99f0-85c9524e118b\",\"from\":null,\"to\":null,\"delta\":\"CIiFh8TP44kDEIiFh8TP44kDGAUgkZvkzgIokZvkzgI=\",\"last_sync\":\"2024-12-09 07:12:53\",\"dateMode\":\"daily\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"integration-app\",\"crm_owner\":1695,\"team_id\":3143} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1296,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: dd83a147-95bc-407c-8c9b-3c14e7cb7d00 Correlation ID: bb16bae3-b3c4-467e-bd95-4e7ab85364c9 Timestamp: 2026-05-28 08:28:20Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-28 08:28:20Z\\\",\\\"trace_id\\\":\\\"dd83a147-95bc-407c-8c9b-3c14e7cb7d00\\\",\\\"correlation_id\\\":\\\"bb16bae3-b3c4-467e-bd95-4e7ab85364c9\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1296,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":391,\"provider\":\"office\",\"refreshToken\":\"00045eebae0f39b34887c6d53f92ae78064f7145e1f4b67754aebd03cfb2d881\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [Google Calendar] Failed to watch channel for calendar {\"calendarId\":\"a33076c1-8d97-431a-99f0-85c9524e118b\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.WARNING: [Calendar] Sync failed {\"calendarId\":\"a33076c1-8d97-431a-99f0-85c9524e118b\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":391,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 587224ad-f597-4371-b2ba-00fcd2da1b00 Correlation ID: 4e752610-95b1-4606-847f-68e00f90be0a Timestamp: 2026-05-28 08:28:21Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-28 08:28:21Z\\\",\\\"trace_id\\\":\\\"587224ad-f597-4371-b2ba-00fcd2da1b00\\\",\\\"correlation_id\\\":\\\"4e752610-95b1-4606-847f-68e00f90be0a\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":391,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1271,\"provider\":\"office\",\"refreshToken\":\"118cde2c06993147b07ccaec4cbcd5026a819dea6c71081166a492933e392afb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1271,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: dd8bf2a7-bc49-43d7-a487-892c51e27600 Correlation ID: 51e3eb6c-c47e-4dd7-91aa-e734fe5b65cf Timestamp: 2026-05-28 08:28:21Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-28 08:28:21Z\\\",\\\"trace_id\\\":\\\"dd8bf2a7-bc49-43d7-a487-892c51e27600\\\",\\\"correlation_id\\\":\\\"51e3eb6c-c47e-4dd7-91aa-e734fe5b65cf\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1271,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1351,\"provider\":\"google\",\"refreshToken\":\"4271d15b9e60a606439caddc68337f783e472c85b03dacff14d1b6dfded9051c\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1366,\"provider\":\"google\",\"refreshToken\":\"ae21385059b2eebfd43f68aecd56eccd702a1aabb6598f1f7ab594ed8af491b4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: Calendar sync job dispatched {\"calendar_id\":378} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: Calendar sync job dispatched {\"calendar_id\":504} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.NOTICE: Calendar sync end {\"retrieved_calendars\":31,\"processed_calendars\":3} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [Calendar] Processing sync {\"calendarId\":\"2676cb6d-f86c-427e-bf78-591e388e3c1e\",\"from\":null,\"to\":null,\"delta\":\"CJ_x49O3jpIDEJ_x49O3jpIDGAUgw67KlwMow67KlwM=\",\"last_sync\":\"2026-01-19 07:48:40\",\"dateMode\":\"daily\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.WARNING: [Pipedrive] Account not connected for user {\"userId\":\"e6538737-e7b4-455f-a37a-3e79b665a220\",\"account\":{\"Jiminny\\\\Models\\\\SocialAccount\":{\"id\":1116,\"sociable_id\":241,\"provider_user_id\":\"19555731\",\"expires\":1779091997,\"refresh_token_expires\":null,\"provider\":\"pipedrive\",\"state\":\"full-refresh\",\"auth_scope\":\"base,deals:full,activities:full,contacts:full,search:read\",\"retry_after\":null,\"created_at\":\"2023-09-08 09:44:29\",\"updated_at\":\"2026-05-18 08:13:32\"}}} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"pipedrive\",\"crm_owner\":241,\"team_id\":19} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.WARNING: [Calendar] CRM disconnected for user so events will not be matched {\"provider\":\"pipedrive\",\"user_id\":241,\"message\":\"Your Pipedrive account has become disconnected. Please login to Jiminny to reconnect.\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [Google Calendar] Failed to watch channel for calendar {\"calendarId\":\"2676cb6d-f86c-427e-bf78-591e388e3c1e\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.WARNING: [Calendar] Sync failed {\"calendarId\":\"2676cb6d-f86c-427e-bf78-591e388e3c1e\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [Calendar] Processing sync {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\",\"from\":null,\"to\":null,\"delta\":\"R0usmcdvmMuZCBYV0hguCBhf4uKqyU9_gdNd-le_nqqau95XAUmZwlIAy2uwtCf5sA67w6NIiDvawUSmVjpTzZO-RsxLdeS-g8Kxy6ODZQxAxafq8AJNx0hJV7T291NUvfaPYqLMv-3je3IdhAPUtXQXkHtYaBAlX_DDBxwc3zYoVVQ8iM9mQkBykdA3sh-KVYPyzi7nK4cZg5Jzpu9tHA.UA7urx3pJyhUhdo_16STIbSNXUrF-qeQVIis9stVUeY\",\"last_sync\":\"2026-05-28 07:44:06\",\"dateMode\":\"daily\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [MS Office Calendar] Skipping delta sync for daily mode {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:29:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"86a65904-a9f8-43c9-b0c3-1e9c8b4b9541\",\"trace_id\":\"800b02ff-3553-46a3-a8c6-c12324a1ce53\"}\n[2026-05-28 08:29:05] local.INFO: [ScheduleBotCommand] Dispatched activities to capture {\"count\":0} {\"correlation_id\":\"86a65904-a9f8-43c9-b0c3-1e9c8b4b9541\",\"trace_id\":\"800b02ff-3553-46a3-a8c6-c12324a1ce53\"}\n[2026-05-28 08:29:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"86a65904-a9f8-43c9-b0c3-1e9c8b4b9541\",\"trace_id\":\"800b02ff-3553-46a3-a8c6-c12324a1ce53\"}\n[2026-05-28 08:29:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"e5162656-5b49-4857-9917-f0cf3ddffe27\",\"trace_id\":\"a3a53cc5-596d-474c-b9c4-f6c5f7ebdfd0\"}\n[2026-05-28 08:29:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"e5162656-5b49-4857-9917-f0cf3ddffe27\",\"trace_id\":\"a3a53cc5-596d-474c-b9c4-f6c5f7ebdfd0\"}\n[2026-05-28 08:29:09] local.NOTICE: Monitoring start {\"correlation_id\":\"f559463e-0c12-46df-8b53-0c50cf4aa3eb\",\"trace_id\":\"5a09aee9-5f75-4bfa-93e4-df0ff76f2326\"}\n[2026-05-28 08:29:09] local.NOTICE: Monitoring end {\"correlation_id\":\"f559463e-0c12-46df-8b53-0c50cf4aa3eb\",\"trace_id\":\"5a09aee9-5f75-4bfa-93e4-df0ff76f2326\"}\n[2026-05-28 08:29:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"9b1cc2f1-0c8a-4aa0-a9e8-57ac879839e4\",\"trace_id\":\"d69dd133-b5f0-4568-81ef-9cce3d1c5c9e\"}\n[2026-05-28 08:29:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"9b1cc2f1-0c8a-4aa0-a9e8-57ac879839e4\",\"trace_id\":\"d69dd133-b5f0-4568-81ef-9cce3d1c5c9e\"}\n[2026-05-28 08:29:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"87e10603-9e6f-4d71-97ab-5ba67103e5fd\",\"trace_id\":\"0aae0afb-779f-4999-892d-0188e14b3dcb\"}\n[2026-05-28 08:29:12] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"87e10603-9e6f-4d71-97ab-5ba67103e5fd\",\"trace_id\":\"0aae0afb-779f-4999-892d-0188e14b3dcb\"}\n[2026-05-28 08:29:12] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"87e10603-9e6f-4d71-97ab-5ba67103e5fd\",\"trace_id\":\"0aae0afb-779f-4999-892d-0188e14b3dcb\"}\n[2026-05-28 08:29:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"87e10603-9e6f-4d71-97ab-5ba67103e5fd\",\"trace_id\":\"0aae0afb-779f-4999-892d-0188e14b3dcb\"}\n[2026-05-28 08:30:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"200b132b-d8db-4010-b968-fe5e3fcc42f1\",\"trace_id\":\"7457715a-3b9d-4c35-a499-455c83e925ab\"}\n[2026-05-28 08:30:04] local.INFO: [ScheduleBotCommand] Dispatched activities to capture {\"count\":0} {\"correlation_id\":\"200b132b-d8db-4010-b968-fe5e3fcc42f1\",\"trace_id\":\"7457715a-3b9d-4c35-a499-455c83e925ab\"}\n[2026-05-28 08:30:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"200b132b-d8db-4010-b968-fe5e3fcc42f1\",\"trace_id\":\"7457715a-3b9d-4c35-a499-455c83e925ab\"}\n[2026-05-28 08:30:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"a94ef973-dee7-4bdf-99aa-9ff06f5a5561\",\"trace_id\":\"4e37013f-2993-4c71-87d9-560f8f429d27\"}\n[2026-05-28 08:30:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"a94ef973-dee7-4bdf-99aa-9ff06f5a5561\",\"trace_id\":\"4e37013f-2993-4c71-87d9-560f8f429d27\"}\n[2026-05-28 08:30:07] local.NOTICE: Monitoring start {\"correlation_id\":\"691e7103-784d-4b4a-8036-f3c624a186db\",\"trace_id\":\"991dd947-ed80-4080-8769-8ee7efc4e6a4\"}\n[2026-05-28 08:30:07] local.NOTICE: Monitoring end {\"correlation_id\":\"691e7103-784d-4b4a-8036-f3c624a186db\",\"trace_id\":\"991dd947-ed80-4080-8769-8ee7efc4e6a4\"}\n[2026-05-28 08:30:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"00cf8914-d440-4826-a079-ee4586e76c62\",\"trace_id\":\"d77009ee-8d7b-44ae-bb21-2080b6d48c6c\"}\n[2026-05-28 08:30:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"00cf8914-d440-4826-a079-ee4586e76c62\",\"trace_id\":\"d77009ee-8d7b-44ae-bb21-2080b6d48c6c\"}\n[2026-05-28 08:30:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"7c9550d0-42ed-4b1d-b5b3-7dca67f743ec\",\"trace_id\":\"84177853-9530-4b1e-bde6-3dc0c71883ee\"}\n[2026-05-28 08:30:10] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"7c9550d0-42ed-4b1d-b5b3-7dca67f743ec\",\"trace_id\":\"84177853-9530-4b1e-bde6-3dc0c71883ee\"}\n[2026-05-28 08:30:10] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"7c9550d0-42ed-4b1d-b5b3-7dca67f743ec\",\"trace_id\":\"84177853-9530-4b1e-bde6-3dc0c71883ee\"}\n[2026-05-28 08:30:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"7c9550d0-42ed-4b1d-b5b3-7dca67f743ec\",\"trace_id\":\"84177853-9530-4b1e-bde6-3dc0c71883ee\"}\n[2026-05-28 08:30:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"85cc6c92-262b-45c5-bbcc-4a084137fde3\",\"trace_id\":\"058bc9a3-67af-49b6-a87c-8a0a0bc0c47a\"}\n[2026-05-28 08:30:12] local.INFO: Running conference:monitor:count command for activities in (2026-05-28 08:28:00, 2026-05-28 08:30:00] {\"correlation_id\":\"85cc6c92-262b-45c5-bbcc-4a084137fde3\",\"trace_id\":\"058bc9a3-67af-49b6-a87c-8a0a0bc0c47a\"}\n[2026-05-28 08:30:12] local.INFO: [conference:monitor:count] No activities found in (2026-05-28 08:28:00, 2026-05-28 08:30:00] {\"correlation_id\":\"85cc6c92-262b-45c5-bbcc-4a084137fde3\",\"trace_id\":\"058bc9a3-67af-49b6-a87c-8a0a0bc0c47a\"}\n[2026-05-28 08:30:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"85cc6c92-262b-45c5-bbcc-4a084137fde3\",\"trace_id\":\"058bc9a3-67af-49b6-a87c-8a0a0bc0c47a\"}\n[2026-05-28 08:30:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"5083d024-d669-4c6a-bde3-ce084d6a628c\",\"trace_id\":\"83110e1f-4309-4f4a-b8bb-eab5c0ec081e\"}\n[2026-05-28 08:30:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"5083d024-d669-4c6a-bde3-ce084d6a628c\",\"trace_id\":\"83110e1f-4309-4f4a-b8bb-eab5c0ec081e\"}\n[2026-05-28 08:30:15] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"bc529b16-9502-4df3-b2c0-c63fcf977ed5\",\"trace_id\":\"9dd5d6d0-4cc5-454c-a0c8-0e72b69eeb29\"}\n[2026-05-28 08:30:15] local.INFO: [TextRelayService] Starting sync {\"mailbox\":\"catch-all@txt.staging.jiminny.com\",\"expected_alias\":\"catch-all\",\"expected_host\":\"txt.staging.jiminny.com\"} {\"correlation_id\":\"bc529b16-9502-4df3-b2c0-c63fcf977ed5\",\"trace_id\":\"9dd5d6d0-4cc5-454c-a0c8-0e72b69eeb29\"}\n[2026-05-28 08:30:15] local.INFO: [TextRelayService] Sync completed {\"mailbox\":\"catch-all@txt.staging.jiminny.com\",\"messages_processed\":0,\"message_ids\":[]} {\"correlation_id\":\"bc529b16-9502-4df3-b2c0-c63fcf977ed5\",\"trace_id\":\"9dd5d6d0-4cc5-454c-a0c8-0e72b69eeb29\"}\n[2026-05-28 08:30:15] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"bc529b16-9502-4df3-b2c0-c63fcf977ed5\",\"trace_id\":\"9dd5d6d0-4cc5-454c-a0c8-0e72b69eeb29\"}\n[2026-05-28 08:30:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:pre-meeting-notification\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"b655bca8-effa-420b-8686-95ba96f27085\",\"trace_id\":\"b1f43a19-c7ad-428e-9933-4e509b5f0055\"}\n[2026-05-28 08:30:18] local.INFO: Running pre-meeting notification command {\"correlation_id\":\"b655bca8-effa-420b-8686-95ba96f27085\",\"trace_id\":\"b1f43a19-c7ad-428e-9933-4e509b5f0055\"}\n[2026-05-28 08:30:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:pre-meeting-notification\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"b655bca8-effa-420b-8686-95ba96f27085\",\"trace_id\":\"b1f43a19-c7ad-428e-9933-4e509b5f0055\"}\n[2026-05-28 08:30:20] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:start\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"5a8493c4-3ef7-4453-98b7-f32225d453fe\",\"trace_id\":\"056f1aea-8825-4e2c-8239-7804086e504e\"}\n[2026-05-28 08:30:20] local.INFO: Running conference:monitor:start command for activities in (2026-05-28 08:20:00, 2026-05-28 08:25:00] {\"correlation_id\":\"5a8493c4-3ef7-4453-98b7-f32225d453fe\",\"trace_id\":\"056f1aea-8825-4e2c-8239-7804086e504e\"}\n[2026-05-28 08:30:20] local.INFO: [conference:monitor:start] No activities found in (2026-05-28 08:20:00, 2026-05-28 08:25:00] {\"correlation_id\":\"5a8493c4-3ef7-4453-98b7-f32225d453fe\",\"trace_id\":\"056f1aea-8825-4e2c-8239-7804086e504e\"}\n[2026-05-28 08:30:20] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:start\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"5a8493c4-3ef7-4453-98b7-f32225d453fe\",\"trace_id\":\"056f1aea-8825-4e2c-8239-7804086e504e\"}\n[2026-05-28 08:30:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:end\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"18a2aed9-1aae-4b10-a7fa-f75a147db499\",\"trace_id\":\"fff0c9b2-1c60-4402-b30a-2f41d9b45db3\"}\n[2026-05-28 08:30:22] local.INFO: conference:monitor:end:Jiminny\\Console\\Commands\\Activities\\MonitorMeetingEndCommand::logActivitiesEnded {\"from\":\"08:25\",\"to\":\"08:30\"} {\"correlation_id\":\"18a2aed9-1aae-4b10-a7fa-f75a147db499\",\"trace_id\":\"fff0c9b2-1c60-4402-b30a-2f41d9b45db3\"}\n[2026-05-28 08:30:22] local.INFO: conference:monitor:end:Jiminny\\Console\\Commands\\Activities\\MonitorMeetingEndCommand::logActivitiesWithUnfinishedSession {\"from\":\"22:20\",\"to\":\"22:25\"} {\"correlation_id\":\"18a2aed9-1aae-4b10-a7fa-f75a147db499\",\"trace_id\":\"fff0c9b2-1c60-4402-b30a-2f41d9b45db3\"}\n[2026-05-28 08:30:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:end\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"18a2aed9-1aae-4b10-a7fa-f75a147db499\",\"trace_id\":\"fff0c9b2-1c60-4402-b30a-2f41d9b45db3\"}\n[2026-05-28 08:30:23] local.NOTICE: Repairing HubSpot tokens start {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:23] local.INFO: Trying to refresh HubSpot token {\"account_id\":59,\"updated_at\":\"2025-10-03 09:32:05\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:23] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:23] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":59,\"provider\":\"hubspot\",\"refreshToken\":\"97b78f6e2cc49965c00c2492b602b02708b1392551e6b3f113fbaa48992af90b\",\"state\":\"full-refresh\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:26] local.ERROR: Failed to refresh HubSpot token {\"account_id\":59,\"updated_at\":\"2025-10-03 09:32:05\",\"reason\":\"missing or invalid refresh token\",\"previous\":\"\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:26] local.INFO: Trying to refresh HubSpot token {\"account_id\":306,\"updated_at\":\"2023-11-27 09:30:03\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:26] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":306,\"provider\":\"hubspot\",\"refreshToken\":\"6fa6aa8cc641d131231acc3470f5c03cb3b07b2e580fb18f8acb3b1dbb72549b\",\"state\":\"full-refresh\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:27] local.ERROR: Failed to refresh HubSpot token {\"account_id\":306,\"updated_at\":\"2023-11-27 09:30:03\",\"reason\":\"missing or invalid refresh token\",\"previous\":\"\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:27] local.INFO: Trying to refresh HubSpot token {\"account_id\":1372,\"updated_at\":\"2025-10-02 14:47:06\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:27] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:27] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1372,\"provider\":\"hubspot\",\"refreshToken\":\"9aa73948c761da29dce46c177cf9aee1fde483a44169ca38723f9f0597d7a8c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:27] local.ERROR: Failed to refresh HubSpot token {\"account_id\":1372,\"updated_at\":\"2025-10-02 14:47:06\",\"reason\":\"missing or invalid refresh token\",\"previous\":\"\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:27] local.NOTICE: Repairing HubSpot tokens end {\"total\":3,\"fixed\":0,\"failed\":3} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:33] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"jiminny:transcription:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"f73071c7-8696-459f-8aff-e61a7c6b30e6\",\"trace_id\":\"d72fc2ce-5064-4877-b401-10891378e1bc\"}\n[2026-05-28 08:30:33] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:pre-meeting-reminder\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"720d5c87-3d13-4193-87c9-efb8dc6df7c8\",\"trace_id\":\"a9d9c9fe-db29-44a1-a5f6-1905d48dde90\"}\n[2026-05-28 08:30:33] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"jiminny:transcription:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"f73071c7-8696-459f-8aff-e61a7c6b30e6\",\"trace_id\":\"d72fc2ce-5064-4877-b401-10891378e1bc\"}\n[2026-05-28 08:30:33] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:33] local.INFO: [HubSpot Journal Command] Starting polling service {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:33] local.INFO: [HubSpot Journal Polling] Service starting {\"memory_limit\":\"256M\",\"max_execution_time\":\"0\",\"initial_memory_mb\":62.0} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:33] local.INFO: [HubSpot Journal Polling] Acquired polling lock {\"expires_at\":\"2026-05-28T08:32:33.444809Z\"} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:33] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:33] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:33] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:pre-meeting-reminder\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"720d5c87-3d13-4193-87c9-efb8dc6df7c8\",\"trace_id\":\"a9d9c9fe-db29-44a1-a5f6-1905d48dde90\"}\n[2026-05-28 08:30:33] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:35] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:reset-governor\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"c692a62b-3b1a-4ff3-b965-388a7d12f619\",\"trace_id\":\"d1bdfdec-73bd-499f-a31e-b25a864da976\"}\n[2026-05-28 08:30:35] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:reset-governor\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"c692a62b-3b1a-4ff3-b965-388a7d12f619\",\"trace_id\":\"d1bdfdec-73bd-499f-a31e-b25a864da976\"}\n[2026-05-28 08:30:38] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:38] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:38] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:39] local.INFO: Dispatching activity sync job {\"import_id\":814238,\"provider\":\"twilio-flex\",\"team\":\"jiminny\"} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:39] local.INFO: Dispatching activity sync job {\"import_id\":814239,\"provider\":\"xant\",\"team\":\"jiminny\"} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:39] local.INFO: Dispatching activity sync job {\"import_id\":814240,\"provider\":\"apollo\",\"team\":\"jiminny\"} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:39] local.INFO: Dispatching activity sync job {\"import_id\":814241,\"provider\":\"groove\",\"team\":\"jiminny\"} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:39] local.INFO: Dispatching activity sync job {\"import_id\":814242,\"provider\":\"twilio-video\",\"team\":\"jiminny\"} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:39] local.INFO: Dispatching activity sync job {\"import_id\":814243,\"provider\":\"hubspot\",\"team\":\"hubspot\"} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:fail-stalled\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"f6d797df-5275-4cf1-8ddd-300c125df8da\",\"trace_id\":\"2b46c31f-aced-4324-a70a-6395f0a72618\"}\n[2026-05-28 08:30:42] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:fail-stalled\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"f6d797df-5275-4cf1-8ddd-300c125df8da\",\"trace_id\":\"2b46c31f-aced-4324-a70a-6395f0a72618\"}\n[2026-05-28 08:30:42] local.WARNING: [Salesforce] Account not connected for user {\"userId\":\"cdf8b554-d951-4758-bc2b-c1b85d1cd0b9\",\"account\":null} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"salesforce\",\"crm_owner\":3,\"team_id\":1} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [CrmOwnerResolver] TeamMember found with active crm connection {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1194,\"provider\":\"twilio-flex\"} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1194,\"provider\":\"twilio-flex\"} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [SyncActivity] Start {\"import_id\":814238,\"provider\":\"twilio-flex\",\"provider_id\":317,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.NOTICE: [TwilioFlex] Calls import start {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.ALERT: [SyncActivity] Failed {\"import_id\":814238,\"provider\":\"twilio-flex\",\"provider_id\":317,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"[HTTP 401] Unable to fetch page: Authenticate\",\"file\":\"/home/jiminny/vendor/twilio/sdk/src/Twilio/Page.php\",\"line\":60} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [SyncActivity] Start {\"import_id\":814239,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [Salesforce] Performing query {\"query\":\"\n SELECT Playbooks_Call_Date__c,Playbooks_Call_Recording__c,CreatedDate,TaskSubtype,CallType,CallDurationInSeconds,Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type\n FROM Task\n WHERE IsDeleted = false\n AND LastModifiedDate >= :from\n AND LastModifiedDate <= :to\n ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-05-28T08:14:00Z\",\"to\":\"2026-05-28T08:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+Playbooks_Call_Date__c%2CPlaybooks_Call_Recording__c%2CCreatedDate%2CTaskSubtype%2CCallType%2CCallDurationInSeconds%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-05-28T08%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-05-28T08%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:44] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:44] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:44] local.INFO: [Xant (InsideSales)] No calls found. {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SyncActivity] End {\"import_id\":814239,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SyncActivity] Memory usage {\"import_id\":814239,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1,\"memory_usage\":24080216,\"memory_real_usage\":65011712,\"pid\":21708} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SyncActivity] Start {\"import_id\":814240,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [Salesforce] Performing query {\"query\":\"\n SELECT AccountId,CreatedDate,TaskSubtype,CallType,Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type\n FROM Task\n WHERE IsDeleted = false\n AND LastModifiedDate >= :from\n AND LastModifiedDate <= :to\n ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-05-28T08:14:00Z\",\"to\":\"2026-05-28T08:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+AccountId%2CCreatedDate%2CTaskSubtype%2CCallType%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-05-28T08%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-05-28T08%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [Apollo] No calls found. {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SyncActivity] End {\"import_id\":814240,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SyncActivity] Memory usage {\"import_id\":814240,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1,\"memory_usage\":24242184,\"memory_real_usage\":65011712,\"pid\":21708} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SyncActivity] Start {\"import_id\":814241,\"provider\":\"groove\",\"provider_id\":228,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [Salesforce] Performing query {\"query\":\"\n SELECT call_recording_url__c,TaskSubtype,CreatedDate,CallType,CallDurationInSeconds,Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type\n FROM Task\n WHERE IsDeleted = false\n AND LastModifiedDate >= :from\n AND LastModifiedDate <= :to\n ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-05-28T08:14:00Z\",\"to\":\"2026-05-28T08:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+call_recording_url__c%2CTaskSubtype%2CCreatedDate%2CCallType%2CCallDurationInSeconds%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-05-28T08%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-05-28T08%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:bullhorn:ping\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"b2b37795-cfa8-472a-b681-322ce27c0e08\",\"trace_id\":\"605e54f2-d4f9-4683-b3fd-6c1c23e691b7\"}\n[2026-05-28 08:30:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:bullhorn:ping\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"b2b37795-cfa8-472a-b681-322ce27c0e08\",\"trace_id\":\"605e54f2-d4f9-4683-b3fd-6c1c23e691b7\"}\n[2026-05-28 08:30:44] local.ERROR: [Salesforce] Request exception [400] \nSELECT call_recording_url__c,TaskSubtype\n ^\nERROR at Row:1:Column:8\nNo such column 'call_recording_url__c' on entity 'Task'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names. {\"url\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+call_recording_url__c%2CTaskSubtype%2CCreatedDate%2CCallType%2CCallDurationInSeconds%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-05-28T08%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-05-28T08%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000\",\"data\":{\"headers\":{\"Authorization\":\"Bearer 00D2g0000008hH4!AQEAQHWUK6nshi8dSCX9CJphxkVivcv_TK98TLOckVy4i0Z1gQfAoRRuH2LVwDXDkbTPs80wvQjczjAlOz0UEtbHoORBLi2u\"}},\"response\":{\"GuzzleHttp\\\\Psr7\\\\Stream\":\"[{\\\"message\\\":\\\"\\\\nSELECT call_recording_url__c,TaskSubtype\\\\n ^\\\\nERROR at Row:1:Column:8\\\\nNo such column 'call_recording_url__c' on entity 'Task'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names.\\\",\\\"errorCode\\\":\\\"INVALID_FIELD\\\"}]\"},\"fields\":[]} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.ALERT: [SyncActivity] Failed {\"import_id\":814241,\"provider\":\"groove\",\"provider_id\":228,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"\nSELECT call_recording_url__c,TaskSubtype\n ^\nERROR at Row:1:Column:8\nNo such column 'call_recording_url__c' on entity 'Task'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names.\",\"file\":\"/home/jiminny/app/Services/Crm/Salesforce/Client.php\",\"line\":564} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SyncActivity] Start {\"import_id\":814242,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [Salesforce] Performing query {\"query\":\"SELECT Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type,twilio_call_sid__c,Lead_UUID__c,Opportunity__c\n FROM Task\n WHERE Type = 'Video'\n AND isClosed = true\n AND IsDeleted = false\n AND LastModifiedDate >= :from\n AND twilio_call_sid__c != NULL AND LastModifiedDate <= :to ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-05-28T08:14:00Z\",\"to\":\"2026-05-28T08:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=SELECT+Id%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%2Ctwilio_call_sid__c%2CLead_UUID__c%2COpportunity__c%0A++++++++++++++FROM+Task%0A++++++++++++WHERE+Type+%3D+%27Video%27%0A++++++++++++++AND+isClosed+%3D+true%0A++++++++++++++AND+IsDeleted+%3D+false%0A++++++++++++++AND+LastModifiedDate+%3E%3D+2026-05-28T08%3A14%3A00Z%0A++++++++++++++AND+twilio_call_sid__c+%21%3D+NULL+AND+LastModifiedDate+%3C%3D+2026-05-28T08%3A30%3A00Z+ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [Twilio Video] No calls found. {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SyncActivity] End {\"import_id\":814242,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SyncActivity] Memory usage {\"import_id\":814242,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1,\"memory_usage\":24634032,\"memory_real_usage\":65011712,\"pid\":21708} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":408,\"provider\":\"hubspot\"} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":408,\"provider\":\"hubspot\"} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SyncActivity] Start {\"import_id\":814243,\"provider\":\"hubspot\",\"provider_id\":31,\"team\":\"hubspot\",\"team_id\":2} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [HubSpot] Search calls for period {\"from\":\"2026-05-28 08:14:00\",\"to\":\"2026-05-28 08:30:00\"} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SyncActivity] End {\"import_id\":814243,\"provider\":\"hubspot\",\"provider_id\":31,\"team\":\"hubspot\",\"team_id\":2} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SyncActivity] Memory usage {\"import_id\":814243,\"provider\":\"hubspot\",\"provider_id\":31,\"team\":\"hubspot\",\"team_id\":2,\"memory_usage\":24947696,\"memory_real_usage\":65011712,\"pid\":21708} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:46] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"nudges:send\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"f3e257b6-82bb-440c-bf76-855ac528e4e8\",\"trace_id\":\"f8a31218-7290-45ac-8fe1-ffb1b8527521\"}\n[2026-05-28 08:30:46] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"nudges:send\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"f3e257b6-82bb-440c-bf76-855ac528e4e8\",\"trace_id\":\"f8a31218-7290-45ac-8fe1-ffb1b8527521\"}\n[2026-05-28 08:30:50] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"jiminny:playlists:normalize-sort\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"a64b1836-79a1-41da-a4b0-bc0a9703a94b\",\"trace_id\":\"c53faedc-a7a7-41b7-8e51-e84dc2755d79\"}\n[2026-05-28 08:30:50] local.INFO: [Jiminny\\Component\\Playlist\\Command\\NormalizeSortCommand::handle] starting. {\"playlists\":[]} {\"correlation_id\":\"a64b1836-79a1-41da-a4b0-bc0a9703a94b\",\"trace_id\":\"c53faedc-a7a7-41b7-8e51-e84dc2755d79\"}\n[2026-05-28 08:30:50] local.INFO: [Jiminny\\Component\\Playlist\\Command\\NormalizeSortCommand::handle] finished. {\"normalizedPlaylists\":[],\"deletedPlaylists\":[]} {\"correlation_id\":\"a64b1836-79a1-41da-a4b0-bc0a9703a94b\",\"trace_id\":\"c53faedc-a7a7-41b7-8e51-e84dc2755d79\"}\n[2026-05-28 08:30:50] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"jiminny:playlists:normalize-sort\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"a64b1836-79a1-41da-a4b0-bc0a9703a94b\",\"trace_id\":\"c53faedc-a7a7-41b7-8e51-e84dc2755d79\"}\n[2026-05-28 08:30:59] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:59] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:59] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"41db30ba-33c5-46f1-b8bd-5d9e86a70d12\",\"trace_id\":\"a5e2028d-ca41-4ba1-9ddd-a246f83ef4cc\"}\n[2026-05-28 08:31:07] local.INFO: [ScheduleBotCommand] Dispatched activities to capture {\"count\":0} {\"correlation_id\":\"41db30ba-33c5-46f1-b8bd-5d9e86a70d12\",\"trace_id\":\"a5e2028d-ca41-4ba1-9ddd-a246f83ef4cc\"}\n[2026-05-28 08:31:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"41db30ba-33c5-46f1-b8bd-5d9e86a70d12\",\"trace_id\":\"a5e2028d-ca41-4ba1-9ddd-a246f83ef4cc\"}\n[2026-05-28 08:31:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"f6ed0a05-caa1-4cb6-ae83-63f44ae4e0d6\",\"trace_id\":\"16cd898d-57a1-4b74-90d1-f9223bbd8d7a\"}\n[2026-05-28 08:31:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"f6ed0a05-caa1-4cb6-ae83-63f44ae4e0d6\",\"trace_id\":\"16cd898d-57a1-4b74-90d1-f9223bbd8d7a\"}\n[2026-05-28 08:31:15] local.NOTICE: Monitoring start {\"correlation_id\":\"cdf384ea-4f59-4411-8ebf-efc299e4a1c8\",\"trace_id\":\"b069d3c8-2f77-4698-bd53-97de373652e8\"}\n[2026-05-28 08:31:15] local.NOTICE: Monitoring end {\"correlation_id\":\"cdf384ea-4f59-4411-8ebf-efc299e4a1c8\",\"trace_id\":\"b069d3c8-2f77-4698-bd53-97de373652e8\"}\n[2026-05-28 08:31:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"940a264b-4aee-40a0-a527-e101ea555a38\",\"trace_id\":\"cd9169ad-4c1e-452d-b7cb-66005aae4dc8\"}\n[2026-05-28 08:31:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"940a264b-4aee-40a0-a527-e101ea555a38\",\"trace_id\":\"cd9169ad-4c1e-452d-b7cb-66005aae4dc8\"}\n[2026-05-28 08:31:23] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"b798f381-71f2-4c51-a135-f91f09330cfd\",\"trace_id\":\"b44ed325-23f7-4f8b-a447-4c8d0ee99d01\"}\n[2026-05-28 08:31:23] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"b798f381-71f2-4c51-a135-f91f09330cfd\",\"trace_id\":\"b44ed325-23f7-4f8b-a447-4c8d0ee99d01\"}\n[2026-05-28 08:31:24] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"b798f381-71f2-4c51-a135-f91f09330cfd\",\"trace_id\":\"b44ed325-23f7-4f8b-a447-4c8d0ee99d01\"}\n[2026-05-28 08:31:24] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"b798f381-71f2-4c51-a135-f91f09330cfd\",\"trace_id\":\"b44ed325-23f7-4f8b-a447-4c8d0ee99d01\"}\n[2026-05-28 08:31:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:sync-hubspot-objects\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"2d631328-f2b9-44f3-83d3-613b7b9b9ddc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:sync-hubspot-objects\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"2d631328-f2b9-44f3-83d3-613b7b9b9ddc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"usage\":22563136,\"real_usage\":65011712,\"pid\":21703} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [HubSpot] Syncing opportunities using strategy: lastModified {\"team\":2} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.4,\"average_seconds_per_request\":0.4} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [HubSpot] Synced opportunities {\"team\":2,\"strategies\":\"lastModified\",\"sync_count\":0,\"total\":0,\"last_synced_id\":null,\"duration_ms\":432.6} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"provider\":\"hubspot\",\"status\":\"completed\",\"duration_ms\":543.02,\"usage\":22983640,\"real_usage\":65011712,\"pid\":21703} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"usage\":22961848,\"real_usage\":65011712,\"pid\":21703} {\"correlation_id\":\"32799697-dd65-4ad4-b1d1-cd553c4510f8\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"2ac0447f-3c8c-4ce0-baeb-b63ddb76fa9b\",\"account\":null} {\"correlation_id\":\"32799697-dd65-4ad4-b1d1-cd553c4510f8\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":130,\"team_id\":42} {\"correlation_id\":\"32799697-dd65-4ad4-b1d1-cd553c4510f8\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"32799697-dd65-4ad4-b1d1-cd553c4510f8\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"32799697-dd65-4ad4-b1d1-cd553c4510f8\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":23.85,\"usage\":23024000,\"real_usage\":65011712,\"pid\":21703,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"32799697-dd65-4ad4-b1d1-cd553c4510f8\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"usage\":22984760,\"real_usage\":65011712,\"pid\":21703} {\"correlation_id\":\"0732b08b-2bf7-4f5b-93b6-9b2889269535\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"33e34a7a-1c02-4f04-87ac-22c3a385e6e3\",\"account\":{\"Jiminny\\\\Models\\\\SocialAccount\":{\"id\":306,\"sociable_id\":109,\"provider_user_id\":\"11348452\",\"expires\":1701077403,\"refresh_token_expires\":null,\"provider\":\"hubspot\",\"state\":\"full-refresh\",\"auth_scope\":null,\"retry_after\":null,\"created_at\":\"2020-09-01 16:59:04\",\"updated_at\":\"2023-11-27 09:30:03\"}}} {\"correlation_id\":\"0732b08b-2bf7-4f5b-93b6-9b2889269535\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":109,\"team_id\":29} {\"correlation_id\":\"0732b08b-2bf7-4f5b-93b6-9b2889269535\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"0732b08b-2bf7-4f5b-93b6-9b2889269535\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"0732b08b-2bf7-4f5b-93b6-9b2889269535\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":14.98,\"usage\":23047624,\"real_usage\":65011712,\"pid\":21703,\"reason\":\"Your HubSpot account has become disconnected. Please login to Jiminny to reconnect.\"} {\"correlation_id\":\"0732b08b-2bf7-4f5b-93b6-9b2889269535\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:29] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:29] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:29] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:29] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:29] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:29] local.INFO: [HubSpot Journal Polling] Service ending {\"runtime_seconds\":56,\"total_cycles\":5,\"files_downloaded\":0,\"empty_files\":0,\"other_portal_skipped\":0,\"total_events\":0,\"events_per_file\":0,\"avg_api_ms\":192.1,\"avg_download_ms\":0.0,\"avg_transform_ms\":0.0,\"avg_process_ms\":0.0,\"peak_memory_mb\":99.88} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:29] local.INFO: [HubSpot Journal Polling] Released polling lock {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:29] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"usage\":23005136,\"real_usage\":65011712,\"pid\":21703} {\"correlation_id\":\"4f83a1ac-418f-4f49-8377-328b614306fc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:29] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"71e3aac5-fb66-47c5-a236-2d051ae3e319\",\"account\":null} {\"correlation_id\":\"4f83a1ac-418f-4f49-8377-328b614306fc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:29] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":256,\"team_id\":49} {\"correlation_id\":\"4f83a1ac-418f-4f49-8377-328b614306fc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:29] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"4f83a1ac-418f-4f49-8377-328b614306fc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:29] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"4f83a1ac-418f-4f49-8377-328b614306fc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:29] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":34.51,\"usage\":23021248,\"real_usage\":65011712,\"pid\":21703,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"4f83a1ac-418f-4f49-8377-328b614306fc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:32:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"35f98b1b-7b16-4e79-a420-7e9818097d24\",\"trace_id\":\"d66deb58-fa13-486f-a866-2cac402dc589\"}\n[2026-05-28 08:32:06] local.INFO: [ScheduleBotCommand] Dispatched activities to capture {\"count\":0} {\"correlation_id\":\"35f98b1b-7b16-4e79-a420-7e9818097d24\",\"trace_id\":\"d66deb58-fa13-486f-a866-2cac402dc589\"}\n[2026-05-28 08:32:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"35f98b1b-7b16-4e79-a420-7e9818097d24\",\"trace_id\":\"d66deb58-fa13-486f-a866-2cac402dc589\"}\n[2026-05-28 08:32:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"bc6784c1-c46f-4295-be09-87334f4baa39\",\"trace_id\":\"74b00236-4ed0-4464-8b07-99c328ab6461\"}\n[2026-05-28 08:32:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"bc6784c1-c46f-4295-be09-87334f4baa39\",\"trace_id\":\"74b00236-4ed0-4464-8b07-99c328ab6461\"}\n[2026-05-28 08:32:11] local.NOTICE: Monitoring start {\"correlation_id\":\"caf4d1dd-2a52-40f5-834a-f9148f642e83\",\"trace_id\":\"1312bdcc-7e8b-480a-a935-2433fb79dc70\"}\n[2026-05-28 08:32:11] local.NOTICE: Monitoring end {\"correlation_id\":\"caf4d1dd-2a52-40f5-834a-f9148f642e83\",\"trace_id\":\"1312bdcc-7e8b-480a-a935-2433fb79dc70\"}\n[2026-05-28 08:32:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"64d86889-1f0d-4f7d-a1ac-6f56e58e775e\",\"trace_id\":\"b184d66b-49f8-40ec-afbe-2e76e9c7d952\"}\n[2026-05-28 08:32:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"64d86889-1f0d-4f7d-a1ac-6f56e58e775e\",\"trace_id\":\"b184d66b-49f8-40ec-afbe-2e76e9c7d952\"}\n[2026-05-28 08:32:20] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"8e6dd4bc-4f44-4fb2-b2ba-9e2f385996ef\",\"trace_id\":\"7767e0ca-37a3-43a5-972e-ea9a1bedea2f\"}\n[2026-05-28 08:32:20] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"8e6dd4bc-4f44-4fb2-b2ba-9e2f385996ef\",\"trace_id\":\"7767e0ca-37a3-43a5-972e-ea9a1bedea2f\"}\n[2026-05-28 08:32:20] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"8e6dd4bc-4f44-4fb2-b2ba-9e2f385996ef\",\"trace_id\":\"7767e0ca-37a3-43a5-972e-ea9a1bedea2f\"}\n[2026-05-28 08:32:20] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"8e6dd4bc-4f44-4fb2-b2ba-9e2f385996ef\",\"trace_id\":\"7767e0ca-37a3-43a5-972e-ea9a1bedea2f\"}\n[2026-05-28 08:32:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"6750ab25-6dff-4e7b-9f6e-bc7d1fc4221a\",\"trace_id\":\"3db6ce9b-167d-4214-a32c-67c611aa2bbe\"}\n[2026-05-28 08:32:22] local.INFO: Running conference:monitor:count command for activities in (2026-05-28 08:30:00, 2026-05-28 08:32:00] {\"correlation_id\":\"6750ab25-6dff-4e7b-9f6e-bc7d1fc4221a\",\"trace_id\":\"3db6ce9b-167d-4214-a32c-67c611aa2bbe\"}\n[2026-05-28 08:32:22] local.INFO: [conference:monitor:count] No activities found in (2026-05-28 08:30:00, 2026-05-28 08:32:00] {\"correlation_id\":\"6750ab25-6dff-4e7b-9f6e-bc7d1fc4221a\",\"trace_id\":\"3db6ce9b-167d-4214-a32c-67c611aa2bbe\"}\n[2026-05-28 08:32:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"6750ab25-6dff-4e7b-9f6e-bc7d1fc4221a\",\"trace_id\":\"3db6ce9b-167d-4214-a32c-67c611aa2bbe\"}\n[2026-05-28 08:32:23] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"04510c2c-638c-4687-81d7-b12e76f41df3\",\"trace_id\":\"87f0bb69-8fd1-47f1-9c9e-3b0ac3d6bd60\"}\n[2026-05-28 08:32:23] local.INFO: [EmailSchedule] STARTING batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"04510c2c-638c-4687-81d7-b12e76f41df3\",\"trace_id\":\"87f0bb69-8fd1-47f1-9c9e-3b0ac3d6bd60\"}\n[2026-05-28 08:32:23] local.INFO: [EmailSchedule] FINISHED batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"04510c2c-638c-4687-81d7-b12e76f41df3\",\"trace_id\":\"87f0bb69-8fd1-47f1-9c9e-3b0ac3d6bd60\"}\n[2026-05-28 08:32:23] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"04510c2c-638c-4687-81d7-b12e76f41df3\",\"trace_id\":\"87f0bb69-8fd1-47f1-9c9e-3b0ac3d6bd60\"}\n[2026-05-28 08:32:25] local.INFO: [Jiminny\\Jobs\\Mailbox\\CreateBatches] processed 1 inboxes and created 0 batches {\"userId\":null,\"batchSize\":30,\"maxBatches\":1000} {\"correlation_id\":\"e60597ef-aca8-4bda-bb90-18f01e96874d\",\"trace_id\":\"87f0bb69-8fd1-47f1-9c9e-3b0ac3d6bd60\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-37482055105397747
|
3613057122943418527
|
visual_change
|
accessibility
|
NULL
|
remote: Resolving deltas: 100% (10/10), completed remote: Resolving deltas: 100% (10/10), completed with 10 local objects.
2 files committed
JY-20915 fix missing header
text/html
text/html
text/html
Edit Commit Message…
Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
1
15
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
103
Previous Highlighted Error
Next Highlighted Error
[2026-05-28 08:28:04] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.883} {"correlation_id":"73199e23-a066-42ba-b189-2802e0869a44","trace_id":"e53672c2-0f50-409d-af25-84ab485c505d"}
[2026-05-28 08:28:04] local.INFO: [ScheduleBotCommand] Dispatched activities to capture {"count":0} {"correlation_id":"73199e23-a066-42ba-b189-2802e0869a44","trace_id":"e53672c2-0f50-409d-af25-84ab485c505d"}
[2026-05-28 08:28:04] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.883,"memoryPeakAfterCommandInMB":99.883} {"correlation_id":"73199e23-a066-42ba-b189-2802e0869a44","trace_id":"e53672c2-0f50-409d-af25-84ab485c505d"}
[2026-05-28 08:28:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.883} {"correlation_id":"741b2c4e-6a44-4c46-b9fb-fce017aef225","trace_id":"804dc547-eb25-4b32-8d0f-c87019773085"}
[2026-05-28 08:28:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.883,"memoryPeakAfterCommandInMB":99.883} {"correlation_id":"741b2c4e-6a44-4c46-b9fb-fce017aef225","trace_id":"804dc547-eb25-4b32-8d0f-c87019773085"}
[2026-05-28 08:28:09] local.NOTICE: Monitoring start {"correlation_id":"8ae5c131-edd9-463d-8161-875f4d34320f","trace_id":"1b6420c8-ef4e-4566-81b2-688696064648"}
[2026-05-28 08:28:09] local.NOTICE: Monitoring end {"correlation_id":"8ae5c131-edd9-463d-8161-875f4d34320f","trace_id":"1b6420c8-ef4e-4566-81b2-688696064648"}
[2026-05-28 08:28:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.883} {"correlation_id":"6f5390d3-abe7-4e56-a662-aeed1098941b","trace_id":"b57dc2df-2f4d-4d3b-b09a-aa6e61205d5e"}
[2026-05-28 08:28:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.883,"memoryPeakAfterCommandInMB":99.883} {"correlation_id":"6f5390d3-abe7-4e56-a662-aeed1098941b","trace_id":"b57dc2df-2f4d-4d3b-b09a-aa6e61205d5e"}
[2026-05-28 08:28:12] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.883} {"correlation_id":"ba694158-ea28-4ac1-b134-83ac485146e0","trace_id":"83a26b74-baa5-407e-9fc2-2b49d65b1c77"}
[2026-05-28 08:28:12] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"ba694158-ea28-4ac1-b134-83ac485146e0","trace_id":"83a26b74-baa5-407e-9fc2-2b49d65b1c77"}
[2026-05-28 08:28:12] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"ba694158-ea28-4ac1-b134-83ac485146e0","trace_id":"83a26b74-baa5-407e-9fc2-2b49d65b1c77"}
[2026-05-28 08:28:12] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.883,"memoryPeakAfterCommandInMB":99.883} {"correlation_id":"ba694158-ea28-4ac1-b134-83ac485146e0","trace_id":"83a26b74-baa5-407e-9fc2-2b49d65b1c77"}
[2026-05-28 08:28:14] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"conference:monitor:count","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.883} {"correlation_id":"b79833c7-3eab-4eb7-98bb-42359039b8fb","trace_id":"a8e159bc-867d-40af-bf7d-1228cf6d4f16"}
[2026-05-28 08:28:14] local.INFO: Running conference:monitor:count command for activities in (2026-05-28 08:26:00, 2026-05-28 08:28:00] {"correlation_id":"b79833c7-3eab-4eb7-98bb-42359039b8fb","trace_id":"a8e159bc-867d-40af-bf7d-1228cf6d4f16"}
[2026-05-28 08:28:14] local.INFO: [conference:monitor:count] No activities found in (2026-05-28 08:26:00, 2026-05-28 08:28:00] {"correlation_id":"b79833c7-3eab-4eb7-98bb-42359039b8fb","trace_id":"a8e159bc-867d-40af-bf7d-1228cf6d4f16"}
[2026-05-28 08:28:14] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"conference:monitor:count","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.883,"memoryPeakAfterCommandInMB":99.883} {"correlation_id":"b79833c7-3eab-4eb7-98bb-42359039b8fb","trace_id":"a8e159bc-867d-40af-bf7d-1228cf6d4f16"}
[2026-05-28 08:28:16] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:retry-failed","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.883} {"correlation_id":"d51071d5-ead0-45f0-a07e-e3234074f884","trace_id":"77173d9f-596f-405f-ae55-ba117b66455d"}
[2026-05-28 08:28:16] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"calendar:sync","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.883} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:16] local.NOTICE: Calendar sync start {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:16] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:retry-failed","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.883,"memoryPeakAfterCommandInMB":99.883} {"correlation_id":"d51071d5-ead0-45f0-a07e-e3234074f884","trace_id":"77173d9f-596f-405f-ae55-ba117b66455d"}
[2026-05-28 08:28:16] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1393,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:16] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1393,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:16] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:16] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1393,"provider":"google","refreshToken":"5aa7e2d96b53201cd16fca5d2e4ef3ad03320971fc064781d18aee3ae7b99fbf","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1393,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1393,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1387,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1387,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1387,"provider":"google","refreshToken":"8157ac6de94842937194009e9c50e459253600f799dacf6a40755ffdbeb5bba6","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1387,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1387,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1348,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1348,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1348,"provider":"google","refreshToken":"9e7d13d3032d0cb1b79d8e95aef01383e8e91eb52ff8ee960c8a0b6b95cd8c73","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1348,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1348,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1361,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1361,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1361,"provider":"google","refreshToken":"6c843da199c2b9907445329304fcc4ec5057a4ee748d8299641764395c08e1fd","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1361,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1361,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1310,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1310,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1310,"provider":"google","refreshToken":"e34818922c2830a660813a63f6169a4a9a992ae2cccd7dc8dd7796cfdb470ef1","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1310,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1310,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1333,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1333,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1333,"provider":"google","refreshToken":"6c902986546d8e8da1dc539b046cdc1d458f519acc972e5b5f1d6a1a295165e0","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1333,"provider":"google","responseBody":{"error":"unauthorized_client","error_description":"Unauthorized"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1333,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1368,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1368,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1368,"provider":"google","refreshToken":"d2f128898ff8543bd16b69cfae37896ab85119b0f5ed2b431d739593bb600333","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1368,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1368,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1365,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1365,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1365,"provider":"google","refreshToken":"7676e4a9afcd082b413248ab5ec6e487021fec6a9bdf315860a59cefad9caad8","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1365,"provider":"google","responseBody":{"error":"unauthorized_client","error_description":"Unauthorized"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1365,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1364,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1364,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1364,"provider":"google","refreshToken":"dd5882ebce76e645292ce33ae74238abbb77c0a4ecc6a2bfe723cad82e72ba8e","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1364,"provider":"google","responseBody":{"error":"unauthorized_client","error_description":"Unauthorized"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1364,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1370,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1370,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1370,"provider":"office","refreshToken":"b7ee8035306d0043cea6e00e7c4fe14f745e44074a1194db62a31cdf8b70af3e","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1370,"provider":"office","responseBody":"{\"error\":\"invalid_client\",\"error_description\":\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 41d8db5c-a91f-41a6-9b5c-22a6c99b9100 Correlation ID: 70bc0a6f-1565-4e74-94dc-419d94108fa9 Timestamp: 2026-05-28 08:28:18Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-28 08:28:18Z\",\"trace_id\":\"41d8db5c-a91f-41a6-9b5c-22a6c99b9100\",\"correlation_id\":\"70bc0a6f-1565-4e74-94dc-419d94108fa9\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1370,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1202,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1202,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1202,"provider":"office","refreshToken":"b458799ccc29b21a6e2eb5260fdb63e49ccba21bf942a3973fb63799bd7f0afe","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1202,"provider":"office","responseBody":"{\"error\":\"invalid_client\",\"error_description\":\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: d3d14dfc-1b70-40c4-b9db-fab12f748300 Correlation ID: 4c5f746c-8366-44c1-8733-6877607826db Timestamp: 2026-05-28 08:28:19Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-28 08:28:19Z\",\"trace_id\":\"d3d14dfc-1b70-40c4-b9db-fab12f748300\",\"correlation_id\":\"4c5f746c-8366-44c1-8733-6877607826db\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1202,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1502,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1502,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: Calendar sync job dispatched {"calendar_id":501} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1300,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1300,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1300,"provider":"google","refreshToken":"4b811db0725fd9602a95943519a7da935e2a5065da7d9ebfcb170752e3e1ddb8","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1300,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1300,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1409,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1409,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1409,"provider":"google","refreshToken":"e2a3f2d06894894eed1ee87d9db1ace77d4d42ee6e1288a8940ad2c10333b0c4","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1409,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1409,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1352,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1352,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1352,"provider":"google","refreshToken":"dd4b16b00fdc1216da6b717c02338c073636e29162826b2de6db3f064fc029eb","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1352,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1352,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1296,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1296,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1296,"provider":"office","refreshToken":"011ae723c9d800c674e0b4be76f49fc046dac7d501b66c59ef0d9549cfa56ae5","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1502,"provider":"google"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1502,"provider":"google"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [Calendar] Processing sync {"calendarId":"a33076c1-8d97-431a-99f0-85c9524e118b","from":null,"to":null,"delta":"CIiFh8TP44kDEIiFh8TP44kDGAUgkZvkzgIokZvkzgI=","last_sync":"2024-12-09 07:12:53","dateMode":"daily"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"integration-app","crm_owner":1695,"team_id":3143} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1502,"provider":"google"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1502,"provider":"google"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1296,"provider":"office","responseBody":"{\"error\":\"invalid_client\",\"error_description\":\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: dd83a147-95bc-407c-8c9b-3c14e7cb7d00 Correlation ID: bb16bae3-b3c4-467e-bd95-4e7ab85364c9 Timestamp: 2026-05-28 08:28:20Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-28 08:28:20Z\",\"trace_id\":\"dd83a147-95bc-407c-8c9b-3c14e7cb7d00\",\"correlation_id\":\"bb16bae3-b3c4-467e-bd95-4e7ab85364c9\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1296,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":391,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":391,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":391,"provider":"office","refreshToken":"00045eebae0f39b34887c6d53f92ae78064f7145e1f4b67754aebd03cfb2d881","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [Google Calendar] Failed to watch channel for calendar {"calendarId":"a33076c1-8d97-431a-99f0-85c9524e118b","code":400,"reason":"{
\"error\": {
\"errors\": [
{
\"domain\": \"global\",
\"reason\": \"push.webhookUrlNotHttps\",
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
],
\"code\": 400,
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
}"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.WARNING: [Calendar] Sync failed {"calendarId":"a33076c1-8d97-431a-99f0-85c9524e118b","code":400,"reason":"{
\"error\": {
\"errors\": [
{
\"domain\": \"global\",
\"reason\": \"push.webhookUrlNotHttps\",
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
],
\"code\": 400,
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
}"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":391,"provider":"office","responseBody":"{\"error\":\"invalid_client\",\"error_description\":\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 587224ad-f597-4371-b2ba-00fcd2da1b00 Correlation ID: 4e752610-95b1-4606-847f-68e00f90be0a Timestamp: 2026-05-28 08:28:21Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-28 08:28:21Z\",\"trace_id\":\"587224ad-f597-4371-b2ba-00fcd2da1b00\",\"correlation_id\":\"4e752610-95b1-4606-847f-68e00f90be0a\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:21] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":391,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1271,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1271,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:21] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Re...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
81616
|
2832
|
22
|
2026-05-28T08:32:38.316955+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957158316_m1.jpg...
|
PhpStorm
|
Push Commits to app
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp‹$0A-zshscreenpipe"DOCKERO ₴1DEV (docker)worker-calendar:worker-calendar_00:startedworker-conferences:worker-conferences_00: startedworker-crm-sync:worker-crm-sync_00: startedworker-crm-update:worker-crm-update_00: startedworker-download:worker-download_00:startedworker-emails:worker-emails_00:startedworker-es-update:worker-es-update_00:startedworker-nudges:worker-nudges_00: started₴2-zshО 84-zshWhat's next:Try Docker Debug forLearnseamless, persistentdebugging tools in any container or image → docker debug docker_lamp_1moreat [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-fix-missing-header-text-relay) $csfixdocker exec -it docker_lamp_1•/vendor/bin/php-cs-fixer fix--config=.php-cs-fixer.dist.php-v--using-cache=no --diffPHP CS Fixer 3.95.1 Adalbertus by Fabien Potencier, Dariusz Ruminski and contributors.PHP runtime: 8.5.5Loaded config default from ".php-cs-fixer.dist.php".Running analysis on 7 cores with 10 files per process.5698/5698 C80100%*5ec2-user@ip-10-30-1...O 86100% <78• Thu 28 May 11:32:37181ec2-user@ip-10-30-140-...$7Fixed 0 of 5698 files in 52.731 seconds, 799.06 MB memory usedDetected deprecations in use (they will stop working in next major release):- Rule set"@PHP74Migration" is deprecated. Use"@PHP7x4Migration" instead.- Rule set "@PHP80Migration" is deprecated. Use "@PHP8x®Migration" instead.- Rule set "@PHP81Migration" is deprecated. Use "@PHP8x1Migration" instead.- Rule set "@PHP82Migration" is deprecated. Use "@PHP8x2Migration" instead.- Rule set "@PHP83Migration" is deprecated. Use "@PHP8x3Migration"instead.- Rule set "@PHP84Migration" is deprecated. Use "@PHP8x4Migration" instead.What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image + docker debug docker_lamp_1Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-fix-missing-header-text-relay) $D...
|
NULL
|
-438629903361006697
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp‹$0A-zshscreenpipe"DOCKERO ₴1DEV (docker)worker-calendar:worker-calendar_00:startedworker-conferences:worker-conferences_00: startedworker-crm-sync:worker-crm-sync_00: startedworker-crm-update:worker-crm-update_00: startedworker-download:worker-download_00:startedworker-emails:worker-emails_00:startedworker-es-update:worker-es-update_00:startedworker-nudges:worker-nudges_00: started₴2-zshО 84-zshWhat's next:Try Docker Debug forLearnseamless, persistentdebugging tools in any container or image → docker debug docker_lamp_1moreat [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-fix-missing-header-text-relay) $csfixdocker exec -it docker_lamp_1•/vendor/bin/php-cs-fixer fix--config=.php-cs-fixer.dist.php-v--using-cache=no --diffPHP CS Fixer 3.95.1 Adalbertus by Fabien Potencier, Dariusz Ruminski and contributors.PHP runtime: 8.5.5Loaded config default from ".php-cs-fixer.dist.php".Running analysis on 7 cores with 10 files per process.5698/5698 C80100%*5ec2-user@ip-10-30-1...O 86100% <78• Thu 28 May 11:32:37181ec2-user@ip-10-30-140-...$7Fixed 0 of 5698 files in 52.731 seconds, 799.06 MB memory usedDetected deprecations in use (they will stop working in next major release):- Rule set"@PHP74Migration" is deprecated. Use"@PHP7x4Migration" instead.- Rule set "@PHP80Migration" is deprecated. Use "@PHP8x®Migration" instead.- Rule set "@PHP81Migration" is deprecated. Use "@PHP8x1Migration" instead.- Rule set "@PHP82Migration" is deprecated. Use "@PHP8x2Migration" instead.- Rule set "@PHP83Migration" is deprecated. Use "@PHP8x3Migration"instead.- Rule set "@PHP84Migration" is deprecated. Use "@PHP8x4Migration" instead.What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image + docker debug docker_lamp_1Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-fix-missing-header-text-relay) $D...
|
81611
|
NULL
|
NULL
|
NULL
|
|
81615
|
2833
|
24
|
2026-05-28T08:32:38.213774+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957158213_m2.jpg...
|
PhpStorm
|
Push Commits to app
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Show Diff
Group By
Jump to Source
Show Details
Exp Show Diff
Group By
Jump to Source
Show Details
Expand All
Collapse All
For Commit and Push to non-protected branches, preview commits before push
Help
Push tags:
All
Current Branch
Cancel
Push
Push
Push Commits to app...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Show Diff","depth":2,"bounds":{"left":0.61269945,"top":0.31524342,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"bounds":{"left":0.6236702,"top":0.31524342,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"bounds":{"left":0.63231385,"top":0.31524342,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Show Details","depth":2,"bounds":{"left":0.64328456,"top":0.31524342,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"bounds":{"left":0.72539896,"top":0.31524342,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"bounds":{"left":0.7340425,"top":0.31524342,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"For Commit and Push to non-protected branches, preview commits before push","depth":1,"bounds":{"left":0.48304522,"top":0.6783719,"width":0.17287233,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"bounds":{"left":0.4820479,"top":0.7047087,"width":0.00930851,"height":0.027134877},"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Push tags:","depth":1,"bounds":{"left":0.49800533,"top":0.70790106,"width":0.031914894,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"All","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Current Branch","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Cancel","depth":1,"bounds":{"left":0.67586434,"top":0.7047087,"width":0.025930852,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Push","depth":1,"bounds":{"left":0.7037899,"top":0.7047087,"width":0.036236703,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Push","depth":2,"bounds":{"left":0.7037899,"top":0.7047087,"width":0.025930852,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Push Commits to app","depth":1,"bounds":{"left":0.5874335,"top":0.29449323,"width":0.04720745,"height":0.012769354},"on_screen":true,"role_description":"text"}]...
|
-8198980968265644364
|
-1800661210003872170
|
click
|
hybrid
|
NULL
|
Show Diff
Group By
Jump to Source
Show Details
Exp Show Diff
Group By
Jump to Source
Show Details
Expand All
Collapse All
For Commit and Push to non-protected branches, preview commits before push
Help
Push tags:
All
Current Branch
Cancel
Push
Push
Push Commits to app
PhpStormProjectvPwovscorsViewNeweNNCCoocKetucio$2 JY-20915-fx-mWindowTO0У L7inu co mey trrscis© SyneMailbox.php©InternetMessagelnterface.phg© TextRelayServiceTest.phpfyminny.oheE env.production© MailChannelService.phpclass TextReLayService©Textrekysewice.oneoavare tuncczon csroncurehcchvarohaehtMeetingGeneratoranowcadon#OAuth2Playbooks—KeCaLA& DockerfieE laravellog X 4 SF jiminny@localhost)A console [PROD]A console (EU]& console (STAGING)ler henwserwiet+0 .1 catch (\Exception $e) (Log: :error (message:)" [TextRelayService) Failed to inspect message', t'nessage_id' => Snessageid,'exception' => Se-›getMessageO).[2826-85-28 08:28:04] Locat. INFO: Jiminny\Console\Connands\Comnand: :run Menory usage before starting comnand (*co(2826-85-28 08:28:04] Local. INFO: [ScheduleBotConnand) Dispatched activities to capture ("count":0} ("correlation_id*:*7319%2026-05-28 08.28:04 Locat.INFU: Jiminny Console Conmanos Connand::run nenory usage fon conmand connand:seeeino-boc:sc2826-85-28 88:28:86 Local.INF0: Jiminny Console Connands|Comnand::run Menory usage before starting command "command":"dé:12826-05-28 08:28:66 Local.IWFU: Jininny Console Connands\Comnand: :run Menory usage for conmand ("connand": "dialecs:monito)7826-85-78 88:28:89Loca.wurfet:hontcoring star.correlarzon10: 0a050131-0009-4050-0161-87574054520fTKrace10017826-85-78 88:28:89 Local.Nurat: honzzoring eno{"correlation_id*:"8aeSc131-edd9-463d-8161-87Sf4d34320f*,7826-85-78Locau,IWFU: Janinny Console comnands comand::run henory usaac bcrore stariing command comaand: ha(2826-85-28 08:28:10] Local.INFO: Jininny\Console|Connands\Connand::run Menory usage for command {*co88:28:24 Loca,WFU: Jaminny console connands connand::run henory usage berore stariina connand conn.Push Commits to [EMAIL] #L154-188Thought for 1s>eexKeayeMicesthontJotaeoStreaminga TeamTelephony#UserPilot#WebhookC AbstractSemvice cno©ActivityProviderFactory.phpCActviysemce cno© ApiResponseService.phpCeonarsneasaries ood©InsightSeatService.php© InstantMeetingService.php© IntercomService.php©lpapiClient.php(Sentry::captureException(Se);Read TextRelayServiceTest.php #L150-199thought for 16Sasrched testistor@urrent Environment.allsBackio ohesder in teste/UntSamices Moiln extRelay Semvice est.oho.227= 13= 14I need to check if there's a duplicate test now. Let me search for all test methods in the file:prsvate function natchesExpectedRecipfent(string Srecipient, string SoxpectedALias, string SexpectecHoSparts = explode( serif (count(Sparts) la= 2 ll strcasecno(Spartsii], SexpectedHost) (a= 0)€return false.2117:76085028 8827[2826-05-28 08:28inoo:Son8 88e[2026-85-28 88:28[2826-85-28 08:28[2026-05-28 08:2€[2826-85-28 08:28[2826-85-28 88:28GandAROR AR-2Яv 0 2 files~ D app/Services/Mail 1 file© TextRelayService.phpv D tests/Unit/Services/Mail 1 file© TextRelayServiceTest.phpD TextRelayServiceTest.php+1-1+ →Code SWE-1,60.0v Changes o tues, uodalino.F .env.local app© JiminnyDebugCommand.php app/Console/Commandsme logging.php config@ PlaybackService.pho app/Services© TextRelayService.php app/Services/Mail© TextRelayServiceTest.php tests/Unit/Services/Mail› Unversioned Files 9 files→Side-by-side viewer+8828865ed app/Services/try 171 • ?SotterenceSnessage = Sservice-›users_nessages->get(Smailbox, Snessageld):Sheaders = Smessage->getPay2oad()->getHeaders():-To") €foreach (Sheaders as Sheader) (1f (Sheader-»name === "X-Gm-Original-To') €Snatches = Sthis-›natchesExpectedRecipient(Sheader->valve, SexpectedAlias, SexpectFor Commit and Push to non-protected branches, preview commits before pushPush tags:if (1 Snatches) (// Sanitize PII by renoving plus-tag content for loggingSsanitizedOriginalTo = explode(*+', Sheader->value)[e]:Log: : info('[TextRelayService) Refused nessage'.'nessage_id' => Snessageld,'original_to_sanitized' => SsanitizedOriginalto,Push1f (Srecipient I==Snatches = Sthis-›natchesExpectedRecipient(Srecipient, SexpectedAlias, SexpectedHost);D):returratches.if ( Smatches) 4// Sanitize PII by removing plus-tag content for loggingssanitizelecipient = explode(*+*, Srecipient)[0];Log:: infoC' (TextRelayService) Refused message'.ortoiniilo sinkelceLookaaoninald mextRelaySerycel Refused message: miesiing Xec-ucioiinstelo hesden"UTE-R OuA enod...
|
81614
|
NULL
|
NULL
|
NULL
|
|
81614
|
2833
|
23
|
2026-05-28T08:32:35.117294+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957155117_m2.jpg...
|
PhpStorm
|
Push Commits to app
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
JY-20915-fix-missing-header-text-relay → origin : JY-20915-fix-missing-header-text-relay → origin : JY-20915-fix-missing-header-text-relay
JY-20915 fix missing header
Show Diff
Group By
Jump to Source
Show Details
Expand All
Collapse All
2 files, folder
app/Services/Mail 1 file, folder
TextRelayService.php, class
tests/Unit/Services/Mail 1 file, folder
TextRelayServiceTest.php, class
app/Services/Mail 1 file, folder
TextRelayService.php, class
TextRelayService.php, class
tests/Unit/Services/Mail 1 file, folder
TextRelayServiceTest.php, class
TextRelayServiceTest.php, class
For Commit and Push to non-protected branches, preview commits before push
Help
Push tags:
All
Current Branch
Cancel
Push
Push
Push Commits to app...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"JY-20915-fix-missing-header-text-relay → origin : JY-20915-fix-missing-header-text-relay","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.20511968,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"JY-20915 fix missing header","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.20511968,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"bounds":{"left":0.61269945,"top":0.31524342,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"bounds":{"left":0.6236702,"top":0.31524342,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"bounds":{"left":0.63231385,"top":0.31524342,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Show Details","depth":2,"bounds":{"left":0.64328456,"top":0.31524342,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"bounds":{"left":0.72539896,"top":0.31524342,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"bounds":{"left":0.7340425,"top":0.31524342,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2 files, folder","depth":4,"bounds":{"left":0.6203458,"top":0.33838788,"width":0.022273935,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app/Services/Mail 1 file, folder","depth":5,"bounds":{"left":0.62666225,"top":0.35594574,"width":0.05618351,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class","depth":6,"bounds":{"left":0.63297874,"top":0.3735036,"width":0.05285904,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Mail 1 file, folder","depth":5,"bounds":{"left":0.62666225,"top":0.39106146,"width":0.068484046,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class","depth":6,"bounds":{"left":0.63297874,"top":0.4086193,"width":0.061835106,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app/Services/Mail 1 file, folder","depth":4,"bounds":{"left":0.62666225,"top":0.35594574,"width":0.05618351,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class","depth":5,"bounds":{"left":0.63297874,"top":0.3735036,"width":0.05285904,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class","depth":4,"bounds":{"left":0.63297874,"top":0.3735036,"width":0.05285904,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Mail 1 file, folder","depth":4,"bounds":{"left":0.62666225,"top":0.39106146,"width":0.068484046,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class","depth":5,"bounds":{"left":0.63297874,"top":0.4086193,"width":0.061835106,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class","depth":4,"bounds":{"left":0.63297874,"top":0.4086193,"width":0.061835106,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"For Commit and Push to non-protected branches, preview commits before push","depth":1,"bounds":{"left":0.48304522,"top":0.6783719,"width":0.17287233,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"bounds":{"left":0.4820479,"top":0.7047087,"width":0.00930851,"height":0.027134877},"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Push tags:","depth":1,"bounds":{"left":0.49800533,"top":0.70790106,"width":0.031914894,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"All","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Current Branch","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Cancel","depth":1,"bounds":{"left":0.67586434,"top":0.7047087,"width":0.025930852,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Push","depth":1,"bounds":{"left":0.7037899,"top":0.7047087,"width":0.036236703,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Push","depth":2,"bounds":{"left":0.7037899,"top":0.7047087,"width":0.025930852,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Push Commits to app","depth":1,"bounds":{"left":0.5874335,"top":0.29449323,"width":0.04720745,"height":0.012769354},"on_screen":true,"role_description":"text"}]...
|
4832058773799006099
|
-6396795375123516356
|
visual_change
|
accessibility
|
NULL
|
JY-20915-fix-missing-header-text-relay → origin : JY-20915-fix-missing-header-text-relay → origin : JY-20915-fix-missing-header-text-relay
JY-20915 fix missing header
Show Diff
Group By
Jump to Source
Show Details
Expand All
Collapse All
2 files, folder
app/Services/Mail 1 file, folder
TextRelayService.php, class
tests/Unit/Services/Mail 1 file, folder
TextRelayServiceTest.php, class
app/Services/Mail 1 file, folder
TextRelayService.php, class
TextRelayService.php, class
tests/Unit/Services/Mail 1 file, folder
TextRelayServiceTest.php, class
TextRelayServiceTest.php, class
For Commit and Push to non-protected branches, preview commits before push
Help
Push tags:
All
Current Branch
Cancel
Push
Push
Push Commits to app...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
81613
|
2833
|
22
|
2026-05-28T08:32:31.877817+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957151877_m2.jpg...
|
PhpStorm
|
faVsco.js – TextRelayService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Committing…
Project: faVsco.js, menu
JY-20915-fix- Committing…
Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
1
15
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
[2026-05-28 08:28:04] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.883} {"correlation_id":"73199e23-a066-42ba-b189-2802e0869a44","trace_id":"e53672c2-0f50-409d-af25-84ab485c505d"}
[2026-05-28 08:28:04] local.INFO: [ScheduleBotCommand] Dispatched activities to capture {"count":0} {"correlation_id":"73199e23-a066-42ba-b189-2802e0869a44","trace_id":"e53672c2-0f50-409d-af25-84ab485c505d"}
[2026-05-28 08:28:04] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.883,"memoryPeakAfterCommandInMB":99.883} {"correlation_id":"73199e23-a066-42ba-b189-2802e0869a44","trace_id":"e53672c2-0f50-409d-af25-84ab485c505d"}
[2026-05-28 08:28:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.883} {"correlation_id":"741b2c4e-6a44-4c46-b9fb-fce017aef225","trace_id":"804dc547-eb25-4b32-8d0f-c87019773085"}
[2026-05-28 08:28:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.883,"memoryPeakAfterCommandInMB":99.883} {"correlation_id":"741b2c4e-6a44-4c46-b9fb-fce017aef225","trace_id":"804dc547-eb25-4b32-8d0f-c87019773085"}
[2026-05-28 08:28:09] local.NOTICE: Monitoring start {"correlation_id":"8ae5c131-edd9-463d-8161-875f4d34320f","trace_id":"1b6420c8-ef4e-4566-81b2-688696064648"}
[2026-05-28 08:28:09] local.NOTICE: Monitoring end {"correlation_id":"8ae5c131-edd9-463d-8161-875f4d34320f","trace_id":"1b6420c8-ef4e-4566-81b2-688696064648"}
[2026-05-28 08:28:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.883} {"correlation_id":"6f5390d3-abe7-4e56-a662-aeed1098941b","trace_id":"b57dc2df-2f4d-4d3b-b09a-aa6e61205d5e"}
[2026-05-28 08:28:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.883,"memoryPeakAfterCommandInMB":99.883} {"correlation_id":"6f5390d3-abe7-4e56-a662-aeed1098941b","trace_id":"b57dc2df-2f4d-4d3b-b09a-aa6e61205d5e"}
[2026-05-28 08:28:12] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.883} {"correlation_id":"ba694158-ea28-4ac1-b134-83ac485146e0","trace_id":"83a26b74-baa5-407e-9fc2-2b49d65b1c77"}
[2026-05-28 08:28:12] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"ba694158-ea28-4ac1-b134-83ac485146e0","trace_id":"83a26b74-baa5-407e-9fc2-2b49d65b1c77"}
[2026-05-28 08:28:12] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"ba694158-ea28-4ac1-b134-83ac485146e0","trace_id":"83a26b74-baa5-407e-9fc2-2b49d65b1c77"}
[2026-05-28 08:28:12] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.883,"memoryPeakAfterCommandInMB":99.883} {"correlation_id":"ba694158-ea28-4ac1-b134-83ac485146e0","trace_id":"83a26b74-baa5-407e-9fc2-2b49d65b1c77"}
[2026-05-28 08:28:14] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"conference:monitor:count","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.883} {"correlation_id":"b79833c7-3eab-4eb7-98bb-42359039b8fb","trace_id":"a8e159bc-867d-40af-bf7d-1228cf6d4f16"}
[2026-05-28 08:28:14] local.INFO: Running conference:monitor:count command for activities in (2026-05-28 08:26:00, 2026-05-28 08:28:00] {"correlation_id":"b79833c7-3eab-4eb7-98bb-42359039b8fb","trace_id":"a8e159bc-867d-40af-bf7d-1228cf6d4f16"}
[2026-05-28 08:28:14] local.INFO: [conference:monitor:count] No activities found in (2026-05-28 08:26:00, 2026-05-28 08:28:00] {"correlation_id":"b79833c7-3eab-4eb7-98bb-42359039b8fb","trace_id":"a8e159bc-867d-40af-bf7d-1228cf6d4f16"}
[2026-05-28 08:28:14] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"conference:monitor:count","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.883,"memoryPeakAfterCommandInMB":99.883} {"correlation_id":"b79833c7-3eab-4eb7-98bb-42359039b8fb","trace_id":"a8e159bc-867d-40af-bf7d-1228cf6d4f16"}
[2026-05-28 08:28:16] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:retry-failed","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.883} {"correlation_id":"d51071d5-ead0-45f0-a07e-e3234074f884","trace_id":"77173d9f-596f-405f-ae55-ba117b66455d"}
[2026-05-28 08:28:16] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"calendar:sync","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.883} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:16] local.NOTICE: Calendar sync start {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:16] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:retry-failed","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.883,"memoryPeakAfterCommandInMB":99.883} {"correlation_id":"d51071d5-ead0-45f0-a07e-e3234074f884","trace_id":"77173d9f-596f-405f-ae55-ba117b66455d"}
[2026-05-28 08:28:16] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1393,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:16] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1393,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:16] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:16] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1393,"provider":"google","refreshToken":"5aa7e2d96b53201cd16fca5d2e4ef3ad03320971fc064781d18aee3ae7b99fbf","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1393,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1393,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1387,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1387,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1387,"provider":"google","refreshToken":"8157ac6de94842937194009e9c50e459253600f799dacf6a40755ffdbeb5bba6","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1387,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1387,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1348,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1348,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1348,"provider":"google","refreshToken":"9e7d13d3032d0cb1b79d8e95aef01383e8e91eb52ff8ee960c8a0b6b95cd8c73","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1348,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1348,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1361,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1361,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1361,"provider":"google","refreshToken":"6c843da199c2b9907445329304fcc4ec5057a4ee748d8299641764395c08e1fd","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1361,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1361,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1310,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1310,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1310,"provider":"google","refreshToken":"e34818922c2830a660813a63f6169a4a9a992ae2cccd7dc8dd7796cfdb470ef1","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1310,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1310,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1333,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1333,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1333,"provider":"google","refreshToken":"6c902986546d8e8da1dc539b046cdc1d458f519acc972e5b5f1d6a1a295165e0","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1333,"provider":"google","responseBody":{"error":"unauthorized_client","error_description":"Unauthorized"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1333,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1368,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1368,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1368,"provider":"google","refreshToken":"d2f128898ff8543bd16b69cfae37896ab85119b0f5ed2b431d739593bb600333","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1368,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1368,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1365,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1365,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1365,"provider":"google","refreshToken":"7676e4a9afcd082b413248ab5ec6e487021fec6a9bdf315860a59cefad9caad8","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1365,"provider":"google","responseBody":{"error":"unauthorized_client","error_description":"Unauthorized"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1365,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1364,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1364,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1364,"provider":"google","refreshToken":"dd5882ebce76e645292ce33ae74238abbb77c0a4ecc6a2bfe723cad82e72ba8e","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1364,"provider":"google","responseBody":{"error":"unauthorized_client","error_description":"Unauthorized"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1364,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1370,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1370,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1370,"provider":"office","refreshToken":"b7ee8035306d0043cea6e00e7c4fe14f745e44074a1194db62a31cdf8b70af3e","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1370,"provider":"office","responseBody":"{\"error\":\"invalid_client\",\"error_description\":\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 41d8db5c-a91f-41a6-9b5c-22a6c99b9100 Correlation ID: 70bc0a6f-1565-4e74-94dc-419d94108fa9 Timestamp: 2026-05-28 08:28:18Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-28 08:28:18Z\",\"trace_id\":\"41d8db5c-a91f-41a6-9b5c-22a6c99b9100\",\"correlation_id\":\"70bc0a6f-1565-4e74-94dc-419d94108fa9\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1370,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1202,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1202,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1202,"provider":"office","refreshToken":"b458799ccc29b21a6e2eb5260fdb63e49ccba21bf942a3973fb63799bd7f0afe","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1202,"provider":"office","responseBody":"{\"error\":\"invalid_client\",\"error_description\":\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: d3d14dfc-1b70-40c4-b9db-fab12f748300 Correlation ID: 4c5f746c-8366-44c1-8733-6877607826db Timestamp: 2026-05-28 08:28:19Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-28 08:28:19Z\",\"trace_id\":\"d3d14dfc-1b70-40c4-b9db-fab12f748300\",\"correlation_id\":\"4c5f746c-8366-44c1-8733-6877607826db\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1202,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1502,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1502,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: Calendar sync job dispatched {"calendar_id":501} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1300,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1300,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1300,"provider":"google","refreshToken":"4b811db0725fd9602a95943519a7da935e2a5065da7d9ebfcb170752e3e1ddb8","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1300,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1300,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1409,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1409,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1409,"provider":"google","refreshToken":"e2a3f2d06894894eed1ee87d9db1ace77d4d42ee6e1288a8940ad2c10333b0c4","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1409,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1409,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1352,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1352,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1352,"provider":"google","refreshToken":"dd4b16b00fdc1216da6b717c02338c073636e29162826b2de6db3f064fc029eb","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1352,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1352,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1296,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1296,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1296,"provider":"office","refreshToken":"011ae723c9d800c674e0b4be76f49fc046dac7d501b66c59ef0d9549cfa56ae5","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1502,"provider":"google"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1502,"provider":"google"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [Calendar] Processing sync {"calendarId":"a33076c1-8d97-431a-99f0-85c9524e118b","from":null,"to":null,"delta":"CIiFh8TP44kDEIiFh8TP44kDGAUgkZvkzgIokZvkzgI=","last_sync":"2024-12-09 07:12:53","dateMode":"daily"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"integration-app","crm_owner":1695,"team_id":3143} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1502,"provider":"google"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1502,"provider":"google"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1296,"provider":"office","responseBody":"{\"error\":\"invalid_client\",\"error_description\":\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: dd83a147-95bc-407c-8c9b-3c14e7cb7d00 Correlation ID: bb16bae3-b3c4-467e-bd95-4e7ab85364c9 Timestamp: 2026-05-28 08:28:20Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-28 08:28:20Z\",\"trace_id\":\"dd83a147-95bc-407c-8c9b-3c14e7cb7d00\",\"correlation_id\":\"bb16bae3-b3c4-467e-bd95-4e7ab85364c9\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1296,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":391,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":391,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":391,"provider":"office","refreshToken":"00045eebae0f39b34887c6d53f92ae78064f7145e1f4b67754aebd03cfb2d881","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [Google Calendar] Failed to watch channel for calendar {"calendarId":"a33076c1-8d97-431a-99f0-85c9524e118b","code":400,"reason":"{
\"error\": {
\"errors\": [
{
\"domain\": \"global\",
\"reason\": \"push.webhookUrlNotHttps\",
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
],
\"code\": 400,
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
}"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.WARNING: [Calendar] Sync failed {"calendarId":"a33076c1-8d97-431a-99f0-85c9524e118b","code":400,"reason":"{
\"error\": {
\"errors\": [
{
\"domain\": \"global\",
\"reason\": \"push.webhookUrlNotHttps\",
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
],
\"code\": 400,
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
}"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":391,"provider":"office","responseBody":"{\"error\":\"invalid_client\",\"error_description\":\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 587224ad-f597-4371-b2ba-00fcd2da1b00 Correlation ID: 4e752610-95b1-4606-847f-68e00f90be0a Timestamp: 2026-05-28 08:28:21Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-28 08:28:21Z\",\"trace_id\":\"587224ad-f597-4371-b2ba-00fcd2da1b00\",\"correlation_id\":\"4e752610-95b1-4606-847f-68e00f90be0a\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:21] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":391,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1271,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1271,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:21] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1271,"provider":"office","refreshToken":"118cde2c06993147b07ccaec4cbcd5026a819dea6c71081166a492933e392afb","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Committing…","depth":2,"bounds":{"left":0.6625665,"top":0.44134077,"width":0.06948138,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":2,"bounds":{"left":0.6625665,"top":0.47326416,"width":0.06948138,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20915-fix-missing-header-text-relay, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09740692,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20915-fix-missing-header-text-relay","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8374335,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.37632978,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"15","depth":4,"bounds":{"left":0.3856383,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39694148,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40425533,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-28 08:28:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"73199e23-a066-42ba-b189-2802e0869a44\",\"trace_id\":\"e53672c2-0f50-409d-af25-84ab485c505d\"}\n[2026-05-28 08:28:04] local.INFO: [ScheduleBotCommand] Dispatched activities to capture {\"count\":0} {\"correlation_id\":\"73199e23-a066-42ba-b189-2802e0869a44\",\"trace_id\":\"e53672c2-0f50-409d-af25-84ab485c505d\"}\n[2026-05-28 08:28:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"73199e23-a066-42ba-b189-2802e0869a44\",\"trace_id\":\"e53672c2-0f50-409d-af25-84ab485c505d\"}\n[2026-05-28 08:28:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"741b2c4e-6a44-4c46-b9fb-fce017aef225\",\"trace_id\":\"804dc547-eb25-4b32-8d0f-c87019773085\"}\n[2026-05-28 08:28:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"741b2c4e-6a44-4c46-b9fb-fce017aef225\",\"trace_id\":\"804dc547-eb25-4b32-8d0f-c87019773085\"}\n[2026-05-28 08:28:09] local.NOTICE: Monitoring start {\"correlation_id\":\"8ae5c131-edd9-463d-8161-875f4d34320f\",\"trace_id\":\"1b6420c8-ef4e-4566-81b2-688696064648\"}\n[2026-05-28 08:28:09] local.NOTICE: Monitoring end {\"correlation_id\":\"8ae5c131-edd9-463d-8161-875f4d34320f\",\"trace_id\":\"1b6420c8-ef4e-4566-81b2-688696064648\"}\n[2026-05-28 08:28:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"6f5390d3-abe7-4e56-a662-aeed1098941b\",\"trace_id\":\"b57dc2df-2f4d-4d3b-b09a-aa6e61205d5e\"}\n[2026-05-28 08:28:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"6f5390d3-abe7-4e56-a662-aeed1098941b\",\"trace_id\":\"b57dc2df-2f4d-4d3b-b09a-aa6e61205d5e\"}\n[2026-05-28 08:28:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"ba694158-ea28-4ac1-b134-83ac485146e0\",\"trace_id\":\"83a26b74-baa5-407e-9fc2-2b49d65b1c77\"}\n[2026-05-28 08:28:12] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"ba694158-ea28-4ac1-b134-83ac485146e0\",\"trace_id\":\"83a26b74-baa5-407e-9fc2-2b49d65b1c77\"}\n[2026-05-28 08:28:12] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"ba694158-ea28-4ac1-b134-83ac485146e0\",\"trace_id\":\"83a26b74-baa5-407e-9fc2-2b49d65b1c77\"}\n[2026-05-28 08:28:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"ba694158-ea28-4ac1-b134-83ac485146e0\",\"trace_id\":\"83a26b74-baa5-407e-9fc2-2b49d65b1c77\"}\n[2026-05-28 08:28:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"b79833c7-3eab-4eb7-98bb-42359039b8fb\",\"trace_id\":\"a8e159bc-867d-40af-bf7d-1228cf6d4f16\"}\n[2026-05-28 08:28:14] local.INFO: Running conference:monitor:count command for activities in (2026-05-28 08:26:00, 2026-05-28 08:28:00] {\"correlation_id\":\"b79833c7-3eab-4eb7-98bb-42359039b8fb\",\"trace_id\":\"a8e159bc-867d-40af-bf7d-1228cf6d4f16\"}\n[2026-05-28 08:28:14] local.INFO: [conference:monitor:count] No activities found in (2026-05-28 08:26:00, 2026-05-28 08:28:00] {\"correlation_id\":\"b79833c7-3eab-4eb7-98bb-42359039b8fb\",\"trace_id\":\"a8e159bc-867d-40af-bf7d-1228cf6d4f16\"}\n[2026-05-28 08:28:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"b79833c7-3eab-4eb7-98bb-42359039b8fb\",\"trace_id\":\"a8e159bc-867d-40af-bf7d-1228cf6d4f16\"}\n[2026-05-28 08:28:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"d51071d5-ead0-45f0-a07e-e3234074f884\",\"trace_id\":\"77173d9f-596f-405f-ae55-ba117b66455d\"}\n[2026-05-28 08:28:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:16] local.NOTICE: Calendar sync start {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"d51071d5-ead0-45f0-a07e-e3234074f884\",\"trace_id\":\"77173d9f-596f-405f-ae55-ba117b66455d\"}\n[2026-05-28 08:28:16] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:16] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:16] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:16] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1393,\"provider\":\"google\",\"refreshToken\":\"5aa7e2d96b53201cd16fca5d2e4ef3ad03320971fc064781d18aee3ae7b99fbf\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1387,\"provider\":\"google\",\"refreshToken\":\"8157ac6de94842937194009e9c50e459253600f799dacf6a40755ffdbeb5bba6\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1348,\"provider\":\"google\",\"refreshToken\":\"9e7d13d3032d0cb1b79d8e95aef01383e8e91eb52ff8ee960c8a0b6b95cd8c73\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1361,\"provider\":\"google\",\"refreshToken\":\"6c843da199c2b9907445329304fcc4ec5057a4ee748d8299641764395c08e1fd\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1310,\"provider\":\"google\",\"refreshToken\":\"e34818922c2830a660813a63f6169a4a9a992ae2cccd7dc8dd7796cfdb470ef1\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1333,\"provider\":\"google\",\"refreshToken\":\"6c902986546d8e8da1dc539b046cdc1d458f519acc972e5b5f1d6a1a295165e0\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1368,\"provider\":\"google\",\"refreshToken\":\"d2f128898ff8543bd16b69cfae37896ab85119b0f5ed2b431d739593bb600333\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1365,\"provider\":\"google\",\"refreshToken\":\"7676e4a9afcd082b413248ab5ec6e487021fec6a9bdf315860a59cefad9caad8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1364,\"provider\":\"google\",\"refreshToken\":\"dd5882ebce76e645292ce33ae74238abbb77c0a4ecc6a2bfe723cad82e72ba8e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1370,\"provider\":\"office\",\"refreshToken\":\"b7ee8035306d0043cea6e00e7c4fe14f745e44074a1194db62a31cdf8b70af3e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1370,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 41d8db5c-a91f-41a6-9b5c-22a6c99b9100 Correlation ID: 70bc0a6f-1565-4e74-94dc-419d94108fa9 Timestamp: 2026-05-28 08:28:18Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-28 08:28:18Z\\\",\\\"trace_id\\\":\\\"41d8db5c-a91f-41a6-9b5c-22a6c99b9100\\\",\\\"correlation_id\\\":\\\"70bc0a6f-1565-4e74-94dc-419d94108fa9\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1370,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1202,\"provider\":\"office\",\"refreshToken\":\"b458799ccc29b21a6e2eb5260fdb63e49ccba21bf942a3973fb63799bd7f0afe\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1202,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: d3d14dfc-1b70-40c4-b9db-fab12f748300 Correlation ID: 4c5f746c-8366-44c1-8733-6877607826db Timestamp: 2026-05-28 08:28:19Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-28 08:28:19Z\\\",\\\"trace_id\\\":\\\"d3d14dfc-1b70-40c4-b9db-fab12f748300\\\",\\\"correlation_id\\\":\\\"4c5f746c-8366-44c1-8733-6877607826db\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1202,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: Calendar sync job dispatched {\"calendar_id\":501} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1300,\"provider\":\"google\",\"refreshToken\":\"4b811db0725fd9602a95943519a7da935e2a5065da7d9ebfcb170752e3e1ddb8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1409,\"provider\":\"google\",\"refreshToken\":\"e2a3f2d06894894eed1ee87d9db1ace77d4d42ee6e1288a8940ad2c10333b0c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1352,\"provider\":\"google\",\"refreshToken\":\"dd4b16b00fdc1216da6b717c02338c073636e29162826b2de6db3f064fc029eb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1296,\"provider\":\"office\",\"refreshToken\":\"011ae723c9d800c674e0b4be76f49fc046dac7d501b66c59ef0d9549cfa56ae5\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [Calendar] Processing sync {\"calendarId\":\"a33076c1-8d97-431a-99f0-85c9524e118b\",\"from\":null,\"to\":null,\"delta\":\"CIiFh8TP44kDEIiFh8TP44kDGAUgkZvkzgIokZvkzgI=\",\"last_sync\":\"2024-12-09 07:12:53\",\"dateMode\":\"daily\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"integration-app\",\"crm_owner\":1695,\"team_id\":3143} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1296,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: dd83a147-95bc-407c-8c9b-3c14e7cb7d00 Correlation ID: bb16bae3-b3c4-467e-bd95-4e7ab85364c9 Timestamp: 2026-05-28 08:28:20Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-28 08:28:20Z\\\",\\\"trace_id\\\":\\\"dd83a147-95bc-407c-8c9b-3c14e7cb7d00\\\",\\\"correlation_id\\\":\\\"bb16bae3-b3c4-467e-bd95-4e7ab85364c9\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1296,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":391,\"provider\":\"office\",\"refreshToken\":\"00045eebae0f39b34887c6d53f92ae78064f7145e1f4b67754aebd03cfb2d881\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [Google Calendar] Failed to watch channel for calendar {\"calendarId\":\"a33076c1-8d97-431a-99f0-85c9524e118b\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.WARNING: [Calendar] Sync failed {\"calendarId\":\"a33076c1-8d97-431a-99f0-85c9524e118b\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":391,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 587224ad-f597-4371-b2ba-00fcd2da1b00 Correlation ID: 4e752610-95b1-4606-847f-68e00f90be0a Timestamp: 2026-05-28 08:28:21Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-28 08:28:21Z\\\",\\\"trace_id\\\":\\\"587224ad-f597-4371-b2ba-00fcd2da1b00\\\",\\\"correlation_id\\\":\\\"4e752610-95b1-4606-847f-68e00f90be0a\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":391,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1271,\"provider\":\"office\",\"refreshToken\":\"118cde2c06993147b07ccaec4cbcd5026a819dea6c71081166a492933e392afb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1271,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: dd8bf2a7-bc49-43d7-a487-892c51e27600 Correlation ID: 51e3eb6c-c47e-4dd7-91aa-e734fe5b65cf Timestamp: 2026-05-28 08:28:21Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-28 08:28:21Z\\\",\\\"trace_id\\\":\\\"dd8bf2a7-bc49-43d7-a487-892c51e27600\\\",\\\"correlation_id\\\":\\\"51e3eb6c-c47e-4dd7-91aa-e734fe5b65cf\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1271,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1351,\"provider\":\"google\",\"refreshToken\":\"4271d15b9e60a606439caddc68337f783e472c85b03dacff14d1b6dfded9051c\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1366,\"provider\":\"google\",\"refreshToken\":\"ae21385059b2eebfd43f68aecd56eccd702a1aabb6598f1f7ab594ed8af491b4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: Calendar sync job dispatched {\"calendar_id\":378} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: Calendar sync job dispatched {\"calendar_id\":504} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.NOTICE: Calendar sync end {\"retrieved_calendars\":31,\"processed_calendars\":3} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [Calendar] Processing sync {\"calendarId\":\"2676cb6d-f86c-427e-bf78-591e388e3c1e\",\"from\":null,\"to\":null,\"delta\":\"CJ_x49O3jpIDEJ_x49O3jpIDGAUgw67KlwMow67KlwM=\",\"last_sync\":\"2026-01-19 07:48:40\",\"dateMode\":\"daily\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.WARNING: [Pipedrive] Account not connected for user {\"userId\":\"e6538737-e7b4-455f-a37a-3e79b665a220\",\"account\":{\"Jiminny\\\\Models\\\\SocialAccount\":{\"id\":1116,\"sociable_id\":241,\"provider_user_id\":\"19555731\",\"expires\":1779091997,\"refresh_token_expires\":null,\"provider\":\"pipedrive\",\"state\":\"full-refresh\",\"auth_scope\":\"base,deals:full,activities:full,contacts:full,search:read\",\"retry_after\":null,\"created_at\":\"2023-09-08 09:44:29\",\"updated_at\":\"2026-05-18 08:13:32\"}}} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"pipedrive\",\"crm_owner\":241,\"team_id\":19} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.WARNING: [Calendar] CRM disconnected for user so events will not be matched {\"provider\":\"pipedrive\",\"user_id\":241,\"message\":\"Your Pipedrive account has become disconnected. Please login to Jiminny to reconnect.\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [Google Calendar] Failed to watch channel for calendar {\"calendarId\":\"2676cb6d-f86c-427e-bf78-591e388e3c1e\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.WARNING: [Calendar] Sync failed {\"calendarId\":\"2676cb6d-f86c-427e-bf78-591e388e3c1e\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [Calendar] Processing sync {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\",\"from\":null,\"to\":null,\"delta\":\"R0usmcdvmMuZCBYV0hguCBhf4uKqyU9_gdNd-le_nqqau95XAUmZwlIAy2uwtCf5sA67w6NIiDvawUSmVjpTzZO-RsxLdeS-g8Kxy6ODZQxAxafq8AJNx0hJV7T291NUvfaPYqLMv-3je3IdhAPUtXQXkHtYaBAlX_DDBxwc3zYoVVQ8iM9mQkBykdA3sh-KVYPyzi7nK4cZg5Jzpu9tHA.UA7urx3pJyhUhdo_16STIbSNXUrF-qeQVIis9stVUeY\",\"last_sync\":\"2026-05-28 07:44:06\",\"dateMode\":\"daily\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [MS Office Calendar] Skipping delta sync for daily mode {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:29:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"86a65904-a9f8-43c9-b0c3-1e9c8b4b9541\",\"trace_id\":\"800b02ff-3553-46a3-a8c6-c12324a1ce53\"}\n[2026-05-28 08:29:05] local.INFO: [ScheduleBotCommand] Dispatched activities to capture {\"count\":0} {\"correlation_id\":\"86a65904-a9f8-43c9-b0c3-1e9c8b4b9541\",\"trace_id\":\"800b02ff-3553-46a3-a8c6-c12324a1ce53\"}\n[2026-05-28 08:29:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"86a65904-a9f8-43c9-b0c3-1e9c8b4b9541\",\"trace_id\":\"800b02ff-3553-46a3-a8c6-c12324a1ce53\"}\n[2026-05-28 08:29:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"e5162656-5b49-4857-9917-f0cf3ddffe27\",\"trace_id\":\"a3a53cc5-596d-474c-b9c4-f6c5f7ebdfd0\"}\n[2026-05-28 08:29:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"e5162656-5b49-4857-9917-f0cf3ddffe27\",\"trace_id\":\"a3a53cc5-596d-474c-b9c4-f6c5f7ebdfd0\"}\n[2026-05-28 08:29:09] local.NOTICE: Monitoring start {\"correlation_id\":\"f559463e-0c12-46df-8b53-0c50cf4aa3eb\",\"trace_id\":\"5a09aee9-5f75-4bfa-93e4-df0ff76f2326\"}\n[2026-05-28 08:29:09] local.NOTICE: Monitoring end {\"correlation_id\":\"f559463e-0c12-46df-8b53-0c50cf4aa3eb\",\"trace_id\":\"5a09aee9-5f75-4bfa-93e4-df0ff76f2326\"}\n[2026-05-28 08:29:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"9b1cc2f1-0c8a-4aa0-a9e8-57ac879839e4\",\"trace_id\":\"d69dd133-b5f0-4568-81ef-9cce3d1c5c9e\"}\n[2026-05-28 08:29:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"9b1cc2f1-0c8a-4aa0-a9e8-57ac879839e4\",\"trace_id\":\"d69dd133-b5f0-4568-81ef-9cce3d1c5c9e\"}\n[2026-05-28 08:29:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"87e10603-9e6f-4d71-97ab-5ba67103e5fd\",\"trace_id\":\"0aae0afb-779f-4999-892d-0188e14b3dcb\"}\n[2026-05-28 08:29:12] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"87e10603-9e6f-4d71-97ab-5ba67103e5fd\",\"trace_id\":\"0aae0afb-779f-4999-892d-0188e14b3dcb\"}\n[2026-05-28 08:29:12] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"87e10603-9e6f-4d71-97ab-5ba67103e5fd\",\"trace_id\":\"0aae0afb-779f-4999-892d-0188e14b3dcb\"}\n[2026-05-28 08:29:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"87e10603-9e6f-4d71-97ab-5ba67103e5fd\",\"trace_id\":\"0aae0afb-779f-4999-892d-0188e14b3dcb\"}\n[2026-05-28 08:30:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"200b132b-d8db-4010-b968-fe5e3fcc42f1\",\"trace_id\":\"7457715a-3b9d-4c35-a499-455c83e925ab\"}\n[2026-05-28 08:30:04] local.INFO: [ScheduleBotCommand] Dispatched activities to capture {\"count\":0} {\"correlation_id\":\"200b132b-d8db-4010-b968-fe5e3fcc42f1\",\"trace_id\":\"7457715a-3b9d-4c35-a499-455c83e925ab\"}\n[2026-05-28 08:30:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"200b132b-d8db-4010-b968-fe5e3fcc42f1\",\"trace_id\":\"7457715a-3b9d-4c35-a499-455c83e925ab\"}\n[2026-05-28 08:30:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"a94ef973-dee7-4bdf-99aa-9ff06f5a5561\",\"trace_id\":\"4e37013f-2993-4c71-87d9-560f8f429d27\"}\n[2026-05-28 08:30:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"a94ef973-dee7-4bdf-99aa-9ff06f5a5561\",\"trace_id\":\"4e37013f-2993-4c71-87d9-560f8f429d27\"}\n[2026-05-28 08:30:07] local.NOTICE: Monitoring start {\"correlation_id\":\"691e7103-784d-4b4a-8036-f3c624a186db\",\"trace_id\":\"991dd947-ed80-4080-8769-8ee7efc4e6a4\"}\n[2026-05-28 08:30:07] local.NOTICE: Monitoring end {\"correlation_id\":\"691e7103-784d-4b4a-8036-f3c624a186db\",\"trace_id\":\"991dd947-ed80-4080-8769-8ee7efc4e6a4\"}\n[2026-05-28 08:30:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"00cf8914-d440-4826-a079-ee4586e76c62\",\"trace_id\":\"d77009ee-8d7b-44ae-bb21-2080b6d48c6c\"}\n[2026-05-28 08:30:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"00cf8914-d440-4826-a079-ee4586e76c62\",\"trace_id\":\"d77009ee-8d7b-44ae-bb21-2080b6d48c6c\"}\n[2026-05-28 08:30:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"7c9550d0-42ed-4b1d-b5b3-7dca67f743ec\",\"trace_id\":\"84177853-9530-4b1e-bde6-3dc0c71883ee\"}\n[2026-05-28 08:30:10] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"7c9550d0-42ed-4b1d-b5b3-7dca67f743ec\",\"trace_id\":\"84177853-9530-4b1e-bde6-3dc0c71883ee\"}\n[2026-05-28 08:30:10] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"7c9550d0-42ed-4b1d-b5b3-7dca67f743ec\",\"trace_id\":\"84177853-9530-4b1e-bde6-3dc0c71883ee\"}\n[2026-05-28 08:30:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"7c9550d0-42ed-4b1d-b5b3-7dca67f743ec\",\"trace_id\":\"84177853-9530-4b1e-bde6-3dc0c71883ee\"}\n[2026-05-28 08:30:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"85cc6c92-262b-45c5-bbcc-4a084137fde3\",\"trace_id\":\"058bc9a3-67af-49b6-a87c-8a0a0bc0c47a\"}\n[2026-05-28 08:30:12] local.INFO: Running conference:monitor:count command for activities in (2026-05-28 08:28:00, 2026-05-28 08:30:00] {\"correlation_id\":\"85cc6c92-262b-45c5-bbcc-4a084137fde3\",\"trace_id\":\"058bc9a3-67af-49b6-a87c-8a0a0bc0c47a\"}\n[2026-05-28 08:30:12] local.INFO: [conference:monitor:count] No activities found in (2026-05-28 08:28:00, 2026-05-28 08:30:00] {\"correlation_id\":\"85cc6c92-262b-45c5-bbcc-4a084137fde3\",\"trace_id\":\"058bc9a3-67af-49b6-a87c-8a0a0bc0c47a\"}\n[2026-05-28 08:30:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"85cc6c92-262b-45c5-bbcc-4a084137fde3\",\"trace_id\":\"058bc9a3-67af-49b6-a87c-8a0a0bc0c47a\"}\n[2026-05-28 08:30:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"5083d024-d669-4c6a-bde3-ce084d6a628c\",\"trace_id\":\"83110e1f-4309-4f4a-b8bb-eab5c0ec081e\"}\n[2026-05-28 08:30:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"5083d024-d669-4c6a-bde3-ce084d6a628c\",\"trace_id\":\"83110e1f-4309-4f4a-b8bb-eab5c0ec081e\"}\n[2026-05-28 08:30:15] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"bc529b16-9502-4df3-b2c0-c63fcf977ed5\",\"trace_id\":\"9dd5d6d0-4cc5-454c-a0c8-0e72b69eeb29\"}\n[2026-05-28 08:30:15] local.INFO: [TextRelayService] Starting sync {\"mailbox\":\"catch-all@txt.staging.jiminny.com\",\"expected_alias\":\"catch-all\",\"expected_host\":\"txt.staging.jiminny.com\"} {\"correlation_id\":\"bc529b16-9502-4df3-b2c0-c63fcf977ed5\",\"trace_id\":\"9dd5d6d0-4cc5-454c-a0c8-0e72b69eeb29\"}\n[2026-05-28 08:30:15] local.INFO: [TextRelayService] Sync completed {\"mailbox\":\"catch-all@txt.staging.jiminny.com\",\"messages_processed\":0,\"message_ids\":[]} {\"correlation_id\":\"bc529b16-9502-4df3-b2c0-c63fcf977ed5\",\"trace_id\":\"9dd5d6d0-4cc5-454c-a0c8-0e72b69eeb29\"}\n[2026-05-28 08:30:15] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"bc529b16-9502-4df3-b2c0-c63fcf977ed5\",\"trace_id\":\"9dd5d6d0-4cc5-454c-a0c8-0e72b69eeb29\"}\n[2026-05-28 08:30:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:pre-meeting-notification\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"b655bca8-effa-420b-8686-95ba96f27085\",\"trace_id\":\"b1f43a19-c7ad-428e-9933-4e509b5f0055\"}\n[2026-05-28 08:30:18] local.INFO: Running pre-meeting notification command {\"correlation_id\":\"b655bca8-effa-420b-8686-95ba96f27085\",\"trace_id\":\"b1f43a19-c7ad-428e-9933-4e509b5f0055\"}\n[2026-05-28 08:30:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:pre-meeting-notification\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"b655bca8-effa-420b-8686-95ba96f27085\",\"trace_id\":\"b1f43a19-c7ad-428e-9933-4e509b5f0055\"}\n[2026-05-28 08:30:20] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:start\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"5a8493c4-3ef7-4453-98b7-f32225d453fe\",\"trace_id\":\"056f1aea-8825-4e2c-8239-7804086e504e\"}\n[2026-05-28 08:30:20] local.INFO: Running conference:monitor:start command for activities in (2026-05-28 08:20:00, 2026-05-28 08:25:00] {\"correlation_id\":\"5a8493c4-3ef7-4453-98b7-f32225d453fe\",\"trace_id\":\"056f1aea-8825-4e2c-8239-7804086e504e\"}\n[2026-05-28 08:30:20] local.INFO: [conference:monitor:start] No activities found in (2026-05-28 08:20:00, 2026-05-28 08:25:00] {\"correlation_id\":\"5a8493c4-3ef7-4453-98b7-f32225d453fe\",\"trace_id\":\"056f1aea-8825-4e2c-8239-7804086e504e\"}\n[2026-05-28 08:30:20] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:start\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"5a8493c4-3ef7-4453-98b7-f32225d453fe\",\"trace_id\":\"056f1aea-8825-4e2c-8239-7804086e504e\"}\n[2026-05-28 08:30:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:end\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"18a2aed9-1aae-4b10-a7fa-f75a147db499\",\"trace_id\":\"fff0c9b2-1c60-4402-b30a-2f41d9b45db3\"}\n[2026-05-28 08:30:22] local.INFO: conference:monitor:end:Jiminny\\Console\\Commands\\Activities\\MonitorMeetingEndCommand::logActivitiesEnded {\"from\":\"08:25\",\"to\":\"08:30\"} {\"correlation_id\":\"18a2aed9-1aae-4b10-a7fa-f75a147db499\",\"trace_id\":\"fff0c9b2-1c60-4402-b30a-2f41d9b45db3\"}\n[2026-05-28 08:30:22] local.INFO: conference:monitor:end:Jiminny\\Console\\Commands\\Activities\\MonitorMeetingEndCommand::logActivitiesWithUnfinishedSession {\"from\":\"22:20\",\"to\":\"22:25\"} {\"correlation_id\":\"18a2aed9-1aae-4b10-a7fa-f75a147db499\",\"trace_id\":\"fff0c9b2-1c60-4402-b30a-2f41d9b45db3\"}\n[2026-05-28 08:30:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:end\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"18a2aed9-1aae-4b10-a7fa-f75a147db499\",\"trace_id\":\"fff0c9b2-1c60-4402-b30a-2f41d9b45db3\"}\n[2026-05-28 08:30:23] local.NOTICE: Repairing HubSpot tokens start {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:23] local.INFO: Trying to refresh HubSpot token {\"account_id\":59,\"updated_at\":\"2025-10-03 09:32:05\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:23] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:23] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":59,\"provider\":\"hubspot\",\"refreshToken\":\"97b78f6e2cc49965c00c2492b602b02708b1392551e6b3f113fbaa48992af90b\",\"state\":\"full-refresh\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:26] local.ERROR: Failed to refresh HubSpot token {\"account_id\":59,\"updated_at\":\"2025-10-03 09:32:05\",\"reason\":\"missing or invalid refresh token\",\"previous\":\"\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:26] local.INFO: Trying to refresh HubSpot token {\"account_id\":306,\"updated_at\":\"2023-11-27 09:30:03\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:26] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":306,\"provider\":\"hubspot\",\"refreshToken\":\"6fa6aa8cc641d131231acc3470f5c03cb3b07b2e580fb18f8acb3b1dbb72549b\",\"state\":\"full-refresh\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:27] local.ERROR: Failed to refresh HubSpot token {\"account_id\":306,\"updated_at\":\"2023-11-27 09:30:03\",\"reason\":\"missing or invalid refresh token\",\"previous\":\"\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:27] local.INFO: Trying to refresh HubSpot token {\"account_id\":1372,\"updated_at\":\"2025-10-02 14:47:06\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:27] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:27] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1372,\"provider\":\"hubspot\",\"refreshToken\":\"9aa73948c761da29dce46c177cf9aee1fde483a44169ca38723f9f0597d7a8c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:27] local.ERROR: Failed to refresh HubSpot token {\"account_id\":1372,\"updated_at\":\"2025-10-02 14:47:06\",\"reason\":\"missing or invalid refresh token\",\"previous\":\"\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:27] local.NOTICE: Repairing HubSpot tokens end {\"total\":3,\"fixed\":0,\"failed\":3} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:33] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"jiminny:transcription:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"f73071c7-8696-459f-8aff-e61a7c6b30e6\",\"trace_id\":\"d72fc2ce-5064-4877-b401-10891378e1bc\"}\n[2026-05-28 08:30:33] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:pre-meeting-reminder\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"720d5c87-3d13-4193-87c9-efb8dc6df7c8\",\"trace_id\":\"a9d9c9fe-db29-44a1-a5f6-1905d48dde90\"}\n[2026-05-28 08:30:33] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"jiminny:transcription:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"f73071c7-8696-459f-8aff-e61a7c6b30e6\",\"trace_id\":\"d72fc2ce-5064-4877-b401-10891378e1bc\"}\n[2026-05-28 08:30:33] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:33] local.INFO: [HubSpot Journal Command] Starting polling service {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:33] local.INFO: [HubSpot Journal Polling] Service starting {\"memory_limit\":\"256M\",\"max_execution_time\":\"0\",\"initial_memory_mb\":62.0} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:33] local.INFO: [HubSpot Journal Polling] Acquired polling lock {\"expires_at\":\"2026-05-28T08:32:33.444809Z\"} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:33] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:33] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:33] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:pre-meeting-reminder\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"720d5c87-3d13-4193-87c9-efb8dc6df7c8\",\"trace_id\":\"a9d9c9fe-db29-44a1-a5f6-1905d48dde90\"}\n[2026-05-28 08:30:33] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:35] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:reset-governor\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"c692a62b-3b1a-4ff3-b965-388a7d12f619\",\"trace_id\":\"d1bdfdec-73bd-499f-a31e-b25a864da976\"}\n[2026-05-28 08:30:35] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:reset-governor\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"c692a62b-3b1a-4ff3-b965-388a7d12f619\",\"trace_id\":\"d1bdfdec-73bd-499f-a31e-b25a864da976\"}\n[2026-05-28 08:30:38] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:38] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:38] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:39] local.INFO: Dispatching activity sync job {\"import_id\":814238,\"provider\":\"twilio-flex\",\"team\":\"jiminny\"} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:39] local.INFO: Dispatching activity sync job {\"import_id\":814239,\"provider\":\"xant\",\"team\":\"jiminny\"} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:39] local.INFO: Dispatching activity sync job {\"import_id\":814240,\"provider\":\"apollo\",\"team\":\"jiminny\"} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:39] local.INFO: Dispatching activity sync job {\"import_id\":814241,\"provider\":\"groove\",\"team\":\"jiminny\"} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:39] local.INFO: Dispatching activity sync job {\"import_id\":814242,\"provider\":\"twilio-video\",\"team\":\"jiminny\"} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:39] local.INFO: Dispatching activity sync job {\"import_id\":814243,\"provider\":\"hubspot\",\"team\":\"hubspot\"} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:fail-stalled\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"f6d797df-5275-4cf1-8ddd-300c125df8da\",\"trace_id\":\"2b46c31f-aced-4324-a70a-6395f0a72618\"}\n[2026-05-28 08:30:42] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:fail-stalled\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"f6d797df-5275-4cf1-8ddd-300c125df8da\",\"trace_id\":\"2b46c31f-aced-4324-a70a-6395f0a72618\"}\n[2026-05-28 08:30:42] local.WARNING: [Salesforce] Account not connected for user {\"userId\":\"cdf8b554-d951-4758-bc2b-c1b85d1cd0b9\",\"account\":null} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"salesforce\",\"crm_owner\":3,\"team_id\":1} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [CrmOwnerResolver] TeamMember found with active crm connection {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1194,\"provider\":\"twilio-flex\"} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1194,\"provider\":\"twilio-flex\"} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [SyncActivity] Start {\"import_id\":814238,\"provider\":\"twilio-flex\",\"provider_id\":317,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.NOTICE: [TwilioFlex] Calls import start {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.ALERT: [SyncActivity] Failed {\"import_id\":814238,\"provider\":\"twilio-flex\",\"provider_id\":317,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"[HTTP 401] Unable to fetch page: Authenticate\",\"file\":\"/home/jiminny/vendor/twilio/sdk/src/Twilio/Page.php\",\"line\":60} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [SyncActivity] Start {\"import_id\":814239,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [Salesforce] Performing query {\"query\":\"\n SELECT Playbooks_Call_Date__c,Playbooks_Call_Recording__c,CreatedDate,TaskSubtype,CallType,CallDurationInSeconds,Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type\n FROM Task\n WHERE IsDeleted = false\n AND LastModifiedDate >= :from\n AND LastModifiedDate <= :to\n ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-05-28T08:14:00Z\",\"to\":\"2026-05-28T08:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+Playbooks_Call_Date__c%2CPlaybooks_Call_Recording__c%2CCreatedDate%2CTaskSubtype%2CCallType%2CCallDurationInSeconds%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-05-28T08%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-05-28T08%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:44] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:44] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:44] local.INFO: [Xant (InsideSales)] No calls found. {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SyncActivity] End {\"import_id\":814239,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SyncActivity] Memory usage {\"import_id\":814239,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1,\"memory_usage\":24080216,\"memory_real_usage\":65011712,\"pid\":21708} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SyncActivity] Start {\"import_id\":814240,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [Salesforce] Performing query {\"query\":\"\n SELECT AccountId,CreatedDate,TaskSubtype,CallType,Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type\n FROM Task\n WHERE IsDeleted = false\n AND LastModifiedDate >= :from\n AND LastModifiedDate <= :to\n ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-05-28T08:14:00Z\",\"to\":\"2026-05-28T08:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+AccountId%2CCreatedDate%2CTaskSubtype%2CCallType%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-05-28T08%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-05-28T08%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [Apollo] No calls found. {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SyncActivity] End {\"import_id\":814240,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SyncActivity] Memory usage {\"import_id\":814240,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1,\"memory_usage\":24242184,\"memory_real_usage\":65011712,\"pid\":21708} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SyncActivity] Start {\"import_id\":814241,\"provider\":\"groove\",\"provider_id\":228,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [Salesforce] Performing query {\"query\":\"\n SELECT call_recording_url__c,TaskSubtype,CreatedDate,CallType,CallDurationInSeconds,Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type\n FROM Task\n WHERE IsDeleted = false\n AND LastModifiedDate >= :from\n AND LastModifiedDate <= :to\n ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-05-28T08:14:00Z\",\"to\":\"2026-05-28T08:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+call_recording_url__c%2CTaskSubtype%2CCreatedDate%2CCallType%2CCallDurationInSeconds%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-05-28T08%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-05-28T08%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:bullhorn:ping\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"b2b37795-cfa8-472a-b681-322ce27c0e08\",\"trace_id\":\"605e54f2-d4f9-4683-b3fd-6c1c23e691b7\"}\n[2026-05-28 08:30:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:bullhorn:ping\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"b2b37795-cfa8-472a-b681-322ce27c0e08\",\"trace_id\":\"605e54f2-d4f9-4683-b3fd-6c1c23e691b7\"}\n[2026-05-28 08:30:44] local.ERROR: [Salesforce] Request exception [400] \nSELECT call_recording_url__c,TaskSubtype\n ^\nERROR at Row:1:Column:8\nNo such column 'call_recording_url__c' on entity 'Task'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names. {\"url\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+call_recording_url__c%2CTaskSubtype%2CCreatedDate%2CCallType%2CCallDurationInSeconds%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-05-28T08%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-05-28T08%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000\",\"data\":{\"headers\":{\"Authorization\":\"Bearer 00D2g0000008hH4!AQEAQHWUK6nshi8dSCX9CJphxkVivcv_TK98TLOckVy4i0Z1gQfAoRRuH2LVwDXDkbTPs80wvQjczjAlOz0UEtbHoORBLi2u\"}},\"response\":{\"GuzzleHttp\\\\Psr7\\\\Stream\":\"[{\\\"message\\\":\\\"\\\\nSELECT call_recording_url__c,TaskSubtype\\\\n ^\\\\nERROR at Row:1:Column:8\\\\nNo such column 'call_recording_url__c' on entity 'Task'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names.\\\",\\\"errorCode\\\":\\\"INVALID_FIELD\\\"}]\"},\"fields\":[]} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.ALERT: [SyncActivity] Failed {\"import_id\":814241,\"provider\":\"groove\",\"provider_id\":228,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"\nSELECT call_recording_url__c,TaskSubtype\n ^\nERROR at Row:1:Column:8\nNo such column 'call_recording_url__c' on entity 'Task'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names.\",\"file\":\"/home/jiminny/app/Services/Crm/Salesforce/Client.php\",\"line\":564} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SyncActivity] Start {\"import_id\":814242,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [Salesforce] Performing query {\"query\":\"SELECT Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type,twilio_call_sid__c,Lead_UUID__c,Opportunity__c\n FROM Task\n WHERE Type = 'Video'\n AND isClosed = true\n AND IsDeleted = false\n AND LastModifiedDate >= :from\n AND twilio_call_sid__c != NULL AND LastModifiedDate <= :to ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-05-28T08:14:00Z\",\"to\":\"2026-05-28T08:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=SELECT+Id%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%2Ctwilio_call_sid__c%2CLead_UUID__c%2COpportunity__c%0A++++++++++++++FROM+Task%0A++++++++++++WHERE+Type+%3D+%27Video%27%0A++++++++++++++AND+isClosed+%3D+true%0A++++++++++++++AND+IsDeleted+%3D+false%0A++++++++++++++AND+LastModifiedDate+%3E%3D+2026-05-28T08%3A14%3A00Z%0A++++++++++++++AND+twilio_call_sid__c+%21%3D+NULL+AND+LastModifiedDate+%3C%3D+2026-05-28T08%3A30%3A00Z+ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [Twilio Video] No calls found. {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SyncActivity] End {\"import_id\":814242,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SyncActivity] Memory usage {\"import_id\":814242,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1,\"memory_usage\":24634032,\"memory_real_usage\":65011712,\"pid\":21708} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":408,\"provider\":\"hubspot\"} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":408,\"provider\":\"hubspot\"} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SyncActivity] Start {\"import_id\":814243,\"provider\":\"hubspot\",\"provider_id\":31,\"team\":\"hubspot\",\"team_id\":2} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [HubSpot] Search calls for period {\"from\":\"2026-05-28 08:14:00\",\"to\":\"2026-05-28 08:30:00\"} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SyncActivity] End {\"import_id\":814243,\"provider\":\"hubspot\",\"provider_id\":31,\"team\":\"hubspot\",\"team_id\":2} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SyncActivity] Memory usage {\"import_id\":814243,\"provider\":\"hubspot\",\"provider_id\":31,\"team\":\"hubspot\",\"team_id\":2,\"memory_usage\":24947696,\"memory_real_usage\":65011712,\"pid\":21708} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:46] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"nudges:send\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"f3e257b6-82bb-440c-bf76-855ac528e4e8\",\"trace_id\":\"f8a31218-7290-45ac-8fe1-ffb1b8527521\"}\n[2026-05-28 08:30:46] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"nudges:send\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"f3e257b6-82bb-440c-bf76-855ac528e4e8\",\"trace_id\":\"f8a31218-7290-45ac-8fe1-ffb1b8527521\"}\n[2026-05-28 08:30:50] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"jiminny:playlists:normalize-sort\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"a64b1836-79a1-41da-a4b0-bc0a9703a94b\",\"trace_id\":\"c53faedc-a7a7-41b7-8e51-e84dc2755d79\"}\n[2026-05-28 08:30:50] local.INFO: [Jiminny\\Component\\Playlist\\Command\\NormalizeSortCommand::handle] starting. {\"playlists\":[]} {\"correlation_id\":\"a64b1836-79a1-41da-a4b0-bc0a9703a94b\",\"trace_id\":\"c53faedc-a7a7-41b7-8e51-e84dc2755d79\"}\n[2026-05-28 08:30:50] local.INFO: [Jiminny\\Component\\Playlist\\Command\\NormalizeSortCommand::handle] finished. {\"normalizedPlaylists\":[],\"deletedPlaylists\":[]} {\"correlation_id\":\"a64b1836-79a1-41da-a4b0-bc0a9703a94b\",\"trace_id\":\"c53faedc-a7a7-41b7-8e51-e84dc2755d79\"}\n[2026-05-28 08:30:50] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"jiminny:playlists:normalize-sort\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"a64b1836-79a1-41da-a4b0-bc0a9703a94b\",\"trace_id\":\"c53faedc-a7a7-41b7-8e51-e84dc2755d79\"}\n[2026-05-28 08:30:59] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:59] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:59] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"41db30ba-33c5-46f1-b8bd-5d9e86a70d12\",\"trace_id\":\"a5e2028d-ca41-4ba1-9ddd-a246f83ef4cc\"}\n[2026-05-28 08:31:07] local.INFO: [ScheduleBotCommand] Dispatched activities to capture {\"count\":0} {\"correlation_id\":\"41db30ba-33c5-46f1-b8bd-5d9e86a70d12\",\"trace_id\":\"a5e2028d-ca41-4ba1-9ddd-a246f83ef4cc\"}\n[2026-05-28 08:31:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"41db30ba-33c5-46f1-b8bd-5d9e86a70d12\",\"trace_id\":\"a5e2028d-ca41-4ba1-9ddd-a246f83ef4cc\"}\n[2026-05-28 08:31:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"f6ed0a05-caa1-4cb6-ae83-63f44ae4e0d6\",\"trace_id\":\"16cd898d-57a1-4b74-90d1-f9223bbd8d7a\"}\n[2026-05-28 08:31:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"f6ed0a05-caa1-4cb6-ae83-63f44ae4e0d6\",\"trace_id\":\"16cd898d-57a1-4b74-90d1-f9223bbd8d7a\"}\n[2026-05-28 08:31:15] local.NOTICE: Monitoring start {\"correlation_id\":\"cdf384ea-4f59-4411-8ebf-efc299e4a1c8\",\"trace_id\":\"b069d3c8-2f77-4698-bd53-97de373652e8\"}\n[2026-05-28 08:31:15] local.NOTICE: Monitoring end {\"correlation_id\":\"cdf384ea-4f59-4411-8ebf-efc299e4a1c8\",\"trace_id\":\"b069d3c8-2f77-4698-bd53-97de373652e8\"}\n[2026-05-28 08:31:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"940a264b-4aee-40a0-a527-e101ea555a38\",\"trace_id\":\"cd9169ad-4c1e-452d-b7cb-66005aae4dc8\"}\n[2026-05-28 08:31:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"940a264b-4aee-40a0-a527-e101ea555a38\",\"trace_id\":\"cd9169ad-4c1e-452d-b7cb-66005aae4dc8\"}\n[2026-05-28 08:31:23] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"b798f381-71f2-4c51-a135-f91f09330cfd\",\"trace_id\":\"b44ed325-23f7-4f8b-a447-4c8d0ee99d01\"}\n[2026-05-28 08:31:23] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"b798f381-71f2-4c51-a135-f91f09330cfd\",\"trace_id\":\"b44ed325-23f7-4f8b-a447-4c8d0ee99d01\"}\n[2026-05-28 08:31:24] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"b798f381-71f2-4c51-a135-f91f09330cfd\",\"trace_id\":\"b44ed325-23f7-4f8b-a447-4c8d0ee99d01\"}\n[2026-05-28 08:31:24] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"b798f381-71f2-4c51-a135-f91f09330cfd\",\"trace_id\":\"b44ed325-23f7-4f8b-a447-4c8d0ee99d01\"}\n[2026-05-28 08:31:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:sync-hubspot-objects\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"2d631328-f2b9-44f3-83d3-613b7b9b9ddc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:sync-hubspot-objects\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"2d631328-f2b9-44f3-83d3-613b7b9b9ddc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"usage\":22563136,\"real_usage\":65011712,\"pid\":21703} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [HubSpot] Syncing opportunities using strategy: lastModified {\"team\":2} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.4,\"average_seconds_per_request\":0.4} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [HubSpot] Synced opportunities {\"team\":2,\"strategies\":\"lastModified\",\"sync_count\":0,\"total\":0,\"last_synced_id\":null,\"duration_ms\":432.6} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"provider\":\"hubspot\",\"status\":\"completed\",\"duration_ms\":543.02,\"usage\":22983640,\"real_usage\":65011712,\"pid\":21703} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"usage\":22961848,\"real_usage\":65011712,\"pid\":21703} {\"correlation_id\":\"32799697-dd65-4ad4-b1d1-cd553c4510f8\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"2ac0447f-3c8c-4ce0-baeb-b63ddb76fa9b\",\"account\":null} {\"correlation_id\":\"32799697-dd65-4ad4-b1d1-cd553c4510f8\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":130,\"team_id\":42} {\"correlation_id\":\"32799697-dd65-4ad4-b1d1-cd553c4510f8\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"32799697-dd65-4ad4-b1d1-cd553c4510f8\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"32799697-dd65-4ad4-b1d1-cd553c4510f8\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":23.85,\"usage\":23024000,\"real_usage\":65011712,\"pid\":21703,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"32799697-dd65-4ad4-b1d1-cd553c4510f8\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"usage\":22984760,\"real_usage\":65011712,\"pid\":21703} {\"correlation_id\":\"0732b08b-2bf7-4f5b-93b6-9b2889269535\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"33e34a7a-1c02-4f04-87ac-22c3a385e6e3\",\"account\":{\"Jiminny\\\\Models\\\\SocialAccount\":{\"id\":306,\"sociable_id\":109,\"provider_user_id\":\"11348452\",\"expires\":1701077403,\"refresh_token_expires\":null,\"provider\":\"hubspot\",\"state\":\"full-refresh\",\"auth_scope\":null,\"retry_after\":null,\"created_at\":\"2020-09-01 16:59:04\",\"updated_at\":\"2023-11-27 09:30:03\"}}} {\"correlation_id\":\"0732b08b-2bf7-4f5b-93b6-9b2889269535\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":109,\"team_id\":29} {\"correlation_id\":\"0732b08b-2bf7-4f5b-93b6-9b2889269535\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"0732b08b-2bf7-4f5b-93b6-9b2889269535\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"0732b08b-2bf7-4f5b-93b6-9b2889269535\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":14.98,\"usage\":23047624,\"real_usage\":65011712,\"pid\":21703,\"reason\":\"Your HubSpot account has become disconnected. Please login to Jiminny to reconnect.\"} {\"correlation_id\":\"0732b08b-2bf7-4f5b-93b6-9b2889269535\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:29] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:29] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:29] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:29] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:29] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:29] local.INFO: [HubSpot Journal Polling] Service ending {\"runtime_seconds\":56,\"total_cycles\":5,\"files_downloaded\":0,\"empty_files\":0,\"other_portal_skipped\":0,\"total_events\":0,\"events_per_file\":0,\"avg_api_ms\":192.1,\"avg_download_ms\":0.0,\"avg_transform_ms\":0.0,\"avg_process_ms\":0.0,\"peak_memory_mb\":99.88} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:29] local.INFO: [HubSpot Journal Polling] Released polling lock {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:29] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"usage\":23005136,\"real_usage\":65011712,\"pid\":21703} {\"correlation_id\":\"4f83a1ac-418f-4f49-8377-328b614306fc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:29] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"71e3aac5-fb66-47c5-a236-2d051ae3e319\",\"account\":null} {\"correlation_id\":\"4f83a1ac-418f-4f49-8377-328b614306fc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:29] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":256,\"team_id\":49} {\"correlation_id\":\"4f83a1ac-418f-4f49-8377-328b614306fc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:29] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"4f83a1ac-418f-4f49-8377-328b614306fc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:29] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"4f83a1ac-418f-4f49-8377-328b614306fc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:29] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":34.51,\"usage\":23021248,\"real_usage\":65011712,\"pid\":21703,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"4f83a1ac-418f-4f49-8377-328b614306fc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:32:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"35f98b1b-7b16-4e79-a420-7e9818097d24\",\"trace_id\":\"d66deb58-fa13-486f-a866-2cac402dc589\"}\n[2026-05-28 08:32:06] local.INFO: [ScheduleBotCommand] Dispatched activities to capture {\"count\":0} {\"correlation_id\":\"35f98b1b-7b16-4e79-a420-7e9818097d24\",\"trace_id\":\"d66deb58-fa13-486f-a866-2cac402dc589\"}\n[2026-05-28 08:32:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"35f98b1b-7b16-4e79-a420-7e9818097d24\",\"trace_id\":\"d66deb58-fa13-486f-a866-2cac402dc589\"}\n[2026-05-28 08:32:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"bc6784c1-c46f-4295-be09-87334f4baa39\",\"trace_id\":\"74b00236-4ed0-4464-8b07-99c328ab6461\"}\n[2026-05-28 08:32:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"bc6784c1-c46f-4295-be09-87334f4baa39\",\"trace_id\":\"74b00236-4ed0-4464-8b07-99c328ab6461\"}\n[2026-05-28 08:32:11] local.NOTICE: Monitoring start {\"correlation_id\":\"caf4d1dd-2a52-40f5-834a-f9148f642e83\",\"trace_id\":\"1312bdcc-7e8b-480a-a935-2433fb79dc70\"}\n[2026-05-28 08:32:11] local.NOTICE: Monitoring end {\"correlation_id\":\"caf4d1dd-2a52-40f5-834a-f9148f642e83\",\"trace_id\":\"1312bdcc-7e8b-480a-a935-2433fb79dc70\"}\n[2026-05-28 08:32:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"64d86889-1f0d-4f7d-a1ac-6f56e58e775e\",\"trace_id\":\"b184d66b-49f8-40ec-afbe-2e76e9c7d952\"}\n[2026-05-28 08:32:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"64d86889-1f0d-4f7d-a1ac-6f56e58e775e\",\"trace_id\":\"b184d66b-49f8-40ec-afbe-2e76e9c7d952\"}\n[2026-05-28 08:32:20] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"8e6dd4bc-4f44-4fb2-b2ba-9e2f385996ef\",\"trace_id\":\"7767e0ca-37a3-43a5-972e-ea9a1bedea2f\"}\n[2026-05-28 08:32:20] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"8e6dd4bc-4f44-4fb2-b2ba-9e2f385996ef\",\"trace_id\":\"7767e0ca-37a3-43a5-972e-ea9a1bedea2f\"}\n[2026-05-28 08:32:20] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"8e6dd4bc-4f44-4fb2-b2ba-9e2f385996ef\",\"trace_id\":\"7767e0ca-37a3-43a5-972e-ea9a1bedea2f\"}\n[2026-05-28 08:32:20] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"8e6dd4bc-4f44-4fb2-b2ba-9e2f385996ef\",\"trace_id\":\"7767e0ca-37a3-43a5-972e-ea9a1bedea2f\"}\n[2026-05-28 08:32:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"6750ab25-6dff-4e7b-9f6e-bc7d1fc4221a\",\"trace_id\":\"3db6ce9b-167d-4214-a32c-67c611aa2bbe\"}\n[2026-05-28 08:32:22] local.INFO: Running conference:monitor:count command for activities in (2026-05-28 08:30:00, 2026-05-28 08:32:00] {\"correlation_id\":\"6750ab25-6dff-4e7b-9f6e-bc7d1fc4221a\",\"trace_id\":\"3db6ce9b-167d-4214-a32c-67c611aa2bbe\"}\n[2026-05-28 08:32:22] local.INFO: [conference:monitor:count] No activities found in (2026-05-28 08:30:00, 2026-05-28 08:32:00] {\"correlation_id\":\"6750ab25-6dff-4e7b-9f6e-bc7d1fc4221a\",\"trace_id\":\"3db6ce9b-167d-4214-a32c-67c611aa2bbe\"}\n[2026-05-28 08:32:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"6750ab25-6dff-4e7b-9f6e-bc7d1fc4221a\",\"trace_id\":\"3db6ce9b-167d-4214-a32c-67c611aa2bbe\"}\n[2026-05-28 08:32:23] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"04510c2c-638c-4687-81d7-b12e76f41df3\",\"trace_id\":\"87f0bb69-8fd1-47f1-9c9e-3b0ac3d6bd60\"}\n[2026-05-28 08:32:23] local.INFO: [EmailSchedule] STARTING batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"04510c2c-638c-4687-81d7-b12e76f41df3\",\"trace_id\":\"87f0bb69-8fd1-47f1-9c9e-3b0ac3d6bd60\"}\n[2026-05-28 08:32:23] local.INFO: [EmailSchedule] FINISHED batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"04510c2c-638c-4687-81d7-b12e76f41df3\",\"trace_id\":\"87f0bb69-8fd1-47f1-9c9e-3b0ac3d6bd60\"}\n[2026-05-28 08:32:23] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"04510c2c-638c-4687-81d7-b12e76f41df3\",\"trace_id\":\"87f0bb69-8fd1-47f1-9c9e-3b0ac3d6bd60\"}\n[2026-05-28 08:32:25] local.INFO: [Jiminny\\Jobs\\Mailbox\\CreateBatches] processed 1 inboxes and created 0 batches {\"userId\":null,\"batchSize\":30,\"maxBatches\":1000} {\"correlation_id\":\"e60597ef-aca8-4bda-bb90-18f01e96874d\",\"trace_id\":\"87f0bb69-8fd1-47f1-9c9e-3b0ac3d6bd60\"}","depth":4,"bounds":{"left":0.4331782,"top":0.09736632,"width":0.5668218,"height":0.90263367},"on_screen":true,"value":"[2026-05-28 08:28:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"73199e23-a066-42ba-b189-2802e0869a44\",\"trace_id\":\"e53672c2-0f50-409d-af25-84ab485c505d\"}\n[2026-05-28 08:28:04] local.INFO: [ScheduleBotCommand] Dispatched activities to capture {\"count\":0} {\"correlation_id\":\"73199e23-a066-42ba-b189-2802e0869a44\",\"trace_id\":\"e53672c2-0f50-409d-af25-84ab485c505d\"}\n[2026-05-28 08:28:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"73199e23-a066-42ba-b189-2802e0869a44\",\"trace_id\":\"e53672c2-0f50-409d-af25-84ab485c505d\"}\n[2026-05-28 08:28:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"741b2c4e-6a44-4c46-b9fb-fce017aef225\",\"trace_id\":\"804dc547-eb25-4b32-8d0f-c87019773085\"}\n[2026-05-28 08:28:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"741b2c4e-6a44-4c46-b9fb-fce017aef225\",\"trace_id\":\"804dc547-eb25-4b32-8d0f-c87019773085\"}\n[2026-05-28 08:28:09] local.NOTICE: Monitoring start {\"correlation_id\":\"8ae5c131-edd9-463d-8161-875f4d34320f\",\"trace_id\":\"1b6420c8-ef4e-4566-81b2-688696064648\"}\n[2026-05-28 08:28:09] local.NOTICE: Monitoring end {\"correlation_id\":\"8ae5c131-edd9-463d-8161-875f4d34320f\",\"trace_id\":\"1b6420c8-ef4e-4566-81b2-688696064648\"}\n[2026-05-28 08:28:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"6f5390d3-abe7-4e56-a662-aeed1098941b\",\"trace_id\":\"b57dc2df-2f4d-4d3b-b09a-aa6e61205d5e\"}\n[2026-05-28 08:28:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"6f5390d3-abe7-4e56-a662-aeed1098941b\",\"trace_id\":\"b57dc2df-2f4d-4d3b-b09a-aa6e61205d5e\"}\n[2026-05-28 08:28:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"ba694158-ea28-4ac1-b134-83ac485146e0\",\"trace_id\":\"83a26b74-baa5-407e-9fc2-2b49d65b1c77\"}\n[2026-05-28 08:28:12] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"ba694158-ea28-4ac1-b134-83ac485146e0\",\"trace_id\":\"83a26b74-baa5-407e-9fc2-2b49d65b1c77\"}\n[2026-05-28 08:28:12] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"ba694158-ea28-4ac1-b134-83ac485146e0\",\"trace_id\":\"83a26b74-baa5-407e-9fc2-2b49d65b1c77\"}\n[2026-05-28 08:28:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"ba694158-ea28-4ac1-b134-83ac485146e0\",\"trace_id\":\"83a26b74-baa5-407e-9fc2-2b49d65b1c77\"}\n[2026-05-28 08:28:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"b79833c7-3eab-4eb7-98bb-42359039b8fb\",\"trace_id\":\"a8e159bc-867d-40af-bf7d-1228cf6d4f16\"}\n[2026-05-28 08:28:14] local.INFO: Running conference:monitor:count command for activities in (2026-05-28 08:26:00, 2026-05-28 08:28:00] {\"correlation_id\":\"b79833c7-3eab-4eb7-98bb-42359039b8fb\",\"trace_id\":\"a8e159bc-867d-40af-bf7d-1228cf6d4f16\"}\n[2026-05-28 08:28:14] local.INFO: [conference:monitor:count] No activities found in (2026-05-28 08:26:00, 2026-05-28 08:28:00] {\"correlation_id\":\"b79833c7-3eab-4eb7-98bb-42359039b8fb\",\"trace_id\":\"a8e159bc-867d-40af-bf7d-1228cf6d4f16\"}\n[2026-05-28 08:28:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"b79833c7-3eab-4eb7-98bb-42359039b8fb\",\"trace_id\":\"a8e159bc-867d-40af-bf7d-1228cf6d4f16\"}\n[2026-05-28 08:28:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"d51071d5-ead0-45f0-a07e-e3234074f884\",\"trace_id\":\"77173d9f-596f-405f-ae55-ba117b66455d\"}\n[2026-05-28 08:28:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:16] local.NOTICE: Calendar sync start {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"d51071d5-ead0-45f0-a07e-e3234074f884\",\"trace_id\":\"77173d9f-596f-405f-ae55-ba117b66455d\"}\n[2026-05-28 08:28:16] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:16] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:16] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:16] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1393,\"provider\":\"google\",\"refreshToken\":\"5aa7e2d96b53201cd16fca5d2e4ef3ad03320971fc064781d18aee3ae7b99fbf\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1387,\"provider\":\"google\",\"refreshToken\":\"8157ac6de94842937194009e9c50e459253600f799dacf6a40755ffdbeb5bba6\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1348,\"provider\":\"google\",\"refreshToken\":\"9e7d13d3032d0cb1b79d8e95aef01383e8e91eb52ff8ee960c8a0b6b95cd8c73\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1361,\"provider\":\"google\",\"refreshToken\":\"6c843da199c2b9907445329304fcc4ec5057a4ee748d8299641764395c08e1fd\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1310,\"provider\":\"google\",\"refreshToken\":\"e34818922c2830a660813a63f6169a4a9a992ae2cccd7dc8dd7796cfdb470ef1\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1333,\"provider\":\"google\",\"refreshToken\":\"6c902986546d8e8da1dc539b046cdc1d458f519acc972e5b5f1d6a1a295165e0\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1368,\"provider\":\"google\",\"refreshToken\":\"d2f128898ff8543bd16b69cfae37896ab85119b0f5ed2b431d739593bb600333\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1365,\"provider\":\"google\",\"refreshToken\":\"7676e4a9afcd082b413248ab5ec6e487021fec6a9bdf315860a59cefad9caad8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1364,\"provider\":\"google\",\"refreshToken\":\"dd5882ebce76e645292ce33ae74238abbb77c0a4ecc6a2bfe723cad82e72ba8e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1370,\"provider\":\"office\",\"refreshToken\":\"b7ee8035306d0043cea6e00e7c4fe14f745e44074a1194db62a31cdf8b70af3e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1370,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 41d8db5c-a91f-41a6-9b5c-22a6c99b9100 Correlation ID: 70bc0a6f-1565-4e74-94dc-419d94108fa9 Timestamp: 2026-05-28 08:28:18Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-28 08:28:18Z\\\",\\\"trace_id\\\":\\\"41d8db5c-a91f-41a6-9b5c-22a6c99b9100\\\",\\\"correlation_id\\\":\\\"70bc0a6f-1565-4e74-94dc-419d94108fa9\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1370,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1202,\"provider\":\"office\",\"refreshToken\":\"b458799ccc29b21a6e2eb5260fdb63e49ccba21bf942a3973fb63799bd7f0afe\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1202,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: d3d14dfc-1b70-40c4-b9db-fab12f748300 Correlation ID: 4c5f746c-8366-44c1-8733-6877607826db Timestamp: 2026-05-28 08:28:19Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-28 08:28:19Z\\\",\\\"trace_id\\\":\\\"d3d14dfc-1b70-40c4-b9db-fab12f748300\\\",\\\"correlation_id\\\":\\\"4c5f746c-8366-44c1-8733-6877607826db\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1202,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: Calendar sync job dispatched {\"calendar_id\":501} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1300,\"provider\":\"google\",\"refreshToken\":\"4b811db0725fd9602a95943519a7da935e2a5065da7d9ebfcb170752e3e1ddb8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1409,\"provider\":\"google\",\"refreshToken\":\"e2a3f2d06894894eed1ee87d9db1ace77d4d42ee6e1288a8940ad2c10333b0c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1352,\"provider\":\"google\",\"refreshToken\":\"dd4b16b00fdc1216da6b717c02338c073636e29162826b2de6db3f064fc029eb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1296,\"provider\":\"office\",\"refreshToken\":\"011ae723c9d800c674e0b4be76f49fc046dac7d501b66c59ef0d9549cfa56ae5\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [Calendar] Processing sync {\"calendarId\":\"a33076c1-8d97-431a-99f0-85c9524e118b\",\"from\":null,\"to\":null,\"delta\":\"CIiFh8TP44kDEIiFh8TP44kDGAUgkZvkzgIokZvkzgI=\",\"last_sync\":\"2024-12-09 07:12:53\",\"dateMode\":\"daily\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"integration-app\",\"crm_owner\":1695,\"team_id\":3143} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1296,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: dd83a147-95bc-407c-8c9b-3c14e7cb7d00 Correlation ID: bb16bae3-b3c4-467e-bd95-4e7ab85364c9 Timestamp: 2026-05-28 08:28:20Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-28 08:28:20Z\\\",\\\"trace_id\\\":\\\"dd83a147-95bc-407c-8c9b-3c14e7cb7d00\\\",\\\"correlation_id\\\":\\\"bb16bae3-b3c4-467e-bd95-4e7ab85364c9\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1296,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":391,\"provider\":\"office\",\"refreshToken\":\"00045eebae0f39b34887c6d53f92ae78064f7145e1f4b67754aebd03cfb2d881\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.INFO: [Google Calendar] Failed to watch channel for calendar {\"calendarId\":\"a33076c1-8d97-431a-99f0-85c9524e118b\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:20] local.WARNING: [Calendar] Sync failed {\"calendarId\":\"a33076c1-8d97-431a-99f0-85c9524e118b\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"45e201e4-60d2-407c-ae71-13e4739c0875\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":391,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 587224ad-f597-4371-b2ba-00fcd2da1b00 Correlation ID: 4e752610-95b1-4606-847f-68e00f90be0a Timestamp: 2026-05-28 08:28:21Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-28 08:28:21Z\\\",\\\"trace_id\\\":\\\"587224ad-f597-4371-b2ba-00fcd2da1b00\\\",\\\"correlation_id\\\":\\\"4e752610-95b1-4606-847f-68e00f90be0a\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":391,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1271,\"provider\":\"office\",\"refreshToken\":\"118cde2c06993147b07ccaec4cbcd5026a819dea6c71081166a492933e392afb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1271,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: dd8bf2a7-bc49-43d7-a487-892c51e27600 Correlation ID: 51e3eb6c-c47e-4dd7-91aa-e734fe5b65cf Timestamp: 2026-05-28 08:28:21Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-28 08:28:21Z\\\",\\\"trace_id\\\":\\\"dd8bf2a7-bc49-43d7-a487-892c51e27600\\\",\\\"correlation_id\\\":\\\"51e3eb6c-c47e-4dd7-91aa-e734fe5b65cf\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1271,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1351,\"provider\":\"google\",\"refreshToken\":\"4271d15b9e60a606439caddc68337f783e472c85b03dacff14d1b6dfded9051c\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1366,\"provider\":\"google\",\"refreshToken\":\"ae21385059b2eebfd43f68aecd56eccd702a1aabb6598f1f7ab594ed8af491b4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: Calendar sync job dispatched {\"calendar_id\":378} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: Calendar sync job dispatched {\"calendar_id\":504} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.NOTICE: Calendar sync end {\"retrieved_calendars\":31,\"processed_calendars\":3} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"498bfcf1-b82c-466d-8f91-2f3f1cdf980f\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [Calendar] Processing sync {\"calendarId\":\"2676cb6d-f86c-427e-bf78-591e388e3c1e\",\"from\":null,\"to\":null,\"delta\":\"CJ_x49O3jpIDEJ_x49O3jpIDGAUgw67KlwMow67KlwM=\",\"last_sync\":\"2026-01-19 07:48:40\",\"dateMode\":\"daily\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.WARNING: [Pipedrive] Account not connected for user {\"userId\":\"e6538737-e7b4-455f-a37a-3e79b665a220\",\"account\":{\"Jiminny\\\\Models\\\\SocialAccount\":{\"id\":1116,\"sociable_id\":241,\"provider_user_id\":\"19555731\",\"expires\":1779091997,\"refresh_token_expires\":null,\"provider\":\"pipedrive\",\"state\":\"full-refresh\",\"auth_scope\":\"base,deals:full,activities:full,contacts:full,search:read\",\"retry_after\":null,\"created_at\":\"2023-09-08 09:44:29\",\"updated_at\":\"2026-05-18 08:13:32\"}}} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"pipedrive\",\"crm_owner\":241,\"team_id\":19} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.WARNING: [Calendar] CRM disconnected for user so events will not be matched {\"provider\":\"pipedrive\",\"user_id\":241,\"message\":\"Your Pipedrive account has become disconnected. Please login to Jiminny to reconnect.\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:22] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [Google Calendar] Failed to watch channel for calendar {\"calendarId\":\"2676cb6d-f86c-427e-bf78-591e388e3c1e\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.WARNING: [Calendar] Sync failed {\"calendarId\":\"2676cb6d-f86c-427e-bf78-591e388e3c1e\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"9436b0cd-f0cc-4ee2-805e-472888fd5928\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [Calendar] Processing sync {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\",\"from\":null,\"to\":null,\"delta\":\"R0usmcdvmMuZCBYV0hguCBhf4uKqyU9_gdNd-le_nqqau95XAUmZwlIAy2uwtCf5sA67w6NIiDvawUSmVjpTzZO-RsxLdeS-g8Kxy6ODZQxAxafq8AJNx0hJV7T291NUvfaPYqLMv-3je3IdhAPUtXQXkHtYaBAlX_DDBxwc3zYoVVQ8iM9mQkBykdA3sh-KVYPyzi7nK4cZg5Jzpu9tHA.UA7urx3pJyhUhdo_16STIbSNXUrF-qeQVIis9stVUeY\",\"last_sync\":\"2026-05-28 07:44:06\",\"dateMode\":\"daily\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:28:23] local.INFO: [MS Office Calendar] Skipping delta sync for daily mode {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\"} {\"correlation_id\":\"82ba6688-7d5c-4789-9154-9918f63e2bfd\",\"trace_id\":\"19e20da3-a204-446a-a01e-568f25ddd9fd\"}\n[2026-05-28 08:29:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"86a65904-a9f8-43c9-b0c3-1e9c8b4b9541\",\"trace_id\":\"800b02ff-3553-46a3-a8c6-c12324a1ce53\"}\n[2026-05-28 08:29:05] local.INFO: [ScheduleBotCommand] Dispatched activities to capture {\"count\":0} {\"correlation_id\":\"86a65904-a9f8-43c9-b0c3-1e9c8b4b9541\",\"trace_id\":\"800b02ff-3553-46a3-a8c6-c12324a1ce53\"}\n[2026-05-28 08:29:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"86a65904-a9f8-43c9-b0c3-1e9c8b4b9541\",\"trace_id\":\"800b02ff-3553-46a3-a8c6-c12324a1ce53\"}\n[2026-05-28 08:29:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"e5162656-5b49-4857-9917-f0cf3ddffe27\",\"trace_id\":\"a3a53cc5-596d-474c-b9c4-f6c5f7ebdfd0\"}\n[2026-05-28 08:29:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"e5162656-5b49-4857-9917-f0cf3ddffe27\",\"trace_id\":\"a3a53cc5-596d-474c-b9c4-f6c5f7ebdfd0\"}\n[2026-05-28 08:29:09] local.NOTICE: Monitoring start {\"correlation_id\":\"f559463e-0c12-46df-8b53-0c50cf4aa3eb\",\"trace_id\":\"5a09aee9-5f75-4bfa-93e4-df0ff76f2326\"}\n[2026-05-28 08:29:09] local.NOTICE: Monitoring end {\"correlation_id\":\"f559463e-0c12-46df-8b53-0c50cf4aa3eb\",\"trace_id\":\"5a09aee9-5f75-4bfa-93e4-df0ff76f2326\"}\n[2026-05-28 08:29:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"9b1cc2f1-0c8a-4aa0-a9e8-57ac879839e4\",\"trace_id\":\"d69dd133-b5f0-4568-81ef-9cce3d1c5c9e\"}\n[2026-05-28 08:29:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"9b1cc2f1-0c8a-4aa0-a9e8-57ac879839e4\",\"trace_id\":\"d69dd133-b5f0-4568-81ef-9cce3d1c5c9e\"}\n[2026-05-28 08:29:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"87e10603-9e6f-4d71-97ab-5ba67103e5fd\",\"trace_id\":\"0aae0afb-779f-4999-892d-0188e14b3dcb\"}\n[2026-05-28 08:29:12] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"87e10603-9e6f-4d71-97ab-5ba67103e5fd\",\"trace_id\":\"0aae0afb-779f-4999-892d-0188e14b3dcb\"}\n[2026-05-28 08:29:12] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"87e10603-9e6f-4d71-97ab-5ba67103e5fd\",\"trace_id\":\"0aae0afb-779f-4999-892d-0188e14b3dcb\"}\n[2026-05-28 08:29:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"87e10603-9e6f-4d71-97ab-5ba67103e5fd\",\"trace_id\":\"0aae0afb-779f-4999-892d-0188e14b3dcb\"}\n[2026-05-28 08:30:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"200b132b-d8db-4010-b968-fe5e3fcc42f1\",\"trace_id\":\"7457715a-3b9d-4c35-a499-455c83e925ab\"}\n[2026-05-28 08:30:04] local.INFO: [ScheduleBotCommand] Dispatched activities to capture {\"count\":0} {\"correlation_id\":\"200b132b-d8db-4010-b968-fe5e3fcc42f1\",\"trace_id\":\"7457715a-3b9d-4c35-a499-455c83e925ab\"}\n[2026-05-28 08:30:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"200b132b-d8db-4010-b968-fe5e3fcc42f1\",\"trace_id\":\"7457715a-3b9d-4c35-a499-455c83e925ab\"}\n[2026-05-28 08:30:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"a94ef973-dee7-4bdf-99aa-9ff06f5a5561\",\"trace_id\":\"4e37013f-2993-4c71-87d9-560f8f429d27\"}\n[2026-05-28 08:30:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"a94ef973-dee7-4bdf-99aa-9ff06f5a5561\",\"trace_id\":\"4e37013f-2993-4c71-87d9-560f8f429d27\"}\n[2026-05-28 08:30:07] local.NOTICE: Monitoring start {\"correlation_id\":\"691e7103-784d-4b4a-8036-f3c624a186db\",\"trace_id\":\"991dd947-ed80-4080-8769-8ee7efc4e6a4\"}\n[2026-05-28 08:30:07] local.NOTICE: Monitoring end {\"correlation_id\":\"691e7103-784d-4b4a-8036-f3c624a186db\",\"trace_id\":\"991dd947-ed80-4080-8769-8ee7efc4e6a4\"}\n[2026-05-28 08:30:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"00cf8914-d440-4826-a079-ee4586e76c62\",\"trace_id\":\"d77009ee-8d7b-44ae-bb21-2080b6d48c6c\"}\n[2026-05-28 08:30:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"00cf8914-d440-4826-a079-ee4586e76c62\",\"trace_id\":\"d77009ee-8d7b-44ae-bb21-2080b6d48c6c\"}\n[2026-05-28 08:30:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"7c9550d0-42ed-4b1d-b5b3-7dca67f743ec\",\"trace_id\":\"84177853-9530-4b1e-bde6-3dc0c71883ee\"}\n[2026-05-28 08:30:10] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"7c9550d0-42ed-4b1d-b5b3-7dca67f743ec\",\"trace_id\":\"84177853-9530-4b1e-bde6-3dc0c71883ee\"}\n[2026-05-28 08:30:10] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"7c9550d0-42ed-4b1d-b5b3-7dca67f743ec\",\"trace_id\":\"84177853-9530-4b1e-bde6-3dc0c71883ee\"}\n[2026-05-28 08:30:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"7c9550d0-42ed-4b1d-b5b3-7dca67f743ec\",\"trace_id\":\"84177853-9530-4b1e-bde6-3dc0c71883ee\"}\n[2026-05-28 08:30:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"85cc6c92-262b-45c5-bbcc-4a084137fde3\",\"trace_id\":\"058bc9a3-67af-49b6-a87c-8a0a0bc0c47a\"}\n[2026-05-28 08:30:12] local.INFO: Running conference:monitor:count command for activities in (2026-05-28 08:28:00, 2026-05-28 08:30:00] {\"correlation_id\":\"85cc6c92-262b-45c5-bbcc-4a084137fde3\",\"trace_id\":\"058bc9a3-67af-49b6-a87c-8a0a0bc0c47a\"}\n[2026-05-28 08:30:12] local.INFO: [conference:monitor:count] No activities found in (2026-05-28 08:28:00, 2026-05-28 08:30:00] {\"correlation_id\":\"85cc6c92-262b-45c5-bbcc-4a084137fde3\",\"trace_id\":\"058bc9a3-67af-49b6-a87c-8a0a0bc0c47a\"}\n[2026-05-28 08:30:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"85cc6c92-262b-45c5-bbcc-4a084137fde3\",\"trace_id\":\"058bc9a3-67af-49b6-a87c-8a0a0bc0c47a\"}\n[2026-05-28 08:30:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"5083d024-d669-4c6a-bde3-ce084d6a628c\",\"trace_id\":\"83110e1f-4309-4f4a-b8bb-eab5c0ec081e\"}\n[2026-05-28 08:30:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"5083d024-d669-4c6a-bde3-ce084d6a628c\",\"trace_id\":\"83110e1f-4309-4f4a-b8bb-eab5c0ec081e\"}\n[2026-05-28 08:30:15] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"bc529b16-9502-4df3-b2c0-c63fcf977ed5\",\"trace_id\":\"9dd5d6d0-4cc5-454c-a0c8-0e72b69eeb29\"}\n[2026-05-28 08:30:15] local.INFO: [TextRelayService] Starting sync {\"mailbox\":\"catch-all@txt.staging.jiminny.com\",\"expected_alias\":\"catch-all\",\"expected_host\":\"txt.staging.jiminny.com\"} {\"correlation_id\":\"bc529b16-9502-4df3-b2c0-c63fcf977ed5\",\"trace_id\":\"9dd5d6d0-4cc5-454c-a0c8-0e72b69eeb29\"}\n[2026-05-28 08:30:15] local.INFO: [TextRelayService] Sync completed {\"mailbox\":\"catch-all@txt.staging.jiminny.com\",\"messages_processed\":0,\"message_ids\":[]} {\"correlation_id\":\"bc529b16-9502-4df3-b2c0-c63fcf977ed5\",\"trace_id\":\"9dd5d6d0-4cc5-454c-a0c8-0e72b69eeb29\"}\n[2026-05-28 08:30:15] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"bc529b16-9502-4df3-b2c0-c63fcf977ed5\",\"trace_id\":\"9dd5d6d0-4cc5-454c-a0c8-0e72b69eeb29\"}\n[2026-05-28 08:30:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:pre-meeting-notification\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"b655bca8-effa-420b-8686-95ba96f27085\",\"trace_id\":\"b1f43a19-c7ad-428e-9933-4e509b5f0055\"}\n[2026-05-28 08:30:18] local.INFO: Running pre-meeting notification command {\"correlation_id\":\"b655bca8-effa-420b-8686-95ba96f27085\",\"trace_id\":\"b1f43a19-c7ad-428e-9933-4e509b5f0055\"}\n[2026-05-28 08:30:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:pre-meeting-notification\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"b655bca8-effa-420b-8686-95ba96f27085\",\"trace_id\":\"b1f43a19-c7ad-428e-9933-4e509b5f0055\"}\n[2026-05-28 08:30:20] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:start\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"5a8493c4-3ef7-4453-98b7-f32225d453fe\",\"trace_id\":\"056f1aea-8825-4e2c-8239-7804086e504e\"}\n[2026-05-28 08:30:20] local.INFO: Running conference:monitor:start command for activities in (2026-05-28 08:20:00, 2026-05-28 08:25:00] {\"correlation_id\":\"5a8493c4-3ef7-4453-98b7-f32225d453fe\",\"trace_id\":\"056f1aea-8825-4e2c-8239-7804086e504e\"}\n[2026-05-28 08:30:20] local.INFO: [conference:monitor:start] No activities found in (2026-05-28 08:20:00, 2026-05-28 08:25:00] {\"correlation_id\":\"5a8493c4-3ef7-4453-98b7-f32225d453fe\",\"trace_id\":\"056f1aea-8825-4e2c-8239-7804086e504e\"}\n[2026-05-28 08:30:20] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:start\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"5a8493c4-3ef7-4453-98b7-f32225d453fe\",\"trace_id\":\"056f1aea-8825-4e2c-8239-7804086e504e\"}\n[2026-05-28 08:30:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:end\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"18a2aed9-1aae-4b10-a7fa-f75a147db499\",\"trace_id\":\"fff0c9b2-1c60-4402-b30a-2f41d9b45db3\"}\n[2026-05-28 08:30:22] local.INFO: conference:monitor:end:Jiminny\\Console\\Commands\\Activities\\MonitorMeetingEndCommand::logActivitiesEnded {\"from\":\"08:25\",\"to\":\"08:30\"} {\"correlation_id\":\"18a2aed9-1aae-4b10-a7fa-f75a147db499\",\"trace_id\":\"fff0c9b2-1c60-4402-b30a-2f41d9b45db3\"}\n[2026-05-28 08:30:22] local.INFO: conference:monitor:end:Jiminny\\Console\\Commands\\Activities\\MonitorMeetingEndCommand::logActivitiesWithUnfinishedSession {\"from\":\"22:20\",\"to\":\"22:25\"} {\"correlation_id\":\"18a2aed9-1aae-4b10-a7fa-f75a147db499\",\"trace_id\":\"fff0c9b2-1c60-4402-b30a-2f41d9b45db3\"}\n[2026-05-28 08:30:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:end\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"18a2aed9-1aae-4b10-a7fa-f75a147db499\",\"trace_id\":\"fff0c9b2-1c60-4402-b30a-2f41d9b45db3\"}\n[2026-05-28 08:30:23] local.NOTICE: Repairing HubSpot tokens start {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:23] local.INFO: Trying to refresh HubSpot token {\"account_id\":59,\"updated_at\":\"2025-10-03 09:32:05\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:23] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:23] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":59,\"provider\":\"hubspot\",\"refreshToken\":\"97b78f6e2cc49965c00c2492b602b02708b1392551e6b3f113fbaa48992af90b\",\"state\":\"full-refresh\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:26] local.ERROR: Failed to refresh HubSpot token {\"account_id\":59,\"updated_at\":\"2025-10-03 09:32:05\",\"reason\":\"missing or invalid refresh token\",\"previous\":\"\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:26] local.INFO: Trying to refresh HubSpot token {\"account_id\":306,\"updated_at\":\"2023-11-27 09:30:03\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:26] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":306,\"provider\":\"hubspot\",\"refreshToken\":\"6fa6aa8cc641d131231acc3470f5c03cb3b07b2e580fb18f8acb3b1dbb72549b\",\"state\":\"full-refresh\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:27] local.ERROR: Failed to refresh HubSpot token {\"account_id\":306,\"updated_at\":\"2023-11-27 09:30:03\",\"reason\":\"missing or invalid refresh token\",\"previous\":\"\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:27] local.INFO: Trying to refresh HubSpot token {\"account_id\":1372,\"updated_at\":\"2025-10-02 14:47:06\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:27] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:27] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1372,\"provider\":\"hubspot\",\"refreshToken\":\"9aa73948c761da29dce46c177cf9aee1fde483a44169ca38723f9f0597d7a8c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:27] local.ERROR: Failed to refresh HubSpot token {\"account_id\":1372,\"updated_at\":\"2025-10-02 14:47:06\",\"reason\":\"missing or invalid refresh token\",\"previous\":\"\"} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:27] local.NOTICE: Repairing HubSpot tokens end {\"total\":3,\"fixed\":0,\"failed\":3} {\"correlation_id\":\"338516a2-06f3-41cf-8f11-0d2c8a0b2a29\",\"trace_id\":\"58fc6994-1c77-429c-8c16-14c4bd8f57c4\"}\n[2026-05-28 08:30:33] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"jiminny:transcription:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"f73071c7-8696-459f-8aff-e61a7c6b30e6\",\"trace_id\":\"d72fc2ce-5064-4877-b401-10891378e1bc\"}\n[2026-05-28 08:30:33] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:pre-meeting-reminder\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"720d5c87-3d13-4193-87c9-efb8dc6df7c8\",\"trace_id\":\"a9d9c9fe-db29-44a1-a5f6-1905d48dde90\"}\n[2026-05-28 08:30:33] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"jiminny:transcription:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"f73071c7-8696-459f-8aff-e61a7c6b30e6\",\"trace_id\":\"d72fc2ce-5064-4877-b401-10891378e1bc\"}\n[2026-05-28 08:30:33] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:33] local.INFO: [HubSpot Journal Command] Starting polling service {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:33] local.INFO: [HubSpot Journal Polling] Service starting {\"memory_limit\":\"256M\",\"max_execution_time\":\"0\",\"initial_memory_mb\":62.0} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:33] local.INFO: [HubSpot Journal Polling] Acquired polling lock {\"expires_at\":\"2026-05-28T08:32:33.444809Z\"} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:33] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:33] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:33] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:pre-meeting-reminder\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"720d5c87-3d13-4193-87c9-efb8dc6df7c8\",\"trace_id\":\"a9d9c9fe-db29-44a1-a5f6-1905d48dde90\"}\n[2026-05-28 08:30:33] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:35] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:reset-governor\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"c692a62b-3b1a-4ff3-b965-388a7d12f619\",\"trace_id\":\"d1bdfdec-73bd-499f-a31e-b25a864da976\"}\n[2026-05-28 08:30:35] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:reset-governor\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"c692a62b-3b1a-4ff3-b965-388a7d12f619\",\"trace_id\":\"d1bdfdec-73bd-499f-a31e-b25a864da976\"}\n[2026-05-28 08:30:38] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:38] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:38] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:39] local.INFO: Dispatching activity sync job {\"import_id\":814238,\"provider\":\"twilio-flex\",\"team\":\"jiminny\"} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:39] local.INFO: Dispatching activity sync job {\"import_id\":814239,\"provider\":\"xant\",\"team\":\"jiminny\"} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:39] local.INFO: Dispatching activity sync job {\"import_id\":814240,\"provider\":\"apollo\",\"team\":\"jiminny\"} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:39] local.INFO: Dispatching activity sync job {\"import_id\":814241,\"provider\":\"groove\",\"team\":\"jiminny\"} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:39] local.INFO: Dispatching activity sync job {\"import_id\":814242,\"provider\":\"twilio-video\",\"team\":\"jiminny\"} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:39] local.INFO: Dispatching activity sync job {\"import_id\":814243,\"provider\":\"hubspot\",\"team\":\"hubspot\"} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"c6f2bef5-da3d-4403-84b7-84c355175282\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:fail-stalled\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"f6d797df-5275-4cf1-8ddd-300c125df8da\",\"trace_id\":\"2b46c31f-aced-4324-a70a-6395f0a72618\"}\n[2026-05-28 08:30:42] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:fail-stalled\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"f6d797df-5275-4cf1-8ddd-300c125df8da\",\"trace_id\":\"2b46c31f-aced-4324-a70a-6395f0a72618\"}\n[2026-05-28 08:30:42] local.WARNING: [Salesforce] Account not connected for user {\"userId\":\"cdf8b554-d951-4758-bc2b-c1b85d1cd0b9\",\"account\":null} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"salesforce\",\"crm_owner\":3,\"team_id\":1} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [CrmOwnerResolver] TeamMember found with active crm connection {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1194,\"provider\":\"twilio-flex\"} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1194,\"provider\":\"twilio-flex\"} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.INFO: [SyncActivity] Start {\"import_id\":814238,\"provider\":\"twilio-flex\",\"provider_id\":317,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:42] local.NOTICE: [TwilioFlex] Calls import start {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.ALERT: [SyncActivity] Failed {\"import_id\":814238,\"provider\":\"twilio-flex\",\"provider_id\":317,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"[HTTP 401] Unable to fetch page: Authenticate\",\"file\":\"/home/jiminny/vendor/twilio/sdk/src/Twilio/Page.php\",\"line\":60} {\"correlation_id\":\"d90e1584-bf02-4654-b950-685da6f49697\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [SyncActivity] Start {\"import_id\":814239,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [Salesforce] Performing query {\"query\":\"\n SELECT Playbooks_Call_Date__c,Playbooks_Call_Recording__c,CreatedDate,TaskSubtype,CallType,CallDurationInSeconds,Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type\n FROM Task\n WHERE IsDeleted = false\n AND LastModifiedDate >= :from\n AND LastModifiedDate <= :to\n ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-05-28T08:14:00Z\",\"to\":\"2026-05-28T08:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:43] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+Playbooks_Call_Date__c%2CPlaybooks_Call_Recording__c%2CCreatedDate%2CTaskSubtype%2CCallType%2CCallDurationInSeconds%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-05-28T08%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-05-28T08%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:44] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:44] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:44] local.INFO: [Xant (InsideSales)] No calls found. {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SyncActivity] End {\"import_id\":814239,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SyncActivity] Memory usage {\"import_id\":814239,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1,\"memory_usage\":24080216,\"memory_real_usage\":65011712,\"pid\":21708} {\"correlation_id\":\"664afeb1-65f0-4713-918d-c7644e36eda4\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SyncActivity] Start {\"import_id\":814240,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [Salesforce] Performing query {\"query\":\"\n SELECT AccountId,CreatedDate,TaskSubtype,CallType,Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type\n FROM Task\n WHERE IsDeleted = false\n AND LastModifiedDate >= :from\n AND LastModifiedDate <= :to\n ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-05-28T08:14:00Z\",\"to\":\"2026-05-28T08:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+AccountId%2CCreatedDate%2CTaskSubtype%2CCallType%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-05-28T08%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-05-28T08%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [Apollo] No calls found. {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SyncActivity] End {\"import_id\":814240,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SyncActivity] Memory usage {\"import_id\":814240,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1,\"memory_usage\":24242184,\"memory_real_usage\":65011712,\"pid\":21708} {\"correlation_id\":\"82a57baa-c1eb-42dc-b082-fd7d3913c037\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SyncActivity] Start {\"import_id\":814241,\"provider\":\"groove\",\"provider_id\":228,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [Salesforce] Performing query {\"query\":\"\n SELECT call_recording_url__c,TaskSubtype,CreatedDate,CallType,CallDurationInSeconds,Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type\n FROM Task\n WHERE IsDeleted = false\n AND LastModifiedDate >= :from\n AND LastModifiedDate <= :to\n ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-05-28T08:14:00Z\",\"to\":\"2026-05-28T08:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+call_recording_url__c%2CTaskSubtype%2CCreatedDate%2CCallType%2CCallDurationInSeconds%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-05-28T08%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-05-28T08%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:bullhorn:ping\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"b2b37795-cfa8-472a-b681-322ce27c0e08\",\"trace_id\":\"605e54f2-d4f9-4683-b3fd-6c1c23e691b7\"}\n[2026-05-28 08:30:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:bullhorn:ping\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"b2b37795-cfa8-472a-b681-322ce27c0e08\",\"trace_id\":\"605e54f2-d4f9-4683-b3fd-6c1c23e691b7\"}\n[2026-05-28 08:30:44] local.ERROR: [Salesforce] Request exception [400] \nSELECT call_recording_url__c,TaskSubtype\n ^\nERROR at Row:1:Column:8\nNo such column 'call_recording_url__c' on entity 'Task'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names. {\"url\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+call_recording_url__c%2CTaskSubtype%2CCreatedDate%2CCallType%2CCallDurationInSeconds%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-05-28T08%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-05-28T08%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000\",\"data\":{\"headers\":{\"Authorization\":\"Bearer 00D2g0000008hH4!AQEAQHWUK6nshi8dSCX9CJphxkVivcv_TK98TLOckVy4i0Z1gQfAoRRuH2LVwDXDkbTPs80wvQjczjAlOz0UEtbHoORBLi2u\"}},\"response\":{\"GuzzleHttp\\\\Psr7\\\\Stream\":\"[{\\\"message\\\":\\\"\\\\nSELECT call_recording_url__c,TaskSubtype\\\\n ^\\\\nERROR at Row:1:Column:8\\\\nNo such column 'call_recording_url__c' on entity 'Task'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names.\\\",\\\"errorCode\\\":\\\"INVALID_FIELD\\\"}]\"},\"fields\":[]} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.ALERT: [SyncActivity] Failed {\"import_id\":814241,\"provider\":\"groove\",\"provider_id\":228,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"\nSELECT call_recording_url__c,TaskSubtype\n ^\nERROR at Row:1:Column:8\nNo such column 'call_recording_url__c' on entity 'Task'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names.\",\"file\":\"/home/jiminny/app/Services/Crm/Salesforce/Client.php\",\"line\":564} {\"correlation_id\":\"db0b2bb2-4954-4403-80fd-0f02be18b0fb\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [SyncActivity] Start {\"import_id\":814242,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [Salesforce] Performing query {\"query\":\"SELECT Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type,twilio_call_sid__c,Lead_UUID__c,Opportunity__c\n FROM Task\n WHERE Type = 'Video'\n AND isClosed = true\n AND IsDeleted = false\n AND LastModifiedDate >= :from\n AND twilio_call_sid__c != NULL AND LastModifiedDate <= :to ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-05-28T08:14:00Z\",\"to\":\"2026-05-28T08:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:44] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=SELECT+Id%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%2Ctwilio_call_sid__c%2CLead_UUID__c%2COpportunity__c%0A++++++++++++++FROM+Task%0A++++++++++++WHERE+Type+%3D+%27Video%27%0A++++++++++++++AND+isClosed+%3D+true%0A++++++++++++++AND+IsDeleted+%3D+false%0A++++++++++++++AND+LastModifiedDate+%3E%3D+2026-05-28T08%3A14%3A00Z%0A++++++++++++++AND+twilio_call_sid__c+%21%3D+NULL+AND+LastModifiedDate+%3C%3D+2026-05-28T08%3A30%3A00Z+ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [Twilio Video] No calls found. {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SyncActivity] End {\"import_id\":814242,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SyncActivity] Memory usage {\"import_id\":814242,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1,\"memory_usage\":24634032,\"memory_real_usage\":65011712,\"pid\":21708} {\"correlation_id\":\"0b355e21-99f2-41eb-8157-fe7cd227fa58\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":408,\"provider\":\"hubspot\"} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":408,\"provider\":\"hubspot\"} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SyncActivity] Start {\"import_id\":814243,\"provider\":\"hubspot\",\"provider_id\":31,\"team\":\"hubspot\",\"team_id\":2} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [HubSpot] Search calls for period {\"from\":\"2026-05-28 08:14:00\",\"to\":\"2026-05-28 08:30:00\"} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SyncActivity] End {\"import_id\":814243,\"provider\":\"hubspot\",\"provider_id\":31,\"team\":\"hubspot\",\"team_id\":2} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:45] local.INFO: [SyncActivity] Memory usage {\"import_id\":814243,\"provider\":\"hubspot\",\"provider_id\":31,\"team\":\"hubspot\",\"team_id\":2,\"memory_usage\":24947696,\"memory_real_usage\":65011712,\"pid\":21708} {\"correlation_id\":\"4d0058c7-c318-4aea-82c0-403eadc12ccc\",\"trace_id\":\"d36c89c2-6acf-4497-b03c-93e5a090ea28\"}\n[2026-05-28 08:30:46] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"nudges:send\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"f3e257b6-82bb-440c-bf76-855ac528e4e8\",\"trace_id\":\"f8a31218-7290-45ac-8fe1-ffb1b8527521\"}\n[2026-05-28 08:30:46] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"nudges:send\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"f3e257b6-82bb-440c-bf76-855ac528e4e8\",\"trace_id\":\"f8a31218-7290-45ac-8fe1-ffb1b8527521\"}\n[2026-05-28 08:30:50] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"jiminny:playlists:normalize-sort\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"a64b1836-79a1-41da-a4b0-bc0a9703a94b\",\"trace_id\":\"c53faedc-a7a7-41b7-8e51-e84dc2755d79\"}\n[2026-05-28 08:30:50] local.INFO: [Jiminny\\Component\\Playlist\\Command\\NormalizeSortCommand::handle] starting. {\"playlists\":[]} {\"correlation_id\":\"a64b1836-79a1-41da-a4b0-bc0a9703a94b\",\"trace_id\":\"c53faedc-a7a7-41b7-8e51-e84dc2755d79\"}\n[2026-05-28 08:30:50] local.INFO: [Jiminny\\Component\\Playlist\\Command\\NormalizeSortCommand::handle] finished. {\"normalizedPlaylists\":[],\"deletedPlaylists\":[]} {\"correlation_id\":\"a64b1836-79a1-41da-a4b0-bc0a9703a94b\",\"trace_id\":\"c53faedc-a7a7-41b7-8e51-e84dc2755d79\"}\n[2026-05-28 08:30:50] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"jiminny:playlists:normalize-sort\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"a64b1836-79a1-41da-a4b0-bc0a9703a94b\",\"trace_id\":\"c53faedc-a7a7-41b7-8e51-e84dc2755d79\"}\n[2026-05-28 08:30:59] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:59] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:30:59] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"41db30ba-33c5-46f1-b8bd-5d9e86a70d12\",\"trace_id\":\"a5e2028d-ca41-4ba1-9ddd-a246f83ef4cc\"}\n[2026-05-28 08:31:07] local.INFO: [ScheduleBotCommand] Dispatched activities to capture {\"count\":0} {\"correlation_id\":\"41db30ba-33c5-46f1-b8bd-5d9e86a70d12\",\"trace_id\":\"a5e2028d-ca41-4ba1-9ddd-a246f83ef4cc\"}\n[2026-05-28 08:31:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"41db30ba-33c5-46f1-b8bd-5d9e86a70d12\",\"trace_id\":\"a5e2028d-ca41-4ba1-9ddd-a246f83ef4cc\"}\n[2026-05-28 08:31:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"f6ed0a05-caa1-4cb6-ae83-63f44ae4e0d6\",\"trace_id\":\"16cd898d-57a1-4b74-90d1-f9223bbd8d7a\"}\n[2026-05-28 08:31:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"f6ed0a05-caa1-4cb6-ae83-63f44ae4e0d6\",\"trace_id\":\"16cd898d-57a1-4b74-90d1-f9223bbd8d7a\"}\n[2026-05-28 08:31:15] local.NOTICE: Monitoring start {\"correlation_id\":\"cdf384ea-4f59-4411-8ebf-efc299e4a1c8\",\"trace_id\":\"b069d3c8-2f77-4698-bd53-97de373652e8\"}\n[2026-05-28 08:31:15] local.NOTICE: Monitoring end {\"correlation_id\":\"cdf384ea-4f59-4411-8ebf-efc299e4a1c8\",\"trace_id\":\"b069d3c8-2f77-4698-bd53-97de373652e8\"}\n[2026-05-28 08:31:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"940a264b-4aee-40a0-a527-e101ea555a38\",\"trace_id\":\"cd9169ad-4c1e-452d-b7cb-66005aae4dc8\"}\n[2026-05-28 08:31:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"940a264b-4aee-40a0-a527-e101ea555a38\",\"trace_id\":\"cd9169ad-4c1e-452d-b7cb-66005aae4dc8\"}\n[2026-05-28 08:31:23] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"b798f381-71f2-4c51-a135-f91f09330cfd\",\"trace_id\":\"b44ed325-23f7-4f8b-a447-4c8d0ee99d01\"}\n[2026-05-28 08:31:23] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"b798f381-71f2-4c51-a135-f91f09330cfd\",\"trace_id\":\"b44ed325-23f7-4f8b-a447-4c8d0ee99d01\"}\n[2026-05-28 08:31:24] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"b798f381-71f2-4c51-a135-f91f09330cfd\",\"trace_id\":\"b44ed325-23f7-4f8b-a447-4c8d0ee99d01\"}\n[2026-05-28 08:31:24] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"b798f381-71f2-4c51-a135-f91f09330cfd\",\"trace_id\":\"b44ed325-23f7-4f8b-a447-4c8d0ee99d01\"}\n[2026-05-28 08:31:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:sync-hubspot-objects\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"2d631328-f2b9-44f3-83d3-613b7b9b9ddc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:sync-hubspot-objects\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"2d631328-f2b9-44f3-83d3-613b7b9b9ddc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"usage\":22563136,\"real_usage\":65011712,\"pid\":21703} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [HubSpot] Syncing opportunities using strategy: lastModified {\"team\":2} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.4,\"average_seconds_per_request\":0.4} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [HubSpot] Synced opportunities {\"team\":2,\"strategies\":\"lastModified\",\"sync_count\":0,\"total\":0,\"last_synced_id\":null,\"duration_ms\":432.6} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"provider\":\"hubspot\",\"status\":\"completed\",\"duration_ms\":543.02,\"usage\":22983640,\"real_usage\":65011712,\"pid\":21703} {\"correlation_id\":\"5b5669c7-bb21-4c59-97e6-edafd6520cc4\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"usage\":22961848,\"real_usage\":65011712,\"pid\":21703} {\"correlation_id\":\"32799697-dd65-4ad4-b1d1-cd553c4510f8\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"2ac0447f-3c8c-4ce0-baeb-b63ddb76fa9b\",\"account\":null} {\"correlation_id\":\"32799697-dd65-4ad4-b1d1-cd553c4510f8\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":130,\"team_id\":42} {\"correlation_id\":\"32799697-dd65-4ad4-b1d1-cd553c4510f8\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"32799697-dd65-4ad4-b1d1-cd553c4510f8\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"32799697-dd65-4ad4-b1d1-cd553c4510f8\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":23.85,\"usage\":23024000,\"real_usage\":65011712,\"pid\":21703,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"32799697-dd65-4ad4-b1d1-cd553c4510f8\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"usage\":22984760,\"real_usage\":65011712,\"pid\":21703} {\"correlation_id\":\"0732b08b-2bf7-4f5b-93b6-9b2889269535\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"33e34a7a-1c02-4f04-87ac-22c3a385e6e3\",\"account\":{\"Jiminny\\\\Models\\\\SocialAccount\":{\"id\":306,\"sociable_id\":109,\"provider_user_id\":\"11348452\",\"expires\":1701077403,\"refresh_token_expires\":null,\"provider\":\"hubspot\",\"state\":\"full-refresh\",\"auth_scope\":null,\"retry_after\":null,\"created_at\":\"2020-09-01 16:59:04\",\"updated_at\":\"2023-11-27 09:30:03\"}}} {\"correlation_id\":\"0732b08b-2bf7-4f5b-93b6-9b2889269535\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":109,\"team_id\":29} {\"correlation_id\":\"0732b08b-2bf7-4f5b-93b6-9b2889269535\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"0732b08b-2bf7-4f5b-93b6-9b2889269535\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"0732b08b-2bf7-4f5b-93b6-9b2889269535\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:28] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":14.98,\"usage\":23047624,\"real_usage\":65011712,\"pid\":21703,\"reason\":\"Your HubSpot account has become disconnected. Please login to Jiminny to reconnect.\"} {\"correlation_id\":\"0732b08b-2bf7-4f5b-93b6-9b2889269535\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:29] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:29] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:29] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:29] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:29] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:29] local.INFO: [HubSpot Journal Polling] Service ending {\"runtime_seconds\":56,\"total_cycles\":5,\"files_downloaded\":0,\"empty_files\":0,\"other_portal_skipped\":0,\"total_events\":0,\"events_per_file\":0,\"avg_api_ms\":192.1,\"avg_download_ms\":0.0,\"avg_transform_ms\":0.0,\"avg_process_ms\":0.0,\"peak_memory_mb\":99.88} {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:29] local.INFO: [HubSpot Journal Polling] Released polling lock {\"correlation_id\":\"69a84dc3-5ad9-4eb1-8057-5b1e80de395e\",\"trace_id\":\"5e8af621-8c17-4344-bc12-18da6b71d516\"}\n[2026-05-28 08:31:29] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"usage\":23005136,\"real_usage\":65011712,\"pid\":21703} {\"correlation_id\":\"4f83a1ac-418f-4f49-8377-328b614306fc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:29] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"71e3aac5-fb66-47c5-a236-2d051ae3e319\",\"account\":null} {\"correlation_id\":\"4f83a1ac-418f-4f49-8377-328b614306fc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:29] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":256,\"team_id\":49} {\"correlation_id\":\"4f83a1ac-418f-4f49-8377-328b614306fc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:29] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"4f83a1ac-418f-4f49-8377-328b614306fc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:29] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"4f83a1ac-418f-4f49-8377-328b614306fc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:31:29] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":34.51,\"usage\":23021248,\"real_usage\":65011712,\"pid\":21703,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"4f83a1ac-418f-4f49-8377-328b614306fc\",\"trace_id\":\"aa1cae8a-acff-4b75-8a3d-e4f9ed33c815\"}\n[2026-05-28 08:32:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"35f98b1b-7b16-4e79-a420-7e9818097d24\",\"trace_id\":\"d66deb58-fa13-486f-a866-2cac402dc589\"}\n[2026-05-28 08:32:06] local.INFO: [ScheduleBotCommand] Dispatched activities to capture {\"count\":0} {\"correlation_id\":\"35f98b1b-7b16-4e79-a420-7e9818097d24\",\"trace_id\":\"d66deb58-fa13-486f-a866-2cac402dc589\"}\n[2026-05-28 08:32:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"35f98b1b-7b16-4e79-a420-7e9818097d24\",\"trace_id\":\"d66deb58-fa13-486f-a866-2cac402dc589\"}\n[2026-05-28 08:32:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"bc6784c1-c46f-4295-be09-87334f4baa39\",\"trace_id\":\"74b00236-4ed0-4464-8b07-99c328ab6461\"}\n[2026-05-28 08:32:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"bc6784c1-c46f-4295-be09-87334f4baa39\",\"trace_id\":\"74b00236-4ed0-4464-8b07-99c328ab6461\"}\n[2026-05-28 08:32:11] local.NOTICE: Monitoring start {\"correlation_id\":\"caf4d1dd-2a52-40f5-834a-f9148f642e83\",\"trace_id\":\"1312bdcc-7e8b-480a-a935-2433fb79dc70\"}\n[2026-05-28 08:32:11] local.NOTICE: Monitoring end {\"correlation_id\":\"caf4d1dd-2a52-40f5-834a-f9148f642e83\",\"trace_id\":\"1312bdcc-7e8b-480a-a935-2433fb79dc70\"}\n[2026-05-28 08:32:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"64d86889-1f0d-4f7d-a1ac-6f56e58e775e\",\"trace_id\":\"b184d66b-49f8-40ec-afbe-2e76e9c7d952\"}\n[2026-05-28 08:32:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"64d86889-1f0d-4f7d-a1ac-6f56e58e775e\",\"trace_id\":\"b184d66b-49f8-40ec-afbe-2e76e9c7d952\"}\n[2026-05-28 08:32:20] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"8e6dd4bc-4f44-4fb2-b2ba-9e2f385996ef\",\"trace_id\":\"7767e0ca-37a3-43a5-972e-ea9a1bedea2f\"}\n[2026-05-28 08:32:20] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"8e6dd4bc-4f44-4fb2-b2ba-9e2f385996ef\",\"trace_id\":\"7767e0ca-37a3-43a5-972e-ea9a1bedea2f\"}\n[2026-05-28 08:32:20] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"8e6dd4bc-4f44-4fb2-b2ba-9e2f385996ef\",\"trace_id\":\"7767e0ca-37a3-43a5-972e-ea9a1bedea2f\"}\n[2026-05-28 08:32:20] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"8e6dd4bc-4f44-4fb2-b2ba-9e2f385996ef\",\"trace_id\":\"7767e0ca-37a3-43a5-972e-ea9a1bedea2f\"}\n[2026-05-28 08:32:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"6750ab25-6dff-4e7b-9f6e-bc7d1fc4221a\",\"trace_id\":\"3db6ce9b-167d-4214-a32c-67c611aa2bbe\"}\n[2026-05-28 08:32:22] local.INFO: Running conference:monitor:count command for activities in (2026-05-28 08:30:00, 2026-05-28 08:32:00] {\"correlation_id\":\"6750ab25-6dff-4e7b-9f6e-bc7d1fc4221a\",\"trace_id\":\"3db6ce9b-167d-4214-a32c-67c611aa2bbe\"}\n[2026-05-28 08:32:22] local.INFO: [conference:monitor:count] No activities found in (2026-05-28 08:30:00, 2026-05-28 08:32:00] {\"correlation_id\":\"6750ab25-6dff-4e7b-9f6e-bc7d1fc4221a\",\"trace_id\":\"3db6ce9b-167d-4214-a32c-67c611aa2bbe\"}\n[2026-05-28 08:32:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"6750ab25-6dff-4e7b-9f6e-bc7d1fc4221a\",\"trace_id\":\"3db6ce9b-167d-4214-a32c-67c611aa2bbe\"}\n[2026-05-28 08:32:23] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.883} {\"correlation_id\":\"04510c2c-638c-4687-81d7-b12e76f41df3\",\"trace_id\":\"87f0bb69-8fd1-47f1-9c9e-3b0ac3d6bd60\"}\n[2026-05-28 08:32:23] local.INFO: [EmailSchedule] STARTING batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"04510c2c-638c-4687-81d7-b12e76f41df3\",\"trace_id\":\"87f0bb69-8fd1-47f1-9c9e-3b0ac3d6bd60\"}\n[2026-05-28 08:32:23] local.INFO: [EmailSchedule] FINISHED batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"04510c2c-638c-4687-81d7-b12e76f41df3\",\"trace_id\":\"87f0bb69-8fd1-47f1-9c9e-3b0ac3d6bd60\"}\n[2026-05-28 08:32:23] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.883,\"memoryPeakAfterCommandInMB\":99.883} {\"correlation_id\":\"04510c2c-638c-4687-81d7-b12e76f41df3\",\"trace_id\":\"87f0bb69-8fd1-47f1-9c9e-3b0ac3d6bd60\"}\n[2026-05-28 08:32:25] local.INFO: [Jiminny\\Jobs\\Mailbox\\CreateBatches] processed 1 inboxes and created 0 batches {\"userId\":null,\"batchSize\":30,\"maxBatches\":1000} {\"correlation_id\":\"e60597ef-aca8-4bda-bb90-18f01e96874d\",\"trace_id\":\"87f0bb69-8fd1-47f1-9c9e-3b0ac3d6bd60\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-4191285066257905338
|
3613057122943418527
|
visual_change
|
accessibility
|
NULL
|
Committing…
Project: faVsco.js, menu
JY-20915-fix- Committing…
Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
1
15
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
[2026-05-28 08:28:04] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.883} {"correlation_id":"73199e23-a066-42ba-b189-2802e0869a44","trace_id":"e53672c2-0f50-409d-af25-84ab485c505d"}
[2026-05-28 08:28:04] local.INFO: [ScheduleBotCommand] Dispatched activities to capture {"count":0} {"correlation_id":"73199e23-a066-42ba-b189-2802e0869a44","trace_id":"e53672c2-0f50-409d-af25-84ab485c505d"}
[2026-05-28 08:28:04] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.883,"memoryPeakAfterCommandInMB":99.883} {"correlation_id":"73199e23-a066-42ba-b189-2802e0869a44","trace_id":"e53672c2-0f50-409d-af25-84ab485c505d"}
[2026-05-28 08:28:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.883} {"correlation_id":"741b2c4e-6a44-4c46-b9fb-fce017aef225","trace_id":"804dc547-eb25-4b32-8d0f-c87019773085"}
[2026-05-28 08:28:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.883,"memoryPeakAfterCommandInMB":99.883} {"correlation_id":"741b2c4e-6a44-4c46-b9fb-fce017aef225","trace_id":"804dc547-eb25-4b32-8d0f-c87019773085"}
[2026-05-28 08:28:09] local.NOTICE: Monitoring start {"correlation_id":"8ae5c131-edd9-463d-8161-875f4d34320f","trace_id":"1b6420c8-ef4e-4566-81b2-688696064648"}
[2026-05-28 08:28:09] local.NOTICE: Monitoring end {"correlation_id":"8ae5c131-edd9-463d-8161-875f4d34320f","trace_id":"1b6420c8-ef4e-4566-81b2-688696064648"}
[2026-05-28 08:28:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.883} {"correlation_id":"6f5390d3-abe7-4e56-a662-aeed1098941b","trace_id":"b57dc2df-2f4d-4d3b-b09a-aa6e61205d5e"}
[2026-05-28 08:28:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.883,"memoryPeakAfterCommandInMB":99.883} {"correlation_id":"6f5390d3-abe7-4e56-a662-aeed1098941b","trace_id":"b57dc2df-2f4d-4d3b-b09a-aa6e61205d5e"}
[2026-05-28 08:28:12] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.883} {"correlation_id":"ba694158-ea28-4ac1-b134-83ac485146e0","trace_id":"83a26b74-baa5-407e-9fc2-2b49d65b1c77"}
[2026-05-28 08:28:12] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"ba694158-ea28-4ac1-b134-83ac485146e0","trace_id":"83a26b74-baa5-407e-9fc2-2b49d65b1c77"}
[2026-05-28 08:28:12] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"ba694158-ea28-4ac1-b134-83ac485146e0","trace_id":"83a26b74-baa5-407e-9fc2-2b49d65b1c77"}
[2026-05-28 08:28:12] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.883,"memoryPeakAfterCommandInMB":99.883} {"correlation_id":"ba694158-ea28-4ac1-b134-83ac485146e0","trace_id":"83a26b74-baa5-407e-9fc2-2b49d65b1c77"}
[2026-05-28 08:28:14] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"conference:monitor:count","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.883} {"correlation_id":"b79833c7-3eab-4eb7-98bb-42359039b8fb","trace_id":"a8e159bc-867d-40af-bf7d-1228cf6d4f16"}
[2026-05-28 08:28:14] local.INFO: Running conference:monitor:count command for activities in (2026-05-28 08:26:00, 2026-05-28 08:28:00] {"correlation_id":"b79833c7-3eab-4eb7-98bb-42359039b8fb","trace_id":"a8e159bc-867d-40af-bf7d-1228cf6d4f16"}
[2026-05-28 08:28:14] local.INFO: [conference:monitor:count] No activities found in (2026-05-28 08:26:00, 2026-05-28 08:28:00] {"correlation_id":"b79833c7-3eab-4eb7-98bb-42359039b8fb","trace_id":"a8e159bc-867d-40af-bf7d-1228cf6d4f16"}
[2026-05-28 08:28:14] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"conference:monitor:count","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.883,"memoryPeakAfterCommandInMB":99.883} {"correlation_id":"b79833c7-3eab-4eb7-98bb-42359039b8fb","trace_id":"a8e159bc-867d-40af-bf7d-1228cf6d4f16"}
[2026-05-28 08:28:16] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:retry-failed","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.883} {"correlation_id":"d51071d5-ead0-45f0-a07e-e3234074f884","trace_id":"77173d9f-596f-405f-ae55-ba117b66455d"}
[2026-05-28 08:28:16] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"calendar:sync","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.883} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:16] local.NOTICE: Calendar sync start {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:16] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:retry-failed","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.883,"memoryPeakAfterCommandInMB":99.883} {"correlation_id":"d51071d5-ead0-45f0-a07e-e3234074f884","trace_id":"77173d9f-596f-405f-ae55-ba117b66455d"}
[2026-05-28 08:28:16] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1393,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:16] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1393,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:16] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:16] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1393,"provider":"google","refreshToken":"5aa7e2d96b53201cd16fca5d2e4ef3ad03320971fc064781d18aee3ae7b99fbf","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1393,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1393,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1387,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1387,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1387,"provider":"google","refreshToken":"8157ac6de94842937194009e9c50e459253600f799dacf6a40755ffdbeb5bba6","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1387,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1387,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1348,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1348,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1348,"provider":"google","refreshToken":"9e7d13d3032d0cb1b79d8e95aef01383e8e91eb52ff8ee960c8a0b6b95cd8c73","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1348,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1348,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1361,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1361,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1361,"provider":"google","refreshToken":"6c843da199c2b9907445329304fcc4ec5057a4ee748d8299641764395c08e1fd","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1361,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1361,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1310,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1310,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1310,"provider":"google","refreshToken":"e34818922c2830a660813a63f6169a4a9a992ae2cccd7dc8dd7796cfdb470ef1","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1310,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1310,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1333,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1333,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1333,"provider":"google","refreshToken":"6c902986546d8e8da1dc539b046cdc1d458f519acc972e5b5f1d6a1a295165e0","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1333,"provider":"google","responseBody":{"error":"unauthorized_client","error_description":"Unauthorized"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1333,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1368,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1368,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:17] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1368,"provider":"google","refreshToken":"d2f128898ff8543bd16b69cfae37896ab85119b0f5ed2b431d739593bb600333","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1368,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1368,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1365,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1365,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1365,"provider":"google","refreshToken":"7676e4a9afcd082b413248ab5ec6e487021fec6a9bdf315860a59cefad9caad8","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1365,"provider":"google","responseBody":{"error":"unauthorized_client","error_description":"Unauthorized"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1365,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1364,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1364,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1364,"provider":"google","refreshToken":"dd5882ebce76e645292ce33ae74238abbb77c0a4ecc6a2bfe723cad82e72ba8e","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1364,"provider":"google","responseBody":{"error":"unauthorized_client","error_description":"Unauthorized"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1364,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1370,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1370,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1370,"provider":"office","refreshToken":"b7ee8035306d0043cea6e00e7c4fe14f745e44074a1194db62a31cdf8b70af3e","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1370,"provider":"office","responseBody":"{\"error\":\"invalid_client\",\"error_description\":\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 41d8db5c-a91f-41a6-9b5c-22a6c99b9100 Correlation ID: 70bc0a6f-1565-4e74-94dc-419d94108fa9 Timestamp: 2026-05-28 08:28:18Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-28 08:28:18Z\",\"trace_id\":\"41d8db5c-a91f-41a6-9b5c-22a6c99b9100\",\"correlation_id\":\"70bc0a6f-1565-4e74-94dc-419d94108fa9\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1370,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1202,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1202,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:18] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1202,"provider":"office","refreshToken":"b458799ccc29b21a6e2eb5260fdb63e49ccba21bf942a3973fb63799bd7f0afe","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1202,"provider":"office","responseBody":"{\"error\":\"invalid_client\",\"error_description\":\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: d3d14dfc-1b70-40c4-b9db-fab12f748300 Correlation ID: 4c5f746c-8366-44c1-8733-6877607826db Timestamp: 2026-05-28 08:28:19Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-28 08:28:19Z\",\"trace_id\":\"d3d14dfc-1b70-40c4-b9db-fab12f748300\",\"correlation_id\":\"4c5f746c-8366-44c1-8733-6877607826db\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1202,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1502,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1502,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: Calendar sync job dispatched {"calendar_id":501} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1300,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1300,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1300,"provider":"google","refreshToken":"4b811db0725fd9602a95943519a7da935e2a5065da7d9ebfcb170752e3e1ddb8","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1300,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Account has been deleted"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1300,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1409,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1409,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1409,"provider":"google","refreshToken":"e2a3f2d06894894eed1ee87d9db1ace77d4d42ee6e1288a8940ad2c10333b0c4","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1409,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1409,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1352,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1352,"provider":"google"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1352,"provider":"google","refreshToken":"dd4b16b00fdc1216da6b717c02338c073636e29162826b2de6db3f064fc029eb","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1352,"provider":"google","responseBody":{"error":"invalid_grant","error_description":"Bad Request"}} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1352,"provider":"google","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1296,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1296,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1296,"provider":"office","refreshToken":"011ae723c9d800c674e0b4be76f49fc046dac7d501b66c59ef0d9549cfa56ae5","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1502,"provider":"google"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1502,"provider":"google"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [Calendar] Processing sync {"calendarId":"a33076c1-8d97-431a-99f0-85c9524e118b","from":null,"to":null,"delta":"CIiFh8TP44kDEIiFh8TP44kDGAUgkZvkzgIokZvkzgI=","last_sync":"2024-12-09 07:12:53","dateMode":"daily"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"integration-app","crm_owner":1695,"team_id":3143} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1502,"provider":"google"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1502,"provider":"google"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1296,"provider":"office","responseBody":"{\"error\":\"invalid_client\",\"error_description\":\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: dd83a147-95bc-407c-8c9b-3c14e7cb7d00 Correlation ID: bb16bae3-b3c4-467e-bd95-4e7ab85364c9 Timestamp: 2026-05-28 08:28:20Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-28 08:28:20Z\",\"trace_id\":\"dd83a147-95bc-407c-8c9b-3c14e7cb7d00\",\"correlation_id\":\"bb16bae3-b3c4-467e-bd95-4e7ab85364c9\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":1296,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":391,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":391,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":391,"provider":"office","refreshToken":"00045eebae0f39b34887c6d53f92ae78064f7145e1f4b67754aebd03cfb2d881","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.INFO: [Google Calendar] Failed to watch channel for calendar {"calendarId":"a33076c1-8d97-431a-99f0-85c9524e118b","code":400,"reason":"{
\"error\": {
\"errors\": [
{
\"domain\": \"global\",
\"reason\": \"push.webhookUrlNotHttps\",
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
],
\"code\": 400,
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
}"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:20] local.WARNING: [Calendar] Sync failed {"calendarId":"a33076c1-8d97-431a-99f0-85c9524e118b","code":400,"reason":"{
\"error\": {
\"errors\": [
{
\"domain\": \"global\",
\"reason\": \"push.webhookUrlNotHttps\",
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
],
\"code\": 400,
\"message\": \"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\"
}
}"} {"correlation_id":"45e201e4-60d2-407c-ae71-13e4739c0875","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":391,"provider":"office","responseBody":"{\"error\":\"invalid_client\",\"error_description\":\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 587224ad-f597-4371-b2ba-00fcd2da1b00 Correlation ID: 4e752610-95b1-4606-847f-68e00f90be0a Timestamp: 2026-05-28 08:28:21Z\",\"error_codes\":[7000215],\"timestamp\":\"2026-05-28 08:28:21Z\",\"trace_id\":\"587224ad-f597-4371-b2ba-00fcd2da1b00\",\"correlation_id\":\"4e752610-95b1-4606-847f-68e00f90be0a\",\"error_uri\":\"https://login.microsoftonline.com/error?code=7000215\"}"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:21] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {"socialAccountId":391,"provider":"office","reason":"Flow refresh required."} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1271,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1271,"provider":"office"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:21] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"498bfcf1-b82c-466d-8f91-2f3f1cdf980f","trace_id":"19e20da3-a204-446a-a01e-568f25ddd9fd"}
[2026-05-28 08:28:21] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1271,"provider":"office","refreshToken":"118cde2c06993147b07ccaec4cbcd5026a819dea6c71081166a492933e392afb","state":"full-refresh"} {"correlation_id":"498bfcf1-b82c-...
|
81612
|
NULL
|
NULL
|
NULL
|
|
81612
|
2833
|
21
|
2026-05-28T08:32:30.498203+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957150498_m2.jpg...
|
PhpStorm
|
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Committing…
Checking for TODO…
Cancel
Minimize
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Committing…","depth":1,"bounds":{"left":0.44115692,"top":0.42218676,"width":0.15226063,"height":0.01915403},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Checking for TODO…","depth":1,"bounds":{"left":0.44448137,"top":0.4501197,"width":0.11170213,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":1,"bounds":{"left":0.55950797,"top":0.4501197,"width":0.0013297872,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":1,"bounds":{"left":0.44448137,"top":0.47486034,"width":0.11170213,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":1,"bounds":{"left":0.55950797,"top":0.47486034,"width":0.0013297872,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Cancel","depth":1,"bounds":{"left":0.56416225,"top":0.4557063,"width":0.025930852,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Minimize","depth":1,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1576639871368076299
|
20958587585104676
|
click
|
hybrid
|
NULL
|
Committing…
Checking for TODO…
Cancel
Minimize
Php Committing…
Checking for TODO…
Cancel
Minimize
PhpStormCoocWindowPwovscors$2 JY-20915-fx-m) SyneMallbox.php©InternetMessagelnterface.phg© TextRelayServiceTest.phpE env.production© MailChannelService.phpclass TextReLayServiceo Textrelkysewice.on› @ MeetingGeneratoroavare tuncczon zsrorcurehcchvarohaehtanoucadorEOHUNPlaybooks—KeCaLA} catch (\Exception $e) €Log::error( message: ' [TextRelaySe'nessage_id' => Snessageid,'exception' => Se-›getMessagJotaeo(Sentry::captureException(Se);a TeamTelephony#UserPilotWebhookC Abstrac Semvice cho©ActivityProviderFactory.php©ActivityService.php© ApiResponseService.phpCeonarsneasaries ood©InsightSeatService.php© InstantMeetingService.php© IntercomService.php©lpapiClient.phpLocal ChangesChanges 6 filesE.env.local apc© JiminnyDebugCommand.php app/Console/Commandsme logging.php config@ PlaybackService.pho app/Services©) Tex Relayservice ono aoosenniceswas© TextRelayServiceTest.php tests/Unit/Services/Mail› Unversioned Files 9 filesprivate function matchesExpectedRecipienDoarts- exoLouecount Soarts) aaz strcasecnireturn false.8828865ed app/Sttry 1& Dockerfievle Mailltih• © TextRelayService.php©PlaybackService.phpe contiothle2 modifiedCommk Messageo85x nissing neadenCode AnalysisPemescontin oroohoerrors anona watninesoutewouc voulike oxewewienthv Diffeommit andllpusT.0+→0side-oy-soe viewerDo not ignore6828865ed app/Services/Mail/TextRelayService.phpAnalyzing code…..trysuessace_ sservice-susersnessacesoscetshazlooxSheaders - snessa.eserrayloadoscecheadersforeach (Sheaders as Sheader) (if (Sheader-›name aan ^X-Gm-Original-To') €Snatches = Sthis->matchesExpectedRecipient(Sheader->value, SexpectedAL1a1f (1 Smatches) (// Sanitize PII by renoving plus-tag content for loggingriginalto = explode(*+', Sheader->value)[0);Log: :info(' [TextRelayService) Refused message'. []•messaae so' e> Snessacelc'original_to_sanitized' = SsanitizedoriginalTo,Conomalro samoret 8ssanereounonaoD):returratches.Logksnacnina("[TextRelavServicel Refused message: nissina X-6--0otoinsl-To hesdenn"nessage_id' => Snessageld,E laravellog X 4 SF jiminny@localhost)A HSJocal ([iminny@localhost)A console (EU]A console [STAGING)186 €A console [PROD]Changelist:GitAuthor:Amend commitSign-off commitcommt checksUpdate copyrightReformat code• Rearrange codeOptimize importsReview Code AnalysisCancel= Sservice-›users_nessages->get(Snailbox, Snessageid):Sheaders = Snessage->getPayLoad()->getHeaders):owohalo8nueforeach (Sheaders as Sheader) (if (Sheader-›nane === 'X-Gm-Original-To') (sorigznallo = sheader->value} elseif (Sheaden->nane === "To') {Srecipient = SoriginalTo ?? $to;Cancel1f (1 Snatches) (// Sanitize PII by removing plus-tag content for loggingSsanitizedRecipient = explode(*+*, Srecipient)[0]:Log:: info(' (TextRelayService) Refused message',"nessage_id' => Smessageld,ortoiniilo sinkelce7o0sLXInu Lo moy trrocioller henwserwiet+0 .fox@TextRelayServiceTest-phpWsyScrvica est.pho #L154-188voewicclesihont+2-2tayServiceTest.php #L150-199Misfor@urrent Environment.al|sBacklol ohlesder in testeUnt Samicas Moiln extRelayServicel est.oho.ysemwicelescono oo-eayservicelest.phe+1-1SWE-1.6000SotterenceAt, SexpectedAlias, SexpectedHost);UTE-R OuAcnodd...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
81611
|
2832
|
21
|
2026-05-28T08:32:30.396568+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957150396_m1.jpg...
|
PhpStorm
|
Code Analysis
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Code Analysis
text/html
text/html
text/html
2 file Code Analysis
text/html
text/html
text/html
2 files contain problems.<br/>No errors and 14 warnings found.<br/>Would you like to review them?
Cancel
Commit and Push
Review Code Analysis...
|
[{"role":"AXTextField","text [{"role":"AXTextField","text":"Code Analysis","depth":1,"on_screen":true,"value":"Code Analysis","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":2,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":2,"on_screen":true,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":2,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2 files contain problems.<br/>No errors and 14 warnings found.<br/>Would you like to review them?","depth":1,"on_screen":true,"help_text":"text/html","role_description":"text"},{"role":"AXButton","text":"Cancel","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit and Push","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Review Code Analysis","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8959028634516024063
|
1239976702321423084
|
click
|
accessibility
|
NULL
|
Code Analysis
text/html
text/html
text/html
2 file Code Analysis
text/html
text/html
text/html
2 files contain problems.<br/>No errors and 14 warnings found.<br/>Would you like to review them?
Cancel
Commit and Push
Review Code Analysis...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
81610
|
2833
|
20
|
2026-05-28T08:32:17.431273+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957137431_m2.jpg...
|
PhpStorm
|
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormCoocWindowf vovscors$2 JY-20915-fx-m) Syne PhpStormCoocWindowf vovscors$2 JY-20915-fx-m) SyneMallbox.php©InternetMessagelnterface.phg© TextRelayServiceTest.phpE env.production© MailChannelService.phpclass TextReLayServiceo Textrelkysewice.on› @ MeetingGeneratoroavare tuncczon csrorcurehcchvarohaehtanowcadonEOHUNPlaybooks—KeCaLA} catch (\Exception $e) €Log::error( message: ' [TextRelaySe'nessage_id' => Snessageid,'exception' => Se->getMessag& Dockerfie+5GJotaeo(Sentry::captureException(Se);Ve Mailltik• © TextRelayService.php©PlaybackService.phpe contiothle2 modifieda TeamTelephony#UserPilotWebhook227C Abstrac Semvice [EMAIL]©ActivityService.php© ApiResponseService.phpCeonarsneasaries ood©InsightSeatService.php© InstantMeetingService.php© IntercomService.php©lpapiClient.phpLocal ChangesChanges 6 filesE.env.local apc© JiminnyDebugCommand.php app/Console/Commandsme logging.php config@ PlaybackService.pho app/Services©) Tex Relayservice oho aoo sericesvas© TextRelayServiceTest.php tests/Unit/Services/Mail› Unversioned Files 9 filesCommk Message85 x nissina neadenprivate function matchesExpectedRecipienDoants- exorodeltT.0+→0Side-by-side viewer -Do not ignorecount Soarts)aZ strcasecr6828865ed app/Services/Mail/TextRelayService.phpreturn false.Hghight wods -X16 g ?trysuessace_ sservice-susersnessacesoscetshazlooxSnessageId);Sheaders - snessa.eserrayloadoscecheaders8828865ed app/Sforeach (Sheaders as Sheader) (if (Sheader-›name aan ^X-Gm-Original-To') €Snatches = Sthis->matchesExpectedRecipient(Sheader->value, SexpectedALia1f (1 Smatches) (I/ Sanitize PII by renoving plus-tag content for l09gingimiqinauio = exoloder+" Shesder->ual ueslieleLog: :info(' [TextRelayService) Refused message'. []•messaae so' e> Snessacelc'original_to_sanitized' = SsanitizedoriginalTo,ononalco samoreo sssamhereounonauoD):returratches.Logksnacnina("[TextRelavServicel Refused message: nissina X-6--0otoinsl-To hesdenn"nessage_id' => Snessageld,|E laravellog X 4 SF jiminny@localhost)A HSJocal ([iminny@localhost)A console [PROD]A console (EU]A console [STAGING)commit ChandeChangelist:Author:Amend commitSign-off commitcommt checksUpdate copyrightReformat code• Rearrange codeOptimize importsE Current version185186 €try(Snessage = Sservice-›users_nessages->get(Smailbox, SnessageId);Sheaders = Snessage->getPayLoad()->getHeaders):owohalo8nueSto = null;189foreach (Sheaders as Sheader) (if (Sheader-›nane === 'X-Gm-Original-To') (sorigznallo = sheader->value} elseif (Sheaden->nane === "To') {Srecipient = SoriginalTo ?? $to;Cancelif (1 Snatches) (// Sanitize PII by removing plus-tag content for loggingSsanitizedRecipient = explode(*+*, Srecipient)[0]:Log:: info(' (TextRelayService) Refused message',"nessage_id' => Smessageld,ortoiniilo sinkelce7o0sLXinu Lo mey thoc.ler henwserwietfox@TextRelayServiceTest-phpWayScrvica est.pho #l154-188ayoeviceleshont+2-2layServiceTest.php #L150-199Iseoreurrent Environment.al|sBackio ohesder in testeUnt Samicas Moiln extRelay Service est oho.ysemwicalestono00-el+1-1SWE-1.6000SotterenceAt, SexpectedALias, SexpectedHost);UTE-R OuAcnodd...
|
NULL
|
-6206891737901787450
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormCoocWindowf vovscors$2 JY-20915-fx-m) Syne PhpStormCoocWindowf vovscors$2 JY-20915-fx-m) SyneMallbox.php©InternetMessagelnterface.phg© TextRelayServiceTest.phpE env.production© MailChannelService.phpclass TextReLayServiceo Textrelkysewice.on› @ MeetingGeneratoroavare tuncczon csrorcurehcchvarohaehtanowcadonEOHUNPlaybooks—KeCaLA} catch (\Exception $e) €Log::error( message: ' [TextRelaySe'nessage_id' => Snessageid,'exception' => Se->getMessag& Dockerfie+5GJotaeo(Sentry::captureException(Se);Ve Mailltik• © TextRelayService.php©PlaybackService.phpe contiothle2 modifieda TeamTelephony#UserPilotWebhook227C Abstrac Semvice [EMAIL]©ActivityService.php© ApiResponseService.phpCeonarsneasaries ood©InsightSeatService.php© InstantMeetingService.php© IntercomService.php©lpapiClient.phpLocal ChangesChanges 6 filesE.env.local apc© JiminnyDebugCommand.php app/Console/Commandsme logging.php config@ PlaybackService.pho app/Services©) Tex Relayservice oho aoo sericesvas© TextRelayServiceTest.php tests/Unit/Services/Mail› Unversioned Files 9 filesCommk Message85 x nissina neadenprivate function matchesExpectedRecipienDoants- exorodeltT.0+→0Side-by-side viewer -Do not ignorecount Soarts)aZ strcasecr6828865ed app/Services/Mail/TextRelayService.phpreturn false.Hghight wods -X16 g ?trysuessace_ sservice-susersnessacesoscetshazlooxSnessageId);Sheaders - snessa.eserrayloadoscecheaders8828865ed app/Sforeach (Sheaders as Sheader) (if (Sheader-›name aan ^X-Gm-Original-To') €Snatches = Sthis->matchesExpectedRecipient(Sheader->value, SexpectedALia1f (1 Smatches) (I/ Sanitize PII by renoving plus-tag content for l09gingimiqinauio = exoloder+" Shesder->ual ueslieleLog: :info(' [TextRelayService) Refused message'. []•messaae so' e> Snessacelc'original_to_sanitized' = SsanitizedoriginalTo,ononalco samoreo sssamhereounonauoD):returratches.Logksnacnina("[TextRelavServicel Refused message: nissina X-6--0otoinsl-To hesdenn"nessage_id' => Snessageld,|E laravellog X 4 SF jiminny@localhost)A HSJocal ([iminny@localhost)A console [PROD]A console (EU]A console [STAGING)commit ChandeChangelist:Author:Amend commitSign-off commitcommt checksUpdate copyrightReformat code• Rearrange codeOptimize importsE Current version185186 €try(Snessage = Sservice-›users_nessages->get(Smailbox, SnessageId);Sheaders = Snessage->getPayLoad()->getHeaders):owohalo8nueSto = null;189foreach (Sheaders as Sheader) (if (Sheader-›nane === 'X-Gm-Original-To') (sorigznallo = sheader->value} elseif (Sheaden->nane === "To') {Srecipient = SoriginalTo ?? $to;Cancelif (1 Snatches) (// Sanitize PII by removing plus-tag content for loggingSsanitizedRecipient = explode(*+*, Srecipient)[0]:Log:: info(' (TextRelayService) Refused message',"nessage_id' => Smessageld,ortoiniilo sinkelce7o0sLXinu Lo mey thoc.ler henwserwietfox@TextRelayServiceTest-phpWayScrvica est.pho #l154-188ayoeviceleshont+2-2layServiceTest.php #L150-199Iseoreurrent Environment.al|sBackio ohesder in testeUnt Samicas Moiln extRelay Service est oho.ysemwicalestono00-el+1-1SWE-1.6000SotterenceAt, SexpectedALias, SexpectedHost);UTE-R OuAcnodd...
|
81608
|
NULL
|
NULL
|
NULL
|
|
81609
|
2832
|
20
|
2026-05-28T08:32:17.330892+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957137330_m1.jpg...
|
PhpStorm
|
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp‹$0A-zshscreenpipe"DOCKER₴81DEV (docker)worker-calendar:worker-calendar_00:startedworker-conferences:worker-conferences_00: startedworker-crm-sync:worker-crm-sync_00: startedworker-crm-update:worker-crm-update_00: startedworker-download:worker-download_00:startedworker-emails:worker-emails_00:startedworker-es-update:worker-es-update_00:startedworker-nudges:worker-nudges_00: started₴2-zsh-zshWhat's next:Try Docker Debug forseamless, persistentdebugging tools in any container or image → docker debug docker_lamp_1Learnmoreat [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-fix-missing-header-text-relay) $csfixdocker exec -it docker_lamp_1•/vendor/bin/php-cs-fixer fix--config=.php-cs-fixer.dist.php-v--using-cache=no --diffPHP CS Fixer 3.95.1 Adalbertus by Fabien Potencier, Dariusz Ruminski and contributors.PHP runtime: 8.5.5Loaded config default from ".php-cs-fixer.dist.php".Running analysis on 7 cores with 10 files per process.5698/5698 C80100%*5ec2-user@ip-10-30-1...O 86100% <78• Thu 28 May 11:32:16T81ec2-user@ip-10-30-140-...$7Fixed 0 of 5698 files in 52.731 seconds, 799.06 MB memory usedDetected deprecations in use (they will stop working in next major release):- Rule set"@PHP74Migration" is deprecated. Use"@PHP7x4Migration" instead.- Rule set "@PHP80Migration" is deprecated. Use "@PHP8x®Migration" instead.- Rule set "@PHP81Migration" is deprecated. Use "@PHP8x1Migration" instead.- Rule set "@PHP82Migration" is deprecated. Use "@PHP8x2Migration" instead.- Rule set "@PHP83Migration" is deprecated. Use "@PHP8x3Migration"instead.- Rule set "@PHP84Migration" is deprecated. Use "@PHP8x4Migration" instead.What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image + docker debug docker_lamp_1Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-fix-missing-header-text-relay) $D...
|
NULL
|
530210252177125290
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp‹$0A-zshscreenpipe"DOCKER₴81DEV (docker)worker-calendar:worker-calendar_00:startedworker-conferences:worker-conferences_00: startedworker-crm-sync:worker-crm-sync_00: startedworker-crm-update:worker-crm-update_00: startedworker-download:worker-download_00:startedworker-emails:worker-emails_00:startedworker-es-update:worker-es-update_00:startedworker-nudges:worker-nudges_00: started₴2-zsh-zshWhat's next:Try Docker Debug forseamless, persistentdebugging tools in any container or image → docker debug docker_lamp_1Learnmoreat [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-fix-missing-header-text-relay) $csfixdocker exec -it docker_lamp_1•/vendor/bin/php-cs-fixer fix--config=.php-cs-fixer.dist.php-v--using-cache=no --diffPHP CS Fixer 3.95.1 Adalbertus by Fabien Potencier, Dariusz Ruminski and contributors.PHP runtime: 8.5.5Loaded config default from ".php-cs-fixer.dist.php".Running analysis on 7 cores with 10 files per process.5698/5698 C80100%*5ec2-user@ip-10-30-1...O 86100% <78• Thu 28 May 11:32:16T81ec2-user@ip-10-30-140-...$7Fixed 0 of 5698 files in 52.731 seconds, 799.06 MB memory usedDetected deprecations in use (they will stop working in next major release):- Rule set"@PHP74Migration" is deprecated. Use"@PHP7x4Migration" instead.- Rule set "@PHP80Migration" is deprecated. Use "@PHP8x®Migration" instead.- Rule set "@PHP81Migration" is deprecated. Use "@PHP8x1Migration" instead.- Rule set "@PHP82Migration" is deprecated. Use "@PHP8x2Migration" instead.- Rule set "@PHP83Migration" is deprecated. Use "@PHP8x3Migration"instead.- Rule set "@PHP84Migration" is deprecated. Use "@PHP8x4Migration" instead.What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image + docker debug docker_lamp_1Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-fix-missing-header-text-relay) $D...
|
81607
|
NULL
|
NULL
|
NULL
|
|
81608
|
2833
|
19
|
2026-05-28T08:32:15.912358+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957135912_m2.jpg...
|
PhpStorm
|
Commit Changes
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
6 files, folder partially checked
app 3 files, folder partially checked
Console/Commands 1 file, folder not checked
Services 2 files, folder partially checked
Mail 1 file, folder checked
TextRelayService.php, class checked
PlaybackService.php, class not checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
.env.local not checked
app 3 files, folder partially checked
Console/Commands 1 file, folder not checked
Services 2 files, folder partially checked
Mail 1 file, folder checked
TextRelayService.php, class checked
PlaybackService.php, class not checked
Console/Commands 1 file, folder not checked
Services 2 files, folder partially checked
Mail 1 file, folder checked
TextRelayService.php, class checked
PlaybackService.php, class not checked
Mail 1 file, folder checked
TextRelayService.php, class checked
TextRelayService.php, class checked
PlaybackService.php, class not checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
TextRelayServiceTest.php, class checked
.env.local not checked
Unversioned Files 9 files not checked
2 modified
Analyzing…...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"bounds":{"left":0.25531915,"top":0.39185953,"width":0.013962766,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"bounds":{"left":0.58211434,"top":0.15802075,"width":0.022938829,"height":0.027134877},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"bounds":{"left":0.60638297,"top":0.15802075,"width":0.042220745,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"bounds":{"left":0.25664893,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"bounds":{"left":0.26529256,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"bounds":{"left":0.27393618,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"bounds":{"left":0.28490692,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"bounds":{"left":0.56150264,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"bounds":{"left":0.57014626,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6 files, folder partially checked","depth":4,"bounds":{"left":0.26462767,"top":0.11572227,"width":0.030585106,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 3 files, folder partially checked","depth":5,"bounds":{"left":0.27094415,"top":0.13328013,"width":0.038231384,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":6,"bounds":{"left":0.27726063,"top":0.15083799,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services 2 files, folder partially checked","depth":6,"bounds":{"left":0.27726063,"top":0.16839585,"width":0.047872342,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Mail 1 file, folder checked","depth":7,"bounds":{"left":0.2835771,"top":0.1859537,"width":0.035904255,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":8,"bounds":{"left":0.28989363,"top":0.20351157,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"PlaybackService.php, class not checked","depth":7,"bounds":{"left":0.2835771,"top":0.22106944,"width":0.058843084,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"config 1 file, folder not checked","depth":5,"bounds":{"left":0.27094415,"top":0.2386273,"width":0.040226065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Mail 1 file, folder checked","depth":5,"bounds":{"left":0.27094415,"top":0.25618514,"width":0.07646277,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class checked","depth":6,"bounds":{"left":0.27726063,"top":0.273743,"width":0.06981383,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":5,"bounds":{"left":0.27094415,"top":0.29130086,"width":0.03557181,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 3 files, folder partially checked","depth":4,"bounds":{"left":0.27094415,"top":0.13328013,"width":0.038231384,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":5,"bounds":{"left":0.27726063,"top":0.15083799,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services 2 files, folder partially checked","depth":5,"bounds":{"left":0.27726063,"top":0.16839585,"width":0.047872342,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Mail 1 file, folder checked","depth":6,"bounds":{"left":0.2835771,"top":0.1859537,"width":0.035904255,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":7,"bounds":{"left":0.28989363,"top":0.20351157,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"PlaybackService.php, class not checked","depth":6,"bounds":{"left":0.2835771,"top":0.22106944,"width":0.058843084,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":4,"bounds":{"left":0.27726063,"top":0.15083799,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services 2 files, folder partially checked","depth":4,"bounds":{"left":0.27726063,"top":0.16839585,"width":0.047872342,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Mail 1 file, folder checked","depth":5,"bounds":{"left":0.2835771,"top":0.1859537,"width":0.035904255,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":6,"bounds":{"left":0.28989363,"top":0.20351157,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"PlaybackService.php, class not checked","depth":5,"bounds":{"left":0.2835771,"top":0.22106944,"width":0.058843084,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Mail 1 file, folder checked","depth":4,"bounds":{"left":0.2835771,"top":0.1859537,"width":0.035904255,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":5,"bounds":{"left":0.28989363,"top":0.20351157,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":4,"bounds":{"left":0.28989363,"top":0.20351157,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"PlaybackService.php, class not checked","depth":4,"bounds":{"left":0.2835771,"top":0.22106944,"width":0.058843084,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"config 1 file, folder not checked","depth":4,"bounds":{"left":0.27094415,"top":0.2386273,"width":0.040226065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Mail 1 file, folder checked","depth":4,"bounds":{"left":0.27094415,"top":0.25618514,"width":0.07646277,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class checked","depth":5,"bounds":{"left":0.27726063,"top":0.273743,"width":0.06981383,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class checked","depth":4,"bounds":{"left":0.27726063,"top":0.273743,"width":0.06981383,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":4,"bounds":{"left":0.27094415,"top":0.29130086,"width":0.03557181,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Unversioned Files 9 files not checked","depth":4,"bounds":{"left":0.26462767,"top":0.30885875,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.3932846,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Analyzing…","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.019946808,"height":0.0},"on_screen":false,"role_description":"text"}]...
|
-8679114625230186637
|
-2408818372429766284
|
click
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
6 files, folder partially checked
app 3 files, folder partially checked
Console/Commands 1 file, folder not checked
Services 2 files, folder partially checked
Mail 1 file, folder checked
TextRelayService.php, class checked
PlaybackService.php, class not checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
.env.local not checked
app 3 files, folder partially checked
Console/Commands 1 file, folder not checked
Services 2 files, folder partially checked
Mail 1 file, folder checked
TextRelayService.php, class checked
PlaybackService.php, class not checked
Console/Commands 1 file, folder not checked
Services 2 files, folder partially checked
Mail 1 file, folder checked
TextRelayService.php, class checked
PlaybackService.php, class not checked
Mail 1 file, folder checked
TextRelayService.php, class checked
TextRelayService.php, class checked
PlaybackService.php, class not checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
TextRelayServiceTest.php, class checked
.env.local not checked
Unversioned Files 9 files not checked
2 modified
Analyzing…...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
81607
|
2832
|
19
|
2026-05-28T08:32:15.812371+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957135812_m1.jpg...
|
PhpStorm
|
Commit Changes
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
6 files, folder partially checked
app 3 files, folder partially checked
Console/Commands 1 file, folder not checked
Services 2 files, folder partially checked
Mail 1 file, folder checked
TextRelayService.php, class checked
PlaybackService.php, class not checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
.env.local not checked
app 3 files, folder partially checked
Console/Commands 1 file, folder not checked
Services 2 files, folder partially checked
Mail 1 file, folder checked
TextRelayService.php, class checked
PlaybackService.php, class not checked
Console/Commands 1 file, folder not checked
Services 2 files, folder partially checked
Mail 1 file, folder checked
TextRelayService.php, class checked
PlaybackService.php, class not checked
Mail 1 file, folder checked
TextRelayService.php, class checked
TextRelayService.php, class checked
PlaybackService.php, class not checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
TextRelayServiceTest.php, class checked
.env.local not checked
Unversioned Files 9 files not checked
2 modified
Analyzing…
JY-20915 fix missing header
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6 files, folder partially checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 3 files, folder partially checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services 2 files, folder partially checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Mail 1 file, folder checked","depth":7,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":8,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"PlaybackService.php, class not checked","depth":7,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"config 1 file, folder not checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Mail 1 file, folder checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 3 files, folder partially checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services 2 files, folder partially checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Mail 1 file, folder checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":7,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"PlaybackService.php, class not checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services 2 files, folder partially checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Mail 1 file, folder checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":6,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"PlaybackService.php, class not checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Mail 1 file, folder checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"PlaybackService.php, class not checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"config 1 file, folder not checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Mail 1 file, folder checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class checked","depth":5,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Unversioned Files 9 files not checked","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2 modified","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Analyzing…","depth":3,"bounds":{"left":0.0,"top":0.0,"width":0.041666668,"height":0.02},"on_screen":false,"role_description":"text"},{"role":"AXTextArea","text":"JY-20915 fix missing header","depth":2,"on_screen":true,"value":"JY-20915 fix missing header","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Git","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
9200374331756036901
|
-102975371806039708
|
click
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
6 files, folder partially checked
app 3 files, folder partially checked
Console/Commands 1 file, folder not checked
Services 2 files, folder partially checked
Mail 1 file, folder checked
TextRelayService.php, class checked
PlaybackService.php, class not checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
.env.local not checked
app 3 files, folder partially checked
Console/Commands 1 file, folder not checked
Services 2 files, folder partially checked
Mail 1 file, folder checked
TextRelayService.php, class checked
PlaybackService.php, class not checked
Console/Commands 1 file, folder not checked
Services 2 files, folder partially checked
Mail 1 file, folder checked
TextRelayService.php, class checked
PlaybackService.php, class not checked
Mail 1 file, folder checked
TextRelayService.php, class checked
TextRelayService.php, class checked
PlaybackService.php, class not checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
TextRelayServiceTest.php, class checked
.env.local not checked
Unversioned Files 9 files not checked
2 modified
Analyzing…
JY-20915 fix missing header
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
81606
|
2833
|
18
|
2026-05-28T08:32:15.292048+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957135292_m2.jpg...
|
PhpStorm
|
Commit Changes
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
6 files, folder partially checked
app 3 files, folder partially checked
Console/Commands 1 file, folder not checked
Services 2 files, folder partially checked
Mail 1 file, folder checked
TextRelayService.php, class checked
PlaybackService.php, class not checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
.env.local not checked
app 3 files, folder partially checked
Console/Commands 1 file, folder not checked
Services 2 files, folder partially checked
Mail 1 file, folder checked
TextRelayService.php, class checked
PlaybackService.php, class not checked
Console/Commands 1 file, folder not checked...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"bounds":{"left":0.25531915,"top":0.39185953,"width":0.013962766,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"bounds":{"left":0.58211434,"top":0.15802075,"width":0.022938829,"height":0.027134877},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"bounds":{"left":0.60638297,"top":0.15802075,"width":0.042220745,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"bounds":{"left":0.25664893,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"bounds":{"left":0.26529256,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"bounds":{"left":0.27393618,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"bounds":{"left":0.28490692,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"bounds":{"left":0.56150264,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"bounds":{"left":0.57014626,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6 files, folder partially checked","depth":4,"bounds":{"left":0.26462767,"top":0.11572227,"width":0.030585106,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 3 files, folder partially checked","depth":5,"bounds":{"left":0.27094415,"top":0.13328013,"width":0.038231384,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":6,"bounds":{"left":0.27726063,"top":0.15083799,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services 2 files, folder partially checked","depth":6,"bounds":{"left":0.27726063,"top":0.16839585,"width":0.047872342,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Mail 1 file, folder checked","depth":7,"bounds":{"left":0.2835771,"top":0.1859537,"width":0.035904255,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":8,"bounds":{"left":0.28989363,"top":0.20351157,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"PlaybackService.php, class not checked","depth":7,"bounds":{"left":0.2835771,"top":0.22106944,"width":0.058843084,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"config 1 file, folder not checked","depth":5,"bounds":{"left":0.27094415,"top":0.2386273,"width":0.040226065,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tests/Unit/Services/Mail 1 file, folder checked","depth":5,"bounds":{"left":0.27094415,"top":0.25618514,"width":0.07646277,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayServiceTest.php, class checked","depth":6,"bounds":{"left":0.27726063,"top":0.273743,"width":0.06981383,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":".env.local not checked","depth":5,"bounds":{"left":0.27094415,"top":0.29130086,"width":0.03557181,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"app 3 files, folder partially checked","depth":4,"bounds":{"left":0.27094415,"top":0.13328013,"width":0.038231384,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":5,"bounds":{"left":0.27726063,"top":0.15083799,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Services 2 files, folder partially checked","depth":5,"bounds":{"left":0.27726063,"top":0.16839585,"width":0.047872342,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Mail 1 file, folder checked","depth":6,"bounds":{"left":0.2835771,"top":0.1859537,"width":0.035904255,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TextRelayService.php, class checked","depth":7,"bounds":{"left":0.28989363,"top":0.20351157,"width":0.060837764,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"PlaybackService.php, class not checked","depth":6,"bounds":{"left":0.2835771,"top":0.22106944,"width":0.058843084,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Console/Commands 1 file, folder not checked","depth":4,"bounds":{"left":0.27726063,"top":0.15083799,"width":0.06881649,"height":0.017557861},"on_screen":true,"role_description":"text"}]...
|
-6210975635184074221
|
6526323287199719748
|
visual_change
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
6 files, folder partially checked
app 3 files, folder partially checked
Console/Commands 1 file, folder not checked
Services 2 files, folder partially checked
Mail 1 file, folder checked
TextRelayService.php, class checked
PlaybackService.php, class not checked
config 1 file, folder not checked
tests/Unit/Services/Mail 1 file, folder checked
TextRelayServiceTest.php, class checked
.env.local not checked
app 3 files, folder partially checked
Console/Commands 1 file, folder not checked
Services 2 files, folder partially checked
Mail 1 file, folder checked
TextRelayService.php, class checked
PlaybackService.php, class not checked
Console/Commands 1 file, folder not checked...
|
81603
|
NULL
|
NULL
|
NULL
|
|
81605
|
2832
|
18
|
2026-05-28T08:32:13.729686+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957133729_m1.jpg...
|
PhpStorm
|
faVsco.js – TextRelayService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20915-fix-missing-head Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
1
15
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
Editor for laravel.log...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20915-fix-missing-header-text-relay, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20915-fix-missing-header-text-relay","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"15","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for laravel.log","depth":4,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-3638810784233747753
|
-6006532924280239189
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20915-fix-missing-head Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
1
15
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
Editor for laravel.log...
|
81604
|
NULL
|
NULL
|
NULL
|
|
81604
|
2832
|
17
|
2026-05-28T08:32:10.997533+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957130997_m1.jpg...
|
PhpStorm
|
faVsco.js – TextRelayService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20915-fix-missing-head Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
1
15
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20915-fix-missing-header-text-relay, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20915-fix-missing-header-text-relay","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"15","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"","depth":4,"on_screen":true,"value":"","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-2129860785133473404
|
-4704983834769847381
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20915-fix-missing-head Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
1
15
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
81603
|
2833
|
17
|
2026-05-28T08:31:47.546303+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957107546_m2.jpg...
|
PhpStorm
|
faVsco.js – TextRelayService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20915-fix-missing-head Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
1
15
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20915-fix-missing-header-text-relay, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09740692,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20915-fix-missing-header-text-relay","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8374335,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.37632978,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"15","depth":4,"bounds":{"left":0.3856383,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39694148,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40425533,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"","depth":4,"bounds":{"left":0.43051863,"top":0.09736632,"width":0.32280585,"height":0.40223464},"on_screen":true,"value":"","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-2129860785133473404
|
-4704983834769847381
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20915-fix-missing-head Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
1
15
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
81602
|
2832
|
16
|
2026-05-28T08:31:45.911289+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957105911_m1.jpg...
|
PhpStorm
|
faVsco.js – TextRelayService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20915-fix-missing-head Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
1
15
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Code changed:
Hide
Sync Changes
Hide This Notification...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20915-fix-missing-header-text-relay, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20915-fix-missing-header-text-relay","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"15","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"","depth":4,"on_screen":true,"value":"","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-2628780741070192589
|
-5857843767832835157
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20915-fix-missing-head Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
1
15
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Code changed:
Hide
Sync Changes
Hide This Notification...
|
81600
|
NULL
|
NULL
|
NULL
|
|
81601
|
2833
|
16
|
2026-05-28T08:31:44.488823+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957104488_m2.jpg...
|
PhpStorm
|
faVsco.js – TextRelayService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormFV faVsco.|s ~ProjectvViewNeweNNC$2 JY-209 PhpStormFV faVsco.|s ~ProjectvViewNeweNNC$2 JY-20915-fix-missioCoocKetucioTOOI-Window) Kernel.php© SyneMailbox.php©InternetMessagelnterface.phg© TextRelayServiceTest.phpfyminny.oheE env.production© MailChannelService.phpclass TextReLayServiceo Textrelkysewice.onoavare tuncczon csroncurehcchvarohaehtMeetingGeneratoranoucadorEOHUNPlaybooks—KeCaLA& Dockerfie1 catch (\Exception $e) (Log: :error ((message:)" [TextRelayService) Failed to inspect message', t'nessage_id' => Snessageid,'exception' => Se-›getMessageO).Jotaeo(Sentry::captureException(Se):a Team#UserPilotWebhookC AbstractSemvice cno©ActivityProviderFactory.phpCActviysemce cno©ApiResponseService.phpeeonaraneacarhes ood©InsightSeatService.php© InstantMeetingService.php© IntercomService.php©lpapiClient.phpirávate function natchesExpectedRecipfent (string Srecipient, string SexpoctedAlias, string SexpectedHoDoants - exorodelseif (count(Sparts) la= 2 ll strcasecno(Spartsii]. SexpectedHost) (a= 0)%return false.Changes o luesF .env.local app© JiminnyDebugCommand.php app/Console/Commandsme logging.php config@ PlaybackService.pho app/Services© TextRelayService.php app/Services/Mailoex RehyswMicalieaoho› Unversioned Files 9 filesE laravellog X 4 SF jiminny@localhost)HSJocal ([iminny@localhost)A console (PROD)A console [STAGING)A console (EU]TO0У L7oo tnu comoy tiroteeU TextRelayServiceTest~+0 .fox@TextRelayServiceTest-phprner hensewietResdTextRelayScrvicalest.oho #L154-188eexKeayeMicesthontRead TextRelayServiceTest.php #L150-199Sasrched testistorcurrent Environment.allsBackio ohesder in testerUnSamrices Moiln extRelay Service est.oho.I need to check if there's a duplicate test now. Let me search for all test methods in the file:ResonexRehyscwicelcscono 00-2lD TextRelayServiceTest.phpAsk amahioo+ < Code SWE-1.6+1-10003ditterences+ → , Side-by-side viewerDo notignorey Highlight words71 • ?8828865601Sthiisoassent sailse Sresultpublic function testIsForCurrentEnvironnentIgnoresToHeader(): voidConfig::set('jininny-google_text_host', 'txt.jiminny.com');Snessage = Sthis-›createMock(GnailMessage: :class);Sresult = Snethod-›invoke(Sservice, SgnailService, 'catch-all', 'asg123', 'catch-all', 'txt.jiminny.com'):Sthis->assertFalse(Sresult);nublic functzion testlefondunnentEnwironnentWithEnotvHeadeneOe voi.nethodosserrccesgtolet ruePurtent vereionSthisosassentFalselSresultpublic function testIsForCurrentEnvironmentFallsBackToToHeader(): voidConfig::set("jininny.google text host'*txt.jininny.com');Smessage = Sthis-›createMock(GmailMessage::class):Smethod->setAccessibte(true);Sresult = Snethod->invoke(Sservice, SgnailService, 'catch-all', 'nsg123', 'catch-all', 'txt.jininny.con'):Scnis->asserclruelSresutooublie funatzion testleforCurcentEnvirdoc h0d->eeeeherueTAGEnactArem ditee sooutwnderAmereyenhensno....
|
NULL
|
6710810205627921530
|
NULL
|
visual_change
|
ocr
|
NULL
|
PhpStormFV faVsco.|s ~ProjectvViewNeweNNC$2 JY-209 PhpStormFV faVsco.|s ~ProjectvViewNeweNNC$2 JY-20915-fix-missioCoocKetucioTOOI-Window) Kernel.php© SyneMailbox.php©InternetMessagelnterface.phg© TextRelayServiceTest.phpfyminny.oheE env.production© MailChannelService.phpclass TextReLayServiceo Textrelkysewice.onoavare tuncczon csroncurehcchvarohaehtMeetingGeneratoranoucadorEOHUNPlaybooks—KeCaLA& Dockerfie1 catch (\Exception $e) (Log: :error ((message:)" [TextRelayService) Failed to inspect message', t'nessage_id' => Snessageid,'exception' => Se-›getMessageO).Jotaeo(Sentry::captureException(Se):a Team#UserPilotWebhookC AbstractSemvice cno©ActivityProviderFactory.phpCActviysemce cno©ApiResponseService.phpeeonaraneacarhes ood©InsightSeatService.php© InstantMeetingService.php© IntercomService.php©lpapiClient.phpirávate function natchesExpectedRecipfent (string Srecipient, string SexpoctedAlias, string SexpectedHoDoants - exorodelseif (count(Sparts) la= 2 ll strcasecno(Spartsii]. SexpectedHost) (a= 0)%return false.Changes o luesF .env.local app© JiminnyDebugCommand.php app/Console/Commandsme logging.php config@ PlaybackService.pho app/Services© TextRelayService.php app/Services/Mailoex RehyswMicalieaoho› Unversioned Files 9 filesE laravellog X 4 SF jiminny@localhost)HSJocal ([iminny@localhost)A console (PROD)A console [STAGING)A console (EU]TO0У L7oo tnu comoy tiroteeU TextRelayServiceTest~+0 .fox@TextRelayServiceTest-phprner hensewietResdTextRelayScrvicalest.oho #L154-188eexKeayeMicesthontRead TextRelayServiceTest.php #L150-199Sasrched testistorcurrent Environment.allsBackio ohesder in testerUnSamrices Moiln extRelay Service est.oho.I need to check if there's a duplicate test now. Let me search for all test methods in the file:ResonexRehyscwicelcscono 00-2lD TextRelayServiceTest.phpAsk amahioo+ < Code SWE-1.6+1-10003ditterences+ → , Side-by-side viewerDo notignorey Highlight words71 • ?8828865601Sthiisoassent sailse Sresultpublic function testIsForCurrentEnvironnentIgnoresToHeader(): voidConfig::set('jininny-google_text_host', 'txt.jiminny.com');Snessage = Sthis-›createMock(GnailMessage: :class);Sresult = Snethod-›invoke(Sservice, SgnailService, 'catch-all', 'asg123', 'catch-all', 'txt.jiminny.com'):Sthis->assertFalse(Sresult);nublic functzion testlefondunnentEnwironnentWithEnotvHeadeneOe voi.nethodosserrccesgtolet ruePurtent vereionSthisosassentFalselSresultpublic function testIsForCurrentEnvironmentFallsBackToToHeader(): voidConfig::set("jininny.google text host'*txt.jininny.com');Smessage = Sthis-›createMock(GmailMessage::class):Smethod->setAccessibte(true);Sresult = Snethod->invoke(Sservice, SgnailService, 'catch-all', 'nsg123', 'catch-all', 'txt.jininny.con'):Scnis->asserclruelSresutooublie funatzion testleforCurcentEnvirdoc h0d->eeeeherueTAGEnactArem ditee sooutwnderAmereyenhensno....
|
81599
|
NULL
|
NULL
|
NULL
|
|
81600
|
2832
|
15
|
2026-05-28T08:31:43.909932+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957103909_m1.jpg...
|
PhpStorm
|
faVsco.js – TextRelayService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20915-fix-missing-head Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20915-fix-missing-header-text-relay, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20915-fix-missing-header-text-relay","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-4528724110787610736
|
2604719229101344208
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20915-fix-missing-head Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
iTerm2ShellEditViewSessionScriptsProfilesWindowHelp‹$0A-zshscreenpipe"DOCKERO ₴1DEV (docker)worker-calendar:worker-calendar_00:startedworker-conferences:worker-conferences_00: startedworker-crm-sync:worker-crm-sync_00: startedworker-crm-update:worker-crm-update_00: startedworker-download:worker-download_00:startedworker-emails:worker-emails_00:startedworker-es-update:worker-es-update_00:startedworker-nudges:worker-nudges_00: started₴2-zshО 84-zshWhat's next:Try Docker Debug forLearnseamless, persistentdebugging tools in any container or image → docker debug docker_lamp_1moreat [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-fix-missing-header-text-relay) $csfixdocker exec -it docker_lamp_1•/vendor/bin/php-cs-fixer fix--config=.php-cs-fixer.dist.php-v--using-cache=no --diffPHP CS Fixer 3.95.1 Adalbertus by Fabien Potencier, Dariusz Ruminski and contributors.PHP runtime: 8.5.5Loaded config default from ".php-cs-fixer.dist.php".Running analysis on 7 cores with 10 files per process.5698/5698 C80100%*5ec2-user@ip-10-30-1...O 86100% <78 • Thu 28 May 11:31:43T81ec2-user@ip-10-30-140-...$7Fixed 0 of 5698 files in 52.731 seconds, 799.06 MB memory usedDetected deprecations in use (they will stop working in next major release):- Rule set"@PHP74Migration" is deprecated. Use"@PHP7x4Migration" instead.- Rule set "@PHP80Migration" is deprecated. Use "@PHP8x®Migration" instead.- Rule set "@PHP81Migration" is deprecated. Use "@PHP8x1Migration" instead.- Rule set "@PHP82Migration" is deprecated. Use "@PHP8x2Migration" instead.- Rule set "@PHP83Migration" is deprecated. Use "@PHP8x3Migration"instead.- Rule set "@PHP84Migration" is deprecated. Use "@PHP8x4Migration" instead.What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image + docker debug docker_lamp_1Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-fix-missing-header-text-relay) $D...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
81599
|
2833
|
15
|
2026-05-28T08:31:25.793080+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957085793_m2.jpg...
|
PhpStorm
|
faVsco.js – TextRelayService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20915-fix-missing-head Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
1
15
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20915-fix-missing-header-text-relay, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09740692,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20915-fix-missing-header-text-relay","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8374335,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.37632978,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"15","depth":4,"bounds":{"left":0.3856383,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39694148,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40425533,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"","depth":4,"bounds":{"left":0.43051863,"top":0.09736632,"width":0.32280585,"height":0.40223464},"on_screen":true,"value":"","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-2129860785133473404
|
-4704983834769847381
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20915-fix-missing-head Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
1
15
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
81597
|
2832
|
14
|
2026-05-28T08:31:24.150359+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957084150_m1.jpg...
|
PhpStorm
|
faVsco.js – TextRelayService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20915-fix-missing-head Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
1
15
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Code changed:
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20915-fix-missing-header-text-relay, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20915-fix-missing-header-text-relay","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"15","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-8171751502949769767
|
-5862347367460205653
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20915-fix-missing-head Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Code changed:
Hide
Sync Changes
Hide This Notification
1
15
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Code changed:
Hide...
|
81596
|
NULL
|
NULL
|
NULL
|
|
81598
|
2833
|
14
|
2026-05-28T08:31:22.667957+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957082667_m2.jpg...
|
PhpStorm
|
faVsco.js – TextRelayServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20915-fix-missing-head Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20915-fix-missing-header-text-relay, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09740692,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20915-fix-missing-header-text-relay","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8374335,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-147301692555525249
|
-8776298399911410750
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20915-fix-missing-head Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
rapstomViewCoocKetucioTOOI-WindowFV faVsco.s ~$ JY-20915-fix-missing-header-text-relaproidet© SyncMailbox.phpDockerfile= laravel.log X SF (iminny@localhost)© AutomatedReportsSenAutomstedReporsseh© AutomatedReportsSenСАнтотаксонсрогсосл© AutomatedReportsSenСАutоmatсонсoогоосn© RecipientServiceTest.pv (D Mail>DActions› Office©) TextRelayServiceTest.php x php fiminny.phpA console (STAGING]cionedectare(Strzcrcypes=.)"nanesoace vests Unzc services naze"usc 6000Le Servzce 6302l a$ 16000Lc60322usc 6000Lc Service 6n022 nessaoc as 6ma2chessagc:usc 6000Le Service 6na2, hessagcPanaJ- KPSOVPuse 6000le Service 6na2 nessagePar Header:> Da ValidatorsC BatchServiceTest.php© EmailActivityServiceTestuse zuminare Suppor Facades conta.use uminate Suppors Facades, Loa:use miinny Serusces.nu tey cel aysenuicos©InboxserviceTest.phpc) Textkelayservicetest ohoEn MeetinoGeneratoruse PHPUn1t\Franework\Attributes (coversclassuse Prpnst Eranemorkrhures hara?rousderuse ests iestlase,NotrcationRecallAlaTeamuse Pe ections assuse Hockery:icovenchlassGextRelavServsca..class.L TelephonyelUsenclace TeytRelavSenusceTest eytends Testhas.L UserPilotLosal chancesChanges o luesE.env.local apc© JiminnyDebugCommand.php app/Console/Commands+ → , Side-by-side viewer@828865ed app/Services/Mail/TextRelayService.phgDo not ignore"Highlight words -x15€wplogging.php coniig@ PlaybackService.pho app/ServicestrySnessage = Sservice-›users nessages-›get(Smailbox, Snessageld):Sheadens a Sressane->oetPawloadlo->netHeadensor©) Tex Relayservice oho aoo sericesvas@ TextRelavServiceTest.nho toate/Unit/Serviccs/Mai> Unversioned Filos 9 ficsforeach (Sheaders as Sheader)Chasdon nend eertyacanhnsasnalrtaSmatches = Sthis->natchesExpectedRecipient(Sheader-›value, SexpectedALias, SexpectedHost):if Smatches)Sanitize PII by renoving plus-tag content for loggingSsanitizedOriginalTo = explode("+'Sheader->value) [в]:Log: : info("[TextRelayService) Refused message'.nessage1o = saessadelo'original to sanitized' => SsanitizedOriginalTdneunnrchesLogksnacnina(* (TextRelavSerwicel Refused messaae: missing X-6--0nioinal-Teconsole [PROD)# consolefFul7o0sLXoo inu co moy throteTextRelayServiceTestrner hensewiet+0 .tix @TextRelayServiceTest.phpTnodgnttor 1sResdTextRelayScrvicalest.oho #L154-188Thought for 1seexKeayeMicesthont+2-2Read TextRelayServiceTest.php #L150-199Sasrched testistor@urrent Environment.allsBackio ohesder in teste/UntSamices Moiln extRelay Semvice est.oho.toaad to chack i tharale n dunicata thet now llat ma dascch for ail thet mathode in tha GiAResonexRehyscwicelcscono 00-2lTextRelayServiceTest.phpAsk ammnina+ < Code SWE-1.6SotterencePurtent vereionSto& Sheader->valuesSrecipient = Soriginalto ?? Stoif (Srecipient (a= null)CrntthosCthie-antchoeSynontoroomnhontGronnont Coynontodalne Coynontodhnet1f (l Snatches)Sanitize PII by removing plus-tag content for loggingSsanitizeRecipient = explode(*+*. Srecipient) [81)Log: :info('[TextRelayService) Refused message'.nessaceo a> ssessageloAntatnol tA conteianat es Goontetonaonaladondneturn Snatches:loot warnnolmrxre meruncolleorusce nosshore mssind xconeorsonmii elto noto neadersTAGEnactArem ditee sooRekuntre%Aensao....
|
81595
|
NULL
|
NULL
|
NULL
|
|
81596
|
2832
|
13
|
2026-05-28T08:31:21.787315+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957081787_m1.jpg...
|
PhpStorm
|
faVsco.js – TextRelayServiceTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp‹$0A-zshscreenpipe"DOCKERO ₴1DEV (docker)worker-calendar:worker-calendar_00:startedworker-conferences:worker-conferences_00: startedworker-crm-sync:worker-crm-sync_00: startedworker-crm-update:worker-crm-update_00: startedworker-download:worker-download_00:startedworker-emails:worker-emails_00:startedworker-es-update:worker-es-update_00:startedworker-nudges:worker-nudges_00: started₴2-zsh- 84-zshWhat's next:Try Docker Debug forLearnseamless, persistentdebugging tools in any container or image → docker debug docker_lamp_1moreat [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-fix-missing-header-text-relay) $csfixdocker exec -it docker_lamp_1•/vendor/bin/php-cs-fixer fix--config=.php-cs-fixer.dist.php-v--using-cache=no --diffPHP CS Fixer 3.95.1 Adalbertus by Fabien Potencier, Dariusz Ruminski and contributors.PHP runtime: 8.5.5Loaded config default from ".php-cs-fixer.dist.php".Running analysis on 7 cores with 10 files per process.5698/5698 C80100%*5ec2-user@ip-10-30-1...O 86100% <78• Thu 28 May 11:31:21181ec2-user@ip-10-30-140-...$7Fixed 0 of 5698 files in 52.731 seconds, 799.06 MB memory usedDetected deprecations in use (they will stop working in next major release):- Rule set"@PHP74Migration" is deprecated. Use"@PHP7x4Migration" instead.- Rule set "@PHP80Migration" is deprecated. Use "@PHP8x®Migration" instead.- Rule set "@PHP81Migration" is deprecated. Use "@PHP8x1Migration" instead.- Rule set "@PHP82Migration" is deprecated. Use "@PHP8x2Migration" instead.- Rule set "@PHP83Migration" is deprecated. Use "@PHP8x3Migration"instead.- Rule set "@PHP84Migration" is deprecated. Use "@PHP8x4Migration" instead.What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image + docker debug docker_lamp_1Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-fix-missing-header-text-relay) $D...
|
NULL
|
3830539129301962242
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp‹$0A-zshscreenpipe"DOCKERO ₴1DEV (docker)worker-calendar:worker-calendar_00:startedworker-conferences:worker-conferences_00: startedworker-crm-sync:worker-crm-sync_00: startedworker-crm-update:worker-crm-update_00: startedworker-download:worker-download_00:startedworker-emails:worker-emails_00:startedworker-es-update:worker-es-update_00:startedworker-nudges:worker-nudges_00: started₴2-zsh- 84-zshWhat's next:Try Docker Debug forLearnseamless, persistentdebugging tools in any container or image → docker debug docker_lamp_1moreat [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-fix-missing-header-text-relay) $csfixdocker exec -it docker_lamp_1•/vendor/bin/php-cs-fixer fix--config=.php-cs-fixer.dist.php-v--using-cache=no --diffPHP CS Fixer 3.95.1 Adalbertus by Fabien Potencier, Dariusz Ruminski and contributors.PHP runtime: 8.5.5Loaded config default from ".php-cs-fixer.dist.php".Running analysis on 7 cores with 10 files per process.5698/5698 C80100%*5ec2-user@ip-10-30-1...O 86100% <78• Thu 28 May 11:31:21181ec2-user@ip-10-30-140-...$7Fixed 0 of 5698 files in 52.731 seconds, 799.06 MB memory usedDetected deprecations in use (they will stop working in next major release):- Rule set"@PHP74Migration" is deprecated. Use"@PHP7x4Migration" instead.- Rule set "@PHP80Migration" is deprecated. Use "@PHP8x®Migration" instead.- Rule set "@PHP81Migration" is deprecated. Use "@PHP8x1Migration" instead.- Rule set "@PHP82Migration" is deprecated. Use "@PHP8x2Migration" instead.- Rule set "@PHP83Migration" is deprecated. Use "@PHP8x3Migration"instead.- Rule set "@PHP84Migration" is deprecated. Use "@PHP8x4Migration" instead.What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image + docker debug docker_lamp_1Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-fix-missing-header-text-relay) $D...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
81595
|
2833
|
13
|
2026-05-28T08:31:19.695597+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957079695_m2.jpg...
|
PhpStorm
|
faVsco.js – TextRelayServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20915-fix-missing-head Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
13
64
Previous Highlighted Error
Next Highlighted Error...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20915-fix-missing-header-text-relay, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09740692,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20915-fix-missing-header-text-relay","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8374335,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"13","depth":4,"bounds":{"left":0.37333778,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"64","depth":4,"bounds":{"left":0.3849734,"top":0.10055866,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39694148,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40425533,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-8014399306852202777
|
-6398116391010759742
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20915-fix-missing-head Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
13
64
Previous Highlighted Error
Next Highlighted Error
rapstomViewNeweNNCCoocKetucioTOOI-WindowFV faVsco.s$ JY-20915-fix-missing-header-text-relaproidet© SyncMailbox.php© ServiceTest.php© SyncBatchRedisServiceRaseSercetest oho© CachedCrmServiceDecora©) TextRelayServiceTest.php x php fiminny.php3.env.productioncionedectare(Strzcrcypes=.)"© CrmActivityServiceTest.ptc CrmConfigurationSettingsnanesoace vests Unzc services naze"CrmObiectsResolverTest.gDefaultProspectSearchStusc 6000Le Servzcc 6n02l a$ 6000Lc609223EmailHelperTest.phousc 6000Lc Service 6n022 nessaoc as 6ma2chessagc:© FieldValueConverterTest.gusc 6000Le Service 6na2, hessagcPana© LayoutManagerTest.phduse 6000le Service 6na2 nessagePar Header:C MigrateProviderServiceTeuse zuminare Suppor Facades conta.© OpportunityActivityMatchuseuminate Suppors Facades, Loa?iOpportunitySyncStrateay© ProspectCacheTest.phouse riinny Serusces.uneutey cel auseruicosuse PHPUn1t\Franework\Attributes (coversclass;c Prosoccsearchstratcoyt© ProviderRecistryTest.phouse Prpnst Eranemorkrhures hara?rousderuse ests iestlase,c Recordseleclortestonouse Pe ections ass) ResoivecomoanvNamesvluse HockenyLochlehnotchandes 5iasenullocallaoc@.liminnv?ebuCommand.cho.ano/Console/Commandsphe logging.php config© PlaybackService.php app/Services© TextRelayService.php app/Services/MaiC TextRelayServiceTest.php tests/Unit/Services/Mai• Uoversionad Filas 9 GlaelWockhrhta= laravel.log X SF (iminny@localhost)A console (STAGING]HS Jocal (jiminny(blocalhost)console [PROD# consolefFul7o0sLXoo inu comoy tirotriTextRelayServiceTestrner hensewiet+0.tix @TextRelayServiceTest.phpTnodgnttor 1sResdTextRelayScrvicalest.oho #L154-188Thought for 1seexKeayeMicesthont+2-2ThoughtsRead TextRelayServiceTest.php #L150-199Sasrched testistorcurrent Environment.allsBackio ohesder in testerUnSamrices Moiln extRelay Service est.oho.toaad to chack i thardle a dunicato thet now llat ma cascch for ail thet mathode in tha 6laAsk annineeo+ @ Code SWE-16T0 +→ 0 Side-by-side viewerö 828865ed app/Services/Mail/TextRelayService.phDo not ignoreyHoblioht woteyXB0?try 4Snessage = Sservice->users nessages->get(Smailbox, Snessageld):Sheaders = Ssessage->getPayLoad@)->getHeaders@>forcach (Sheaders as Sheader)$f (Sheaden->nane aa= 1X-Gn-Opioinal-To') {S-atches = Sthis->natchesExoectedRecinient(Sheaden->vallue.ScxoectedAlins. ScxocctedHost)ie (1 Shatches)Santteze Pethy cenouino nluscrad content onlocointJOriginalTo = explode(*+', Sheader-›value) [0];Loa:-intolextRelavServscel Retused nessage"message_id' => Smessageld,'orsosnal to sanitazedl Ssans ta zeddcs osinaittolreturn Smatches:Log::warning* TextRelayService) Refused message: missing X-6m-Oniginal-To b"message id' => Snessageld.catch Exception $e)Log::error(" "TextRelayService) Failed to inspect message'. !"acssaoc-10 => Snessagc.owaitaroneseSto = Sheader->value›Sreczozent = Sorzoznallo kestonerochohent lEs nutaSnatches = Sthis-snatcheeSxoectedRecinfent/Sreciniont. ScxocctedALins. ScxoectedHost)i ( Snatches) 1Santtaze Plesihy remouine alusorae content on tloggineSsanitizecipient = explode(*, srecipient)10):oa.sinfol TextRolavSeryice Refusedl messane"Scani ta z0/RecsinslentD):return Snatches:Log::warning*TextRelayService Refused nessage: missing X-Gn-Oniginal-To and ToIRNOSGAS KAT EN SENSCAAATAcatchException Se)Log::error**(TextRelayService) Fafled to inspect sessage'."message id' => Smessageld"exception' => Se->getMessageo)TAGEnactArem ditee sooXtwodtur TasmeoxlThiesa/ensao...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
81594
|
2832
|
12
|
2026-05-28T08:31:19.577558+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957079577_m1.jpg...
|
PhpStorm
|
faVsco.js – TextRelayServiceTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20915-fix-missing-head Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20915-fix-missing-header-text-relay, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20915-fix-missing-header-text-relay","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-4060176888399009113
|
-9212323791225028208
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20915-fix-missing-head Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
iTerm2ShellEditViewSessionScriptsProfilesWindowHelp‹$0A-zshscreenpipe"DOCKER₴81DEV (docker)worker-calendar:worker-calendar_00:startedworker-conferences:worker-conferences_00: startedworker-crm-sync:worker-crm-sync_00: startedworker-crm-update:worker-crm-update_00: startedworker-download:worker-download_00:startedworker-emails:worker-emails_00:startedworker-es-update:worker-es-update_00:startedworker-nudges:worker-nudges_00: started₴2-zsh84-zshWhat's next:Try Docker Debug forseamless, persistentdebugging tools in any container or image → docker debug docker_lamp_1Learnmoreat [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-fix-missing-header-text-relay) $csfixdocker exec -it docker_lamp_1•/vendor/bin/php-cs-fixer fix--config=.php-cs-fixer.dist.php-v--using-cache=no --diffPHP CS Fixer 3.95.1 Adalbertus by Fabien Potencier, Dariusz Ruminski and contributors.PHP runtime: 8.5.5Loaded config default from ".php-cs-fixer.dist.php".Running analysis on 7 cores with 10 files per process.5698/5698 C80100%*5ec2-user@ip-10-30-1...O 86100% <78• Thu 28 May 11:31:19T81ec2-user@ip-10-30-140-...$7Fixed 0 of 5698 files in 52.731 seconds, 799.06 MB memory usedDetected deprecations in use (they will stop working in next major release):- Rule set"@PHP74Migration" is deprecated. Use"@PHP7x4Migration" instead.- Rule set "@PHP80Migration" is deprecated. Use "@PHP8x®Migration" instead.- Rule set "@PHP81Migration" is deprecated. Use "@PHP8x1Migration" instead.- Rule set "@PHP82Migration" is deprecated. Use "@PHP8x2Migration" instead.- Rule set "@PHP83Migration" is deprecated. Use "@PHP8x3Migration"instead.- Rule set "@PHP84Migration" is deprecated. Use "@PHP8x4Migration" instead.What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image + docker debug docker_lamp_1Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-fix-missing-header-text-relay) $D...
|
81593
|
NULL
|
NULL
|
NULL
|
|
81593
|
2832
|
11
|
2026-05-28T08:31:10.193380+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957070193_m1.jpg...
|
PhpStorm
|
faVsco.js – TextRelayServiceTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20915-fix-missing-head Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
13
64
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Mail;
use Google\Service\Gmail as GoogleGmail;
use Google\Service\Gmail\Message as GmailMessage;
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\Mail\TextRelayService;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
use ReflectionClass;
use Mockery;
#[CoversClass(TextRelayService::class)]
class TextRelayServiceTest extends TestCase
{
public static function environmentProvider(): array
{
return [
'eu_environment' => ['eu', 'catch-all-eu', '[EMAIL]'],
'us_environment' => ['us', 'catch-all', '[EMAIL]'],
'other_environment_defaults_to_us' => ['other', 'catch-all', '[EMAIL]'],
'eu_environment_with_plus_tag' => ['eu', 'catch-all-eu', '[EMAIL]'],
'us_environment_with_plus_tag' => ['us', 'catch-all', '[EMAIL]'],
];
}
protected function setUp(): void
{
parent::setUp();
}
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
#[DataProvider('environmentProvider')]
public function testIsForCurrentEnvironmentWithMatchingXGmOriginalToHeader(
string $deployRegion,
string $expectedAlias,
string $recipient
): void {
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = $recipient;
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');
$this->assertTrue($result);
}
#[DataProvider('environmentProvider')]
public function testIsForCurrentEnvironmentWithNonMatchingXGmOriginalToHeader(
string $deployRegion,
string $expectedAlias,
string $recipient
): void {
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentLogsRefusedMessage(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Refused message', [
'message_id' => 'msg123',
'original_to_sanitized' => '[EMAIL]',
]);
Log::shouldReceive('error')
->never()
->with('[TextRelayService] Failed to inspect message', \Mockery::type('array'));
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentFallsBackToToHeader(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertTrue($result);
}
public function testIsForCurrentEnvironmentWithEmptyHeaders(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$payload->method('getHeaders')->willReturn([]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
Log::shouldReceive('warning')
->once()
->with('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => 'msg123',
]);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentWithException(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')
->willThrowException(new \Exception('Gmail API error'));
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
Log::shouldReceive('error')
->once()
->with('[TextRelayService] Failed to inspect message', [
'message_id' => 'msg123',
'exception' => 'Gmail API error',
]);
\Sentry::shouldReceive('captureException')
->once()
->with(\Mockery::type(\Exception::class));
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentRejectsWrongHostWithMatchingPlusTag(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testSyncUsesEuAliasForEuRegion(): void
{
Config::set('jiminny.deploy_region', 'eu');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
$service = $this->createTextRelayService();
$gmailService = $this->createMock(GoogleGmail::class);
$historyResponse = $this->createMock(\Google\Service\Gmail\ListHistoryResponse::class);
$historyResponse->historyId = 12345;
$historyResponse->method('getHistory')->willReturn([]);
$historyResponse->method('getNextPageToken')->willReturn(null);
$usersHistory = $this->createMock(\Google\Service\Gmail\Resource\UsersHistory::class);
$usersHistory->method('listUsersHistory')->willReturn($historyResponse);
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
\Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
}
public function testSyncUsesUsAliasForUsRegion(): void
{
Config::set('jiminny.deploy_region', 'us');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
$service = $this->createTextRelayService();
$gmailService = $this->createMock(GoogleGmail::class);
$historyResponse = $this->createMock(\Google\Service\Gmail\ListHistoryResponse::class);
$historyResponse->historyId = 12345;
$historyResponse->method('getHistory')->willReturn([]);
$historyResponse->method('getNextPageToken')->willReturn(null);
$usersHistory = $this->createMock(\Google\Service\Gmail\Resource\UsersHistory::class);
$usersHistory->method('listUsersHistory')->willReturn($historyResponse);
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
\Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
}
public function testSyncWithEnhancedLogging(): void
{
Config::set('jiminny.deploy_region', 'eu');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
// Create a partial mock of the service to control the getHistory method
$service = $this->createPartialMock(TextRelayService::class, ['getHistory']);
// Mock getHistory to return empty array (no messages to process)
$service->method('getHistory')->willReturn([]);
// Mock Log facade - channel() returns a driver mock that accepts any method call
$logDriver = \Mockery::mock();
$logDriver->shouldIgnoreMissing();
Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Starting sync', [
'mailbox' => '[EMAIL]',
'expected_alias' => 'catch-all-eu',
'expected_host' => 'txt.jiminny.com',
]);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Sync completed', [
'mailbox' => '[EMAIL]',
'messages_processed' => 0,
'message_ids' => [],
]);
// Mock Sentry to prevent actual error reporting
\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testSyncWithRefusedMessageLogs(): void
{
Config::set('jiminny.deploy_region', 'eu');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
// Create a partial mock of the service to control the getHistory method
$service = $this->createPartialMock(TextRelayService::class, ['getHistory']);
// Mock getHistory to return empty array (no messages to process)
$service->method('getHistory')->willReturn([]);
// Mock Log facade - channel() returns a driver mock that accepts any method call
$logDriver = \Mockery::mock();
$logDriver->shouldIgnoreMissing();
Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Starting sync', [
'mailbox' => '[EMAIL]',
'expected_alias' => 'catch-all-eu',
'expected_host' => 'txt.jiminny.com',
]);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Sync completed', [
'mailbox' => '[EMAIL]',
'messages_processed' => 0,
'message_ids' => [],
]);
// Mock Sentry to prevent actual error reporting
\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetHistoryLogsErrorOnException(): void
{
Config::set('jiminny.google_text_relay_topic', 'test-topic');
Config::set('jiminny.google_text_user', '[EMAIL]');
$service = $this->createTextRelayService();
$gmailService = $this->createMock(GoogleGmail::class);
$usersHistory = $this->createMock(\Google\Service\Gmail\Resource\UsersHistory::class);
$usersHistory->method('listUsersHistory')
->willThrowException(new \Exception('Gmail API error'));
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
$logDriver = \Mockery::mock();
$logDriver->shouldIgnoreMissing();
Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);
Log::shouldReceive('error')
->once()
->with('[TextRelayService] Failed to fetch Gmail history', [
'exception' => 'Gmail API error',
]);
\Sentry::shouldReceive('captureException')
->once()
->with(\Mockery::type(\Exception::class));
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('getHistory');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
private function createTextRelayService(): TextRelayService
{
$service = new class () extends TextRelayService {
public function __construct()
{
}
};
return $service;
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20915-fix-missing-header-text-relay, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20915-fix-missing-header-text-relay","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"13","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"64","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Mail;\n\nuse Google\\Service\\Gmail as GoogleGmail;\nuse Google\\Service\\Gmail\\Message as GmailMessage;\nuse Google\\Service\\Gmail\\MessagePart;\nuse Google\\Service\\Gmail\\MessagePartHeader;\nuse Illuminate\\Support\\Facades\\Config;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\Mail\\TextRelayService;\nuse PHPUnit\\Framework\\Attributes\\CoversClass;\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse Tests\\TestCase;\nuse ReflectionClass;\nuse Mockery;\n\n#[CoversClass(TextRelayService::class)]\nclass TextRelayServiceTest extends TestCase\n{\n public static function environmentProvider(): array\n {\n return [\n 'eu_environment' => ['eu', 'catch-all-eu', 'catch-all-eu@txt.jiminny.com'],\n 'us_environment' => ['us', 'catch-all', 'catch-all@txt.jiminny.com'],\n 'other_environment_defaults_to_us' => ['other', 'catch-all', 'catch-all@txt.jiminny.com'],\n 'eu_environment_with_plus_tag' => ['eu', 'catch-all-eu', 'catch-all-eu+447700152706.447893937879.3ndZZBqjRo@txt.jiminny.com'],\n 'us_environment_with_plus_tag' => ['us', 'catch-all', 'catch-all+447700152706.447893937879.3ndZZBqjRo@txt.jiminny.com'],\n ];\n }\n\n protected function setUp(): void\n {\n parent::setUp();\n }\n\n protected function tearDown(): void\n {\n Mockery::close();\n parent::tearDown();\n }\n\n #[DataProvider('environmentProvider')]\n public function testIsForCurrentEnvironmentWithMatchingXGmOriginalToHeader(\n string $deployRegion,\n string $expectedAlias,\n string $recipient\n ): void {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = $recipient;\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');\n\n $this->assertTrue($result);\n }\n\n #[DataProvider('environmentProvider')]\n public function testIsForCurrentEnvironmentWithNonMatchingXGmOriginalToHeader(\n string $deployRegion,\n string $expectedAlias,\n string $recipient\n ): void {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = 'other@txt.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentLogsRefusedMessage(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = 'wrong@txt.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Refused message', [\n 'message_id' => 'msg123',\n 'original_to_sanitized' => 'wrong@txt.jiminny.com',\n ]);\n\n Log::shouldReceive('error')\n ->never()\n ->with('[TextRelayService] Failed to inspect message', \\Mockery::type('array'));\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentFallsBackToToHeader(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'To';\n $header->value = 'catch-all@txt.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertTrue($result);\n }\n\n public function testIsForCurrentEnvironmentWithEmptyHeaders(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n\n $payload->method('getHeaders')->willReturn([]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n Log::shouldReceive('warning')\n ->once()\n ->with('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => 'msg123',\n ]);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentWithException(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')\n ->willThrowException(new \\Exception('Gmail API error'));\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n Log::shouldReceive('error')\n ->once()\n ->with('[TextRelayService] Failed to inspect message', [\n 'message_id' => 'msg123',\n 'exception' => 'Gmail API error',\n ]);\n\n \\Sentry::shouldReceive('captureException')\n ->once()\n ->with(\\Mockery::type(\\Exception::class));\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentRejectsWrongHostWithMatchingPlusTag(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = 'catch-all+447700152706.447893937879.3ndZZBqjRo@txt.staging.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testSyncUsesEuAliasForEuRegion(): void\n {\n Config::set('jiminny.deploy_region', 'eu');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n $service = $this->createTextRelayService();\n $gmailService = $this->createMock(GoogleGmail::class);\n\n $historyResponse = $this->createMock(\\Google\\Service\\Gmail\\ListHistoryResponse::class);\n $historyResponse->historyId = 12345;\n $historyResponse->method('getHistory')->willReturn([]);\n $historyResponse->method('getNextPageToken')->willReturn(null);\n\n $usersHistory = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersHistory::class);\n $usersHistory->method('listUsersHistory')->willReturn($historyResponse);\n $gmailService->users_history = $usersHistory;\n\n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n }\n\n public function testSyncUsesUsAliasForUsRegion(): void\n {\n Config::set('jiminny.deploy_region', 'us');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n $service = $this->createTextRelayService();\n $gmailService = $this->createMock(GoogleGmail::class);\n\n $historyResponse = $this->createMock(\\Google\\Service\\Gmail\\ListHistoryResponse::class);\n $historyResponse->historyId = 12345;\n $historyResponse->method('getHistory')->willReturn([]);\n $historyResponse->method('getNextPageToken')->willReturn(null);\n\n $usersHistory = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersHistory::class);\n $usersHistory->method('listUsersHistory')->willReturn($historyResponse);\n $gmailService->users_history = $usersHistory;\n\n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n }\n\n public function testSyncWithEnhancedLogging(): void\n {\n Config::set('jiminny.deploy_region', 'eu');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n\n // Mock Log facade - channel() returns a driver mock that accepts any method call\n $logDriver = \\Mockery::mock();\n $logDriver->shouldIgnoreMissing();\n Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Starting sync', [\n 'mailbox' => 'test@example.com',\n 'expected_alias' => 'catch-all-eu',\n 'expected_host' => 'txt.jiminny.com',\n ]);\n\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Sync completed', [\n 'mailbox' => 'test@example.com',\n 'messages_processed' => 0,\n 'message_ids' => [],\n ]);\n\n // Mock Sentry to prevent actual error reporting\n \\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testSyncWithRefusedMessageLogs(): void\n {\n Config::set('jiminny.deploy_region', 'eu');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n\n // Mock Log facade - channel() returns a driver mock that accepts any method call\n $logDriver = \\Mockery::mock();\n $logDriver->shouldIgnoreMissing();\n Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Starting sync', [\n 'mailbox' => 'test@example.com',\n 'expected_alias' => 'catch-all-eu',\n 'expected_host' => 'txt.jiminny.com',\n ]);\n\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Sync completed', [\n 'mailbox' => 'test@example.com',\n 'messages_processed' => 0,\n 'message_ids' => [],\n ]);\n\n // Mock Sentry to prevent actual error reporting\n \\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetHistoryLogsErrorOnException(): void\n {\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n Config::set('jiminny.google_text_user', 'test@example.com');\n\n $service = $this->createTextRelayService();\n $gmailService = $this->createMock(GoogleGmail::class);\n\n $usersHistory = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersHistory::class);\n $usersHistory->method('listUsersHistory')\n ->willThrowException(new \\Exception('Gmail API error'));\n $gmailService->users_history = $usersHistory;\n\n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n\n $logDriver = \\Mockery::mock();\n $logDriver->shouldIgnoreMissing();\n Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);\n Log::shouldReceive('error')\n ->once()\n ->with('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => 'Gmail API error',\n ]);\n\n \\Sentry::shouldReceive('captureException')\n ->once()\n ->with(\\Mockery::type(\\Exception::class));\n\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('getHistory');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n private function createTextRelayService(): TextRelayService\n {\n $service = new class () extends TextRelayService {\n public function __construct()\n {\n }\n };\n\n return $service;\n }\n\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Mail;\n\nuse Google\\Service\\Gmail as GoogleGmail;\nuse Google\\Service\\Gmail\\Message as GmailMessage;\nuse Google\\Service\\Gmail\\MessagePart;\nuse Google\\Service\\Gmail\\MessagePartHeader;\nuse Illuminate\\Support\\Facades\\Config;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\Mail\\TextRelayService;\nuse PHPUnit\\Framework\\Attributes\\CoversClass;\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse Tests\\TestCase;\nuse ReflectionClass;\nuse Mockery;\n\n#[CoversClass(TextRelayService::class)]\nclass TextRelayServiceTest extends TestCase\n{\n public static function environmentProvider(): array\n {\n return [\n 'eu_environment' => ['eu', 'catch-all-eu', 'catch-all-eu@txt.jiminny.com'],\n 'us_environment' => ['us', 'catch-all', 'catch-all@txt.jiminny.com'],\n 'other_environment_defaults_to_us' => ['other', 'catch-all', 'catch-all@txt.jiminny.com'],\n 'eu_environment_with_plus_tag' => ['eu', 'catch-all-eu', 'catch-all-eu+447700152706.447893937879.3ndZZBqjRo@txt.jiminny.com'],\n 'us_environment_with_plus_tag' => ['us', 'catch-all', 'catch-all+447700152706.447893937879.3ndZZBqjRo@txt.jiminny.com'],\n ];\n }\n\n protected function setUp(): void\n {\n parent::setUp();\n }\n\n protected function tearDown(): void\n {\n Mockery::close();\n parent::tearDown();\n }\n\n #[DataProvider('environmentProvider')]\n public function testIsForCurrentEnvironmentWithMatchingXGmOriginalToHeader(\n string $deployRegion,\n string $expectedAlias,\n string $recipient\n ): void {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = $recipient;\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');\n\n $this->assertTrue($result);\n }\n\n #[DataProvider('environmentProvider')]\n public function testIsForCurrentEnvironmentWithNonMatchingXGmOriginalToHeader(\n string $deployRegion,\n string $expectedAlias,\n string $recipient\n ): void {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = 'other@txt.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentLogsRefusedMessage(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = 'wrong@txt.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Refused message', [\n 'message_id' => 'msg123',\n 'original_to_sanitized' => 'wrong@txt.jiminny.com',\n ]);\n\n Log::shouldReceive('error')\n ->never()\n ->with('[TextRelayService] Failed to inspect message', \\Mockery::type('array'));\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentFallsBackToToHeader(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'To';\n $header->value = 'catch-all@txt.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertTrue($result);\n }\n\n public function testIsForCurrentEnvironmentWithEmptyHeaders(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n\n $payload->method('getHeaders')->willReturn([]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n Log::shouldReceive('warning')\n ->once()\n ->with('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => 'msg123',\n ]);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentWithException(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')\n ->willThrowException(new \\Exception('Gmail API error'));\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n Log::shouldReceive('error')\n ->once()\n ->with('[TextRelayService] Failed to inspect message', [\n 'message_id' => 'msg123',\n 'exception' => 'Gmail API error',\n ]);\n\n \\Sentry::shouldReceive('captureException')\n ->once()\n ->with(\\Mockery::type(\\Exception::class));\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentRejectsWrongHostWithMatchingPlusTag(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = 'catch-all+447700152706.447893937879.3ndZZBqjRo@txt.staging.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testSyncUsesEuAliasForEuRegion(): void\n {\n Config::set('jiminny.deploy_region', 'eu');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n $service = $this->createTextRelayService();\n $gmailService = $this->createMock(GoogleGmail::class);\n\n $historyResponse = $this->createMock(\\Google\\Service\\Gmail\\ListHistoryResponse::class);\n $historyResponse->historyId = 12345;\n $historyResponse->method('getHistory')->willReturn([]);\n $historyResponse->method('getNextPageToken')->willReturn(null);\n\n $usersHistory = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersHistory::class);\n $usersHistory->method('listUsersHistory')->willReturn($historyResponse);\n $gmailService->users_history = $usersHistory;\n\n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n }\n\n public function testSyncUsesUsAliasForUsRegion(): void\n {\n Config::set('jiminny.deploy_region', 'us');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n $service = $this->createTextRelayService();\n $gmailService = $this->createMock(GoogleGmail::class);\n\n $historyResponse = $this->createMock(\\Google\\Service\\Gmail\\ListHistoryResponse::class);\n $historyResponse->historyId = 12345;\n $historyResponse->method('getHistory')->willReturn([]);\n $historyResponse->method('getNextPageToken')->willReturn(null);\n\n $usersHistory = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersHistory::class);\n $usersHistory->method('listUsersHistory')->willReturn($historyResponse);\n $gmailService->users_history = $usersHistory;\n\n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n }\n\n public function testSyncWithEnhancedLogging(): void\n {\n Config::set('jiminny.deploy_region', 'eu');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n\n // Mock Log facade - channel() returns a driver mock that accepts any method call\n $logDriver = \\Mockery::mock();\n $logDriver->shouldIgnoreMissing();\n Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Starting sync', [\n 'mailbox' => 'test@example.com',\n 'expected_alias' => 'catch-all-eu',\n 'expected_host' => 'txt.jiminny.com',\n ]);\n\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Sync completed', [\n 'mailbox' => 'test@example.com',\n 'messages_processed' => 0,\n 'message_ids' => [],\n ]);\n\n // Mock Sentry to prevent actual error reporting\n \\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testSyncWithRefusedMessageLogs(): void\n {\n Config::set('jiminny.deploy_region', 'eu');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n\n // Mock Log facade - channel() returns a driver mock that accepts any method call\n $logDriver = \\Mockery::mock();\n $logDriver->shouldIgnoreMissing();\n Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Starting sync', [\n 'mailbox' => 'test@example.com',\n 'expected_alias' => 'catch-all-eu',\n 'expected_host' => 'txt.jiminny.com',\n ]);\n\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Sync completed', [\n 'mailbox' => 'test@example.com',\n 'messages_processed' => 0,\n 'message_ids' => [],\n ]);\n\n // Mock Sentry to prevent actual error reporting\n \\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetHistoryLogsErrorOnException(): void\n {\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n Config::set('jiminny.google_text_user', 'test@example.com');\n\n $service = $this->createTextRelayService();\n $gmailService = $this->createMock(GoogleGmail::class);\n\n $usersHistory = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersHistory::class);\n $usersHistory->method('listUsersHistory')\n ->willThrowException(new \\Exception('Gmail API error'));\n $gmailService->users_history = $usersHistory;\n\n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n\n $logDriver = \\Mockery::mock();\n $logDriver->shouldIgnoreMissing();\n Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);\n Log::shouldReceive('error')\n ->once()\n ->with('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => 'Gmail API error',\n ]);\n\n \\Sentry::shouldReceive('captureException')\n ->once()\n ->with(\\Mockery::type(\\Exception::class));\n\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('getHistory');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n private function createTextRelayService(): TextRelayService\n {\n $service = new class () extends TextRelayService {\n public function __construct()\n {\n }\n };\n\n return $service;\n }\n\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"","depth":4,"on_screen":true,"value":"","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3656098470495859466
|
-2005170756397648973
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20915-fix-missing-head Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
13
64
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Mail;
use Google\Service\Gmail as GoogleGmail;
use Google\Service\Gmail\Message as GmailMessage;
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\Mail\TextRelayService;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
use ReflectionClass;
use Mockery;
#[CoversClass(TextRelayService::class)]
class TextRelayServiceTest extends TestCase
{
public static function environmentProvider(): array
{
return [
'eu_environment' => ['eu', 'catch-all-eu', '[EMAIL]'],
'us_environment' => ['us', 'catch-all', '[EMAIL]'],
'other_environment_defaults_to_us' => ['other', 'catch-all', '[EMAIL]'],
'eu_environment_with_plus_tag' => ['eu', 'catch-all-eu', '[EMAIL]'],
'us_environment_with_plus_tag' => ['us', 'catch-all', '[EMAIL]'],
];
}
protected function setUp(): void
{
parent::setUp();
}
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
#[DataProvider('environmentProvider')]
public function testIsForCurrentEnvironmentWithMatchingXGmOriginalToHeader(
string $deployRegion,
string $expectedAlias,
string $recipient
): void {
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = $recipient;
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');
$this->assertTrue($result);
}
#[DataProvider('environmentProvider')]
public function testIsForCurrentEnvironmentWithNonMatchingXGmOriginalToHeader(
string $deployRegion,
string $expectedAlias,
string $recipient
): void {
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentLogsRefusedMessage(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Refused message', [
'message_id' => 'msg123',
'original_to_sanitized' => '[EMAIL]',
]);
Log::shouldReceive('error')
->never()
->with('[TextRelayService] Failed to inspect message', \Mockery::type('array'));
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentFallsBackToToHeader(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertTrue($result);
}
public function testIsForCurrentEnvironmentWithEmptyHeaders(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$payload->method('getHeaders')->willReturn([]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
Log::shouldReceive('warning')
->once()
->with('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => 'msg123',
]);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentWithException(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')
->willThrowException(new \Exception('Gmail API error'));
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
Log::shouldReceive('error')
->once()
->with('[TextRelayService] Failed to inspect message', [
'message_id' => 'msg123',
'exception' => 'Gmail API error',
]);
\Sentry::shouldReceive('captureException')
->once()
->with(\Mockery::type(\Exception::class));
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentRejectsWrongHostWithMatchingPlusTag(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testSyncUsesEuAliasForEuRegion(): void
{
Config::set('jiminny.deploy_region', 'eu');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
$service = $this->createTextRelayService();
$gmailService = $this->createMock(GoogleGmail::class);
$historyResponse = $this->createMock(\Google\Service\Gmail\ListHistoryResponse::class);
$historyResponse->historyId = 12345;
$historyResponse->method('getHistory')->willReturn([]);
$historyResponse->method('getNextPageToken')->willReturn(null);
$usersHistory = $this->createMock(\Google\Service\Gmail\Resource\UsersHistory::class);
$usersHistory->method('listUsersHistory')->willReturn($historyResponse);
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
\Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
}
public function testSyncUsesUsAliasForUsRegion(): void
{
Config::set('jiminny.deploy_region', 'us');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
$service = $this->createTextRelayService();
$gmailService = $this->createMock(GoogleGmail::class);
$historyResponse = $this->createMock(\Google\Service\Gmail\ListHistoryResponse::class);
$historyResponse->historyId = 12345;
$historyResponse->method('getHistory')->willReturn([]);
$historyResponse->method('getNextPageToken')->willReturn(null);
$usersHistory = $this->createMock(\Google\Service\Gmail\Resource\UsersHistory::class);
$usersHistory->method('listUsersHistory')->willReturn($historyResponse);
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
\Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
}
public function testSyncWithEnhancedLogging(): void
{
Config::set('jiminny.deploy_region', 'eu');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
// Create a partial mock of the service to control the getHistory method
$service = $this->createPartialMock(TextRelayService::class, ['getHistory']);
// Mock getHistory to return empty array (no messages to process)
$service->method('getHistory')->willReturn([]);
// Mock Log facade - channel() returns a driver mock that accepts any method call
$logDriver = \Mockery::mock();
$logDriver->shouldIgnoreMissing();
Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Starting sync', [
'mailbox' => '[EMAIL]',
'expected_alias' => 'catch-all-eu',
'expected_host' => 'txt.jiminny.com',
]);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Sync completed', [
'mailbox' => '[EMAIL]',
'messages_processed' => 0,
'message_ids' => [],
]);
// Mock Sentry to prevent actual error reporting
\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testSyncWithRefusedMessageLogs(): void
{
Config::set('jiminny.deploy_region', 'eu');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
// Create a partial mock of the service to control the getHistory method
$service = $this->createPartialMock(TextRelayService::class, ['getHistory']);
// Mock getHistory to return empty array (no messages to process)
$service->method('getHistory')->willReturn([]);
// Mock Log facade - channel() returns a driver mock that accepts any method call
$logDriver = \Mockery::mock();
$logDriver->shouldIgnoreMissing();
Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Starting sync', [
'mailbox' => '[EMAIL]',
'expected_alias' => 'catch-all-eu',
'expected_host' => 'txt.jiminny.com',
]);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Sync completed', [
'mailbox' => '[EMAIL]',
'messages_processed' => 0,
'message_ids' => [],
]);
// Mock Sentry to prevent actual error reporting
\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetHistoryLogsErrorOnException(): void
{
Config::set('jiminny.google_text_relay_topic', 'test-topic');
Config::set('jiminny.google_text_user', '[EMAIL]');
$service = $this->createTextRelayService();
$gmailService = $this->createMock(GoogleGmail::class);
$usersHistory = $this->createMock(\Google\Service\Gmail\Resource\UsersHistory::class);
$usersHistory->method('listUsersHistory')
->willThrowException(new \Exception('Gmail API error'));
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
$logDriver = \Mockery::mock();
$logDriver->shouldIgnoreMissing();
Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);
Log::shouldReceive('error')
->once()
->with('[TextRelayService] Failed to fetch Gmail history', [
'exception' => 'Gmail API error',
]);
\Sentry::shouldReceive('captureException')
->once()
->with(\Mockery::type(\Exception::class));
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('getHistory');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
private function createTextRelayService(): TextRelayService
{
$service = new class () extends TextRelayService {
public function __construct()
{
}
};
return $service;
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
81592
|
2833
|
12
|
2026-05-28T08:31:06.802084+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957066802_m2.jpg...
|
PhpStorm
|
faVsco.js – TextRelayServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20915-fix-missing-head Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
13
64
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Mail;
use Google\Service\Gmail as GoogleGmail;
use Google\Service\Gmail\Message as GmailMessage;
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\Mail\TextRelayService;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
use ReflectionClass;
use Mockery;
#[CoversClass(TextRelayService::class)]
class TextRelayServiceTest extends TestCase
{
public static function environmentProvider(): array
{
return [
'eu_environment' => ['eu', 'catch-all-eu', '[EMAIL]'],
'us_environment' => ['us', 'catch-all', '[EMAIL]'],
'other_environment_defaults_to_us' => ['other', 'catch-all', '[EMAIL]'],
'eu_environment_with_plus_tag' => ['eu', 'catch-all-eu', '[EMAIL]'],
'us_environment_with_plus_tag' => ['us', 'catch-all', '[EMAIL]'],
];
}
protected function setUp(): void
{
parent::setUp();
}
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
#[DataProvider('environmentProvider')]
public function testIsForCurrentEnvironmentWithMatchingXGmOriginalToHeader(
string $deployRegion,
string $expectedAlias,
string $recipient
): void {
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = $recipient;
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');
$this->assertTrue($result);
}
#[DataProvider('environmentProvider')]
public function testIsForCurrentEnvironmentWithNonMatchingXGmOriginalToHeader(
string $deployRegion,
string $expectedAlias,
string $recipient
): void {
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentLogsRefusedMessage(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Refused message', [
'message_id' => 'msg123',
'original_to_sanitized' => '[EMAIL]',
]);
Log::shouldReceive('error')
->never()
->with('[TextRelayService] Failed to inspect message', \Mockery::type('array'));
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentFallsBackToToHeader(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertTrue($result);
}
public function testIsForCurrentEnvironmentWithEmptyHeaders(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$payload->method('getHeaders')->willReturn([]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
Log::shouldReceive('warning')
->once()
->with('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => 'msg123',
]);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentWithException(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')
->willThrowException(new \Exception('Gmail API error'));
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
Log::shouldReceive('error')
->once()
->with('[TextRelayService] Failed to inspect message', [
'message_id' => 'msg123',
'exception' => 'Gmail API error',
]);
\Sentry::shouldReceive('captureException')
->once()
->with(\Mockery::type(\Exception::class));
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentRejectsWrongHostWithMatchingPlusTag(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testSyncUsesEuAliasForEuRegion(): void
{
Config::set('jiminny.deploy_region', 'eu');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
$service = $this->createTextRelayService();
$gmailService = $this->createMock(GoogleGmail::class);
$historyResponse = $this->createMock(\Google\Service\Gmail\ListHistoryResponse::class);
$historyResponse->historyId = 12345;
$historyResponse->method('getHistory')->willReturn([]);
$historyResponse->method('getNextPageToken')->willReturn(null);
$usersHistory = $this->createMock(\Google\Service\Gmail\Resource\UsersHistory::class);
$usersHistory->method('listUsersHistory')->willReturn($historyResponse);
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
\Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
}
public function testSyncUsesUsAliasForUsRegion(): void
{
Config::set('jiminny.deploy_region', 'us');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
$service = $this->createTextRelayService();
$gmailService = $this->createMock(GoogleGmail::class);
$historyResponse = $this->createMock(\Google\Service\Gmail\ListHistoryResponse::class);
$historyResponse->historyId = 12345;
$historyResponse->method('getHistory')->willReturn([]);
$historyResponse->method('getNextPageToken')->willReturn(null);
$usersHistory = $this->createMock(\Google\Service\Gmail\Resource\UsersHistory::class);
$usersHistory->method('listUsersHistory')->willReturn($historyResponse);
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
\Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
}
public function testSyncWithEnhancedLogging(): void
{
Config::set('jiminny.deploy_region', 'eu');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
// Create a partial mock of the service to control the getHistory method
$service = $this->createPartialMock(TextRelayService::class, ['getHistory']);
// Mock getHistory to return empty array (no messages to process)
$service->method('getHistory')->willReturn([]);
// Mock Log facade - channel() returns a driver mock that accepts any method call
$logDriver = \Mockery::mock();
$logDriver->shouldIgnoreMissing();
Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Starting sync', [
'mailbox' => '[EMAIL]',
'expected_alias' => 'catch-all-eu',
'expected_host' => 'txt.jiminny.com',
]);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Sync completed', [
'mailbox' => '[EMAIL]',
'messages_processed' => 0,
'message_ids' => [],
]);
// Mock Sentry to prevent actual error reporting
\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testSyncWithRefusedMessageLogs(): void
{
Config::set('jiminny.deploy_region', 'eu');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
// Create a partial mock of the service to control the getHistory method
$service = $this->createPartialMock(TextRelayService::class, ['getHistory']);
// Mock getHistory to return empty array (no messages to process)
$service->method('getHistory')->willReturn([]);
// Mock Log facade - channel() returns a driver mock that accepts any method call
$logDriver = \Mockery::mock();
$logDriver->shouldIgnoreMissing();
Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Starting sync', [
'mailbox' => '[EMAIL]',
'expected_alias' => 'catch-all-eu',
'expected_host' => 'txt.jiminny.com',
]);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Sync completed', [
'mailbox' => '[EMAIL]',
'messages_processed' => 0,
'message_ids' => [],
]);
// Mock Sentry to prevent actual error reporting
\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetHistoryLogsErrorOnException(): void
{
Config::set('jiminny.google_text_relay_topic', 'test-topic');
Config::set('jiminny.google_text_user', '[EMAIL]');
$service = $this->createTextRelayService();
$gmailService = $this->createMock(GoogleGmail::class);
$usersHistory = $this->createMock(\Google\Service\Gmail\Resource\UsersHistory::class);
$usersHistory->method('listUsersHistory')
->willThrowException(new \Exception('Gmail API error'));
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
$logDriver = \Mockery::mock();
$logDriver->shouldIgnoreMissing();
Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);
Log::shouldReceive('error')
->once()
->with('[TextRelayService] Failed to fetch Gmail history', [
'exception' => 'Gmail API error',
]);
\Sentry::shouldReceive('captureException')
->once()
->with(\Mockery::type(\Exception::class));
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('getHistory');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
private function createTextRelayService(): TextRelayService
{
$service = new class () extends TextRelayService {
public function __construct()
{
}
};
return $service;
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20915-fix-missing-header-text-relay, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09740692,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20915-fix-missing-header-text-relay","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8374335,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"13","depth":4,"bounds":{"left":0.37333778,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"64","depth":4,"bounds":{"left":0.3849734,"top":0.10055866,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39694148,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40425533,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Mail;\n\nuse Google\\Service\\Gmail as GoogleGmail;\nuse Google\\Service\\Gmail\\Message as GmailMessage;\nuse Google\\Service\\Gmail\\MessagePart;\nuse Google\\Service\\Gmail\\MessagePartHeader;\nuse Illuminate\\Support\\Facades\\Config;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\Mail\\TextRelayService;\nuse PHPUnit\\Framework\\Attributes\\CoversClass;\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse Tests\\TestCase;\nuse ReflectionClass;\nuse Mockery;\n\n#[CoversClass(TextRelayService::class)]\nclass TextRelayServiceTest extends TestCase\n{\n public static function environmentProvider(): array\n {\n return [\n 'eu_environment' => ['eu', 'catch-all-eu', 'catch-all-eu@txt.jiminny.com'],\n 'us_environment' => ['us', 'catch-all', 'catch-all@txt.jiminny.com'],\n 'other_environment_defaults_to_us' => ['other', 'catch-all', 'catch-all@txt.jiminny.com'],\n 'eu_environment_with_plus_tag' => ['eu', 'catch-all-eu', 'catch-all-eu+447700152706.447893937879.3ndZZBqjRo@txt.jiminny.com'],\n 'us_environment_with_plus_tag' => ['us', 'catch-all', 'catch-all+447700152706.447893937879.3ndZZBqjRo@txt.jiminny.com'],\n ];\n }\n\n protected function setUp(): void\n {\n parent::setUp();\n }\n\n protected function tearDown(): void\n {\n Mockery::close();\n parent::tearDown();\n }\n\n #[DataProvider('environmentProvider')]\n public function testIsForCurrentEnvironmentWithMatchingXGmOriginalToHeader(\n string $deployRegion,\n string $expectedAlias,\n string $recipient\n ): void {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = $recipient;\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');\n\n $this->assertTrue($result);\n }\n\n #[DataProvider('environmentProvider')]\n public function testIsForCurrentEnvironmentWithNonMatchingXGmOriginalToHeader(\n string $deployRegion,\n string $expectedAlias,\n string $recipient\n ): void {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = 'other@txt.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentLogsRefusedMessage(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = 'wrong@txt.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Refused message', [\n 'message_id' => 'msg123',\n 'original_to_sanitized' => 'wrong@txt.jiminny.com',\n ]);\n\n Log::shouldReceive('error')\n ->never()\n ->with('[TextRelayService] Failed to inspect message', \\Mockery::type('array'));\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentFallsBackToToHeader(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'To';\n $header->value = 'catch-all@txt.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertTrue($result);\n }\n\n public function testIsForCurrentEnvironmentWithEmptyHeaders(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n\n $payload->method('getHeaders')->willReturn([]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n Log::shouldReceive('warning')\n ->once()\n ->with('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => 'msg123',\n ]);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentWithException(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')\n ->willThrowException(new \\Exception('Gmail API error'));\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n Log::shouldReceive('error')\n ->once()\n ->with('[TextRelayService] Failed to inspect message', [\n 'message_id' => 'msg123',\n 'exception' => 'Gmail API error',\n ]);\n\n \\Sentry::shouldReceive('captureException')\n ->once()\n ->with(\\Mockery::type(\\Exception::class));\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentRejectsWrongHostWithMatchingPlusTag(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = 'catch-all+447700152706.447893937879.3ndZZBqjRo@txt.staging.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testSyncUsesEuAliasForEuRegion(): void\n {\n Config::set('jiminny.deploy_region', 'eu');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n $service = $this->createTextRelayService();\n $gmailService = $this->createMock(GoogleGmail::class);\n\n $historyResponse = $this->createMock(\\Google\\Service\\Gmail\\ListHistoryResponse::class);\n $historyResponse->historyId = 12345;\n $historyResponse->method('getHistory')->willReturn([]);\n $historyResponse->method('getNextPageToken')->willReturn(null);\n\n $usersHistory = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersHistory::class);\n $usersHistory->method('listUsersHistory')->willReturn($historyResponse);\n $gmailService->users_history = $usersHistory;\n\n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n }\n\n public function testSyncUsesUsAliasForUsRegion(): void\n {\n Config::set('jiminny.deploy_region', 'us');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n $service = $this->createTextRelayService();\n $gmailService = $this->createMock(GoogleGmail::class);\n\n $historyResponse = $this->createMock(\\Google\\Service\\Gmail\\ListHistoryResponse::class);\n $historyResponse->historyId = 12345;\n $historyResponse->method('getHistory')->willReturn([]);\n $historyResponse->method('getNextPageToken')->willReturn(null);\n\n $usersHistory = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersHistory::class);\n $usersHistory->method('listUsersHistory')->willReturn($historyResponse);\n $gmailService->users_history = $usersHistory;\n\n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n }\n\n public function testSyncWithEnhancedLogging(): void\n {\n Config::set('jiminny.deploy_region', 'eu');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n\n // Mock Log facade - channel() returns a driver mock that accepts any method call\n $logDriver = \\Mockery::mock();\n $logDriver->shouldIgnoreMissing();\n Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Starting sync', [\n 'mailbox' => 'test@example.com',\n 'expected_alias' => 'catch-all-eu',\n 'expected_host' => 'txt.jiminny.com',\n ]);\n\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Sync completed', [\n 'mailbox' => 'test@example.com',\n 'messages_processed' => 0,\n 'message_ids' => [],\n ]);\n\n // Mock Sentry to prevent actual error reporting\n \\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testSyncWithRefusedMessageLogs(): void\n {\n Config::set('jiminny.deploy_region', 'eu');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n\n // Mock Log facade - channel() returns a driver mock that accepts any method call\n $logDriver = \\Mockery::mock();\n $logDriver->shouldIgnoreMissing();\n Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Starting sync', [\n 'mailbox' => 'test@example.com',\n 'expected_alias' => 'catch-all-eu',\n 'expected_host' => 'txt.jiminny.com',\n ]);\n\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Sync completed', [\n 'mailbox' => 'test@example.com',\n 'messages_processed' => 0,\n 'message_ids' => [],\n ]);\n\n // Mock Sentry to prevent actual error reporting\n \\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetHistoryLogsErrorOnException(): void\n {\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n Config::set('jiminny.google_text_user', 'test@example.com');\n\n $service = $this->createTextRelayService();\n $gmailService = $this->createMock(GoogleGmail::class);\n\n $usersHistory = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersHistory::class);\n $usersHistory->method('listUsersHistory')\n ->willThrowException(new \\Exception('Gmail API error'));\n $gmailService->users_history = $usersHistory;\n\n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n\n $logDriver = \\Mockery::mock();\n $logDriver->shouldIgnoreMissing();\n Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);\n Log::shouldReceive('error')\n ->once()\n ->with('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => 'Gmail API error',\n ]);\n\n \\Sentry::shouldReceive('captureException')\n ->once()\n ->with(\\Mockery::type(\\Exception::class));\n\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('getHistory');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n private function createTextRelayService(): TextRelayService\n {\n $service = new class () extends TextRelayService {\n public function __construct()\n {\n }\n };\n\n return $service;\n }\n\n}","depth":4,"bounds":{"left":0.13630319,"top":0.09736632,"width":0.27493352,"height":0.90263367},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Mail;\n\nuse Google\\Service\\Gmail as GoogleGmail;\nuse Google\\Service\\Gmail\\Message as GmailMessage;\nuse Google\\Service\\Gmail\\MessagePart;\nuse Google\\Service\\Gmail\\MessagePartHeader;\nuse Illuminate\\Support\\Facades\\Config;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\Mail\\TextRelayService;\nuse PHPUnit\\Framework\\Attributes\\CoversClass;\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse Tests\\TestCase;\nuse ReflectionClass;\nuse Mockery;\n\n#[CoversClass(TextRelayService::class)]\nclass TextRelayServiceTest extends TestCase\n{\n public static function environmentProvider(): array\n {\n return [\n 'eu_environment' => ['eu', 'catch-all-eu', 'catch-all-eu@txt.jiminny.com'],\n 'us_environment' => ['us', 'catch-all', 'catch-all@txt.jiminny.com'],\n 'other_environment_defaults_to_us' => ['other', 'catch-all', 'catch-all@txt.jiminny.com'],\n 'eu_environment_with_plus_tag' => ['eu', 'catch-all-eu', 'catch-all-eu+447700152706.447893937879.3ndZZBqjRo@txt.jiminny.com'],\n 'us_environment_with_plus_tag' => ['us', 'catch-all', 'catch-all+447700152706.447893937879.3ndZZBqjRo@txt.jiminny.com'],\n ];\n }\n\n protected function setUp(): void\n {\n parent::setUp();\n }\n\n protected function tearDown(): void\n {\n Mockery::close();\n parent::tearDown();\n }\n\n #[DataProvider('environmentProvider')]\n public function testIsForCurrentEnvironmentWithMatchingXGmOriginalToHeader(\n string $deployRegion,\n string $expectedAlias,\n string $recipient\n ): void {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = $recipient;\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');\n\n $this->assertTrue($result);\n }\n\n #[DataProvider('environmentProvider')]\n public function testIsForCurrentEnvironmentWithNonMatchingXGmOriginalToHeader(\n string $deployRegion,\n string $expectedAlias,\n string $recipient\n ): void {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = 'other@txt.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentLogsRefusedMessage(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = 'wrong@txt.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Refused message', [\n 'message_id' => 'msg123',\n 'original_to_sanitized' => 'wrong@txt.jiminny.com',\n ]);\n\n Log::shouldReceive('error')\n ->never()\n ->with('[TextRelayService] Failed to inspect message', \\Mockery::type('array'));\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentFallsBackToToHeader(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'To';\n $header->value = 'catch-all@txt.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertTrue($result);\n }\n\n public function testIsForCurrentEnvironmentWithEmptyHeaders(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n\n $payload->method('getHeaders')->willReturn([]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n Log::shouldReceive('warning')\n ->once()\n ->with('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => 'msg123',\n ]);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentWithException(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')\n ->willThrowException(new \\Exception('Gmail API error'));\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n Log::shouldReceive('error')\n ->once()\n ->with('[TextRelayService] Failed to inspect message', [\n 'message_id' => 'msg123',\n 'exception' => 'Gmail API error',\n ]);\n\n \\Sentry::shouldReceive('captureException')\n ->once()\n ->with(\\Mockery::type(\\Exception::class));\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentRejectsWrongHostWithMatchingPlusTag(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = 'catch-all+447700152706.447893937879.3ndZZBqjRo@txt.staging.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testSyncUsesEuAliasForEuRegion(): void\n {\n Config::set('jiminny.deploy_region', 'eu');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n $service = $this->createTextRelayService();\n $gmailService = $this->createMock(GoogleGmail::class);\n\n $historyResponse = $this->createMock(\\Google\\Service\\Gmail\\ListHistoryResponse::class);\n $historyResponse->historyId = 12345;\n $historyResponse->method('getHistory')->willReturn([]);\n $historyResponse->method('getNextPageToken')->willReturn(null);\n\n $usersHistory = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersHistory::class);\n $usersHistory->method('listUsersHistory')->willReturn($historyResponse);\n $gmailService->users_history = $usersHistory;\n\n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n }\n\n public function testSyncUsesUsAliasForUsRegion(): void\n {\n Config::set('jiminny.deploy_region', 'us');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n $service = $this->createTextRelayService();\n $gmailService = $this->createMock(GoogleGmail::class);\n\n $historyResponse = $this->createMock(\\Google\\Service\\Gmail\\ListHistoryResponse::class);\n $historyResponse->historyId = 12345;\n $historyResponse->method('getHistory')->willReturn([]);\n $historyResponse->method('getNextPageToken')->willReturn(null);\n\n $usersHistory = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersHistory::class);\n $usersHistory->method('listUsersHistory')->willReturn($historyResponse);\n $gmailService->users_history = $usersHistory;\n\n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n }\n\n public function testSyncWithEnhancedLogging(): void\n {\n Config::set('jiminny.deploy_region', 'eu');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n\n // Mock Log facade - channel() returns a driver mock that accepts any method call\n $logDriver = \\Mockery::mock();\n $logDriver->shouldIgnoreMissing();\n Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Starting sync', [\n 'mailbox' => 'test@example.com',\n 'expected_alias' => 'catch-all-eu',\n 'expected_host' => 'txt.jiminny.com',\n ]);\n\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Sync completed', [\n 'mailbox' => 'test@example.com',\n 'messages_processed' => 0,\n 'message_ids' => [],\n ]);\n\n // Mock Sentry to prevent actual error reporting\n \\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testSyncWithRefusedMessageLogs(): void\n {\n Config::set('jiminny.deploy_region', 'eu');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n\n // Mock Log facade - channel() returns a driver mock that accepts any method call\n $logDriver = \\Mockery::mock();\n $logDriver->shouldIgnoreMissing();\n Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Starting sync', [\n 'mailbox' => 'test@example.com',\n 'expected_alias' => 'catch-all-eu',\n 'expected_host' => 'txt.jiminny.com',\n ]);\n\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Sync completed', [\n 'mailbox' => 'test@example.com',\n 'messages_processed' => 0,\n 'message_ids' => [],\n ]);\n\n // Mock Sentry to prevent actual error reporting\n \\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetHistoryLogsErrorOnException(): void\n {\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n Config::set('jiminny.google_text_user', 'test@example.com');\n\n $service = $this->createTextRelayService();\n $gmailService = $this->createMock(GoogleGmail::class);\n\n $usersHistory = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersHistory::class);\n $usersHistory->method('listUsersHistory')\n ->willThrowException(new \\Exception('Gmail API error'));\n $gmailService->users_history = $usersHistory;\n\n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n\n $logDriver = \\Mockery::mock();\n $logDriver->shouldIgnoreMissing();\n Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);\n Log::shouldReceive('error')\n ->once()\n ->with('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => 'Gmail API error',\n ]);\n\n \\Sentry::shouldReceive('captureException')\n ->once()\n ->with(\\Mockery::type(\\Exception::class));\n\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('getHistory');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n private function createTextRelayService(): TextRelayService\n {\n $service = new class () extends TextRelayService {\n public function __construct()\n {\n }\n };\n\n return $service;\n }\n\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"","depth":4,"bounds":{"left":0.43051863,"top":0.09736632,"width":0.32280585,"height":0.31683958},"on_screen":true,"value":"","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3656098470495859466
|
-2005170756397648973
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20915-fix-missing-head Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
13
64
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Mail;
use Google\Service\Gmail as GoogleGmail;
use Google\Service\Gmail\Message as GmailMessage;
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\Mail\TextRelayService;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
use ReflectionClass;
use Mockery;
#[CoversClass(TextRelayService::class)]
class TextRelayServiceTest extends TestCase
{
public static function environmentProvider(): array
{
return [
'eu_environment' => ['eu', 'catch-all-eu', '[EMAIL]'],
'us_environment' => ['us', 'catch-all', '[EMAIL]'],
'other_environment_defaults_to_us' => ['other', 'catch-all', '[EMAIL]'],
'eu_environment_with_plus_tag' => ['eu', 'catch-all-eu', '[EMAIL]'],
'us_environment_with_plus_tag' => ['us', 'catch-all', '[EMAIL]'],
];
}
protected function setUp(): void
{
parent::setUp();
}
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
#[DataProvider('environmentProvider')]
public function testIsForCurrentEnvironmentWithMatchingXGmOriginalToHeader(
string $deployRegion,
string $expectedAlias,
string $recipient
): void {
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = $recipient;
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');
$this->assertTrue($result);
}
#[DataProvider('environmentProvider')]
public function testIsForCurrentEnvironmentWithNonMatchingXGmOriginalToHeader(
string $deployRegion,
string $expectedAlias,
string $recipient
): void {
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentLogsRefusedMessage(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Refused message', [
'message_id' => 'msg123',
'original_to_sanitized' => '[EMAIL]',
]);
Log::shouldReceive('error')
->never()
->with('[TextRelayService] Failed to inspect message', \Mockery::type('array'));
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentFallsBackToToHeader(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertTrue($result);
}
public function testIsForCurrentEnvironmentWithEmptyHeaders(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$payload->method('getHeaders')->willReturn([]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
Log::shouldReceive('warning')
->once()
->with('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => 'msg123',
]);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentWithException(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')
->willThrowException(new \Exception('Gmail API error'));
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
Log::shouldReceive('error')
->once()
->with('[TextRelayService] Failed to inspect message', [
'message_id' => 'msg123',
'exception' => 'Gmail API error',
]);
\Sentry::shouldReceive('captureException')
->once()
->with(\Mockery::type(\Exception::class));
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentRejectsWrongHostWithMatchingPlusTag(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testSyncUsesEuAliasForEuRegion(): void
{
Config::set('jiminny.deploy_region', 'eu');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
$service = $this->createTextRelayService();
$gmailService = $this->createMock(GoogleGmail::class);
$historyResponse = $this->createMock(\Google\Service\Gmail\ListHistoryResponse::class);
$historyResponse->historyId = 12345;
$historyResponse->method('getHistory')->willReturn([]);
$historyResponse->method('getNextPageToken')->willReturn(null);
$usersHistory = $this->createMock(\Google\Service\Gmail\Resource\UsersHistory::class);
$usersHistory->method('listUsersHistory')->willReturn($historyResponse);
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
\Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
}
public function testSyncUsesUsAliasForUsRegion(): void
{
Config::set('jiminny.deploy_region', 'us');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
$service = $this->createTextRelayService();
$gmailService = $this->createMock(GoogleGmail::class);
$historyResponse = $this->createMock(\Google\Service\Gmail\ListHistoryResponse::class);
$historyResponse->historyId = 12345;
$historyResponse->method('getHistory')->willReturn([]);
$historyResponse->method('getNextPageToken')->willReturn(null);
$usersHistory = $this->createMock(\Google\Service\Gmail\Resource\UsersHistory::class);
$usersHistory->method('listUsersHistory')->willReturn($historyResponse);
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
\Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
}
public function testSyncWithEnhancedLogging(): void
{
Config::set('jiminny.deploy_region', 'eu');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
// Create a partial mock of the service to control the getHistory method
$service = $this->createPartialMock(TextRelayService::class, ['getHistory']);
// Mock getHistory to return empty array (no messages to process)
$service->method('getHistory')->willReturn([]);
// Mock Log facade - channel() returns a driver mock that accepts any method call
$logDriver = \Mockery::mock();
$logDriver->shouldIgnoreMissing();
Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Starting sync', [
'mailbox' => '[EMAIL]',
'expected_alias' => 'catch-all-eu',
'expected_host' => 'txt.jiminny.com',
]);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Sync completed', [
'mailbox' => '[EMAIL]',
'messages_processed' => 0,
'message_ids' => [],
]);
// Mock Sentry to prevent actual error reporting
\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testSyncWithRefusedMessageLogs(): void
{
Config::set('jiminny.deploy_region', 'eu');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
// Create a partial mock of the service to control the getHistory method
$service = $this->createPartialMock(TextRelayService::class, ['getHistory']);
// Mock getHistory to return empty array (no messages to process)
$service->method('getHistory')->willReturn([]);
// Mock Log facade - channel() returns a driver mock that accepts any method call
$logDriver = \Mockery::mock();
$logDriver->shouldIgnoreMissing();
Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Starting sync', [
'mailbox' => '[EMAIL]',
'expected_alias' => 'catch-all-eu',
'expected_host' => 'txt.jiminny.com',
]);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Sync completed', [
'mailbox' => '[EMAIL]',
'messages_processed' => 0,
'message_ids' => [],
]);
// Mock Sentry to prevent actual error reporting
\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetHistoryLogsErrorOnException(): void
{
Config::set('jiminny.google_text_relay_topic', 'test-topic');
Config::set('jiminny.google_text_user', '[EMAIL]');
$service = $this->createTextRelayService();
$gmailService = $this->createMock(GoogleGmail::class);
$usersHistory = $this->createMock(\Google\Service\Gmail\Resource\UsersHistory::class);
$usersHistory->method('listUsersHistory')
->willThrowException(new \Exception('Gmail API error'));
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
$logDriver = \Mockery::mock();
$logDriver->shouldIgnoreMissing();
Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);
Log::shouldReceive('error')
->once()
->with('[TextRelayService] Failed to fetch Gmail history', [
'exception' => 'Gmail API error',
]);
\Sentry::shouldReceive('captureException')
->once()
->with(\Mockery::type(\Exception::class));
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('getHistory');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
private function createTextRelayService(): TextRelayService
{
$service = new class () extends TextRelayService {
public function __construct()
{
}
};
return $service;
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
81591
|
NULL
|
NULL
|
NULL
|
|
81591
|
2833
|
11
|
2026-05-28T08:31:03.664409+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957063664_m2.jpg...
|
PhpStorm
|
faVsco.js – TextRelayServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20915-fix-missing-head Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
13
64
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Mail;
use Google\Service\Gmail as GoogleGmail;
use Google\Service\Gmail\Message as GmailMessage;
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\Mail\TextRelayService;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
use ReflectionClass;
use Mockery;
#[CoversClass(TextRelayService::class)]
class TextRelayServiceTest extends TestCase
{
public static function environmentProvider(): array
{
return [
'eu_environment' => ['eu', 'catch-all-eu', '[EMAIL]'],
'us_environment' => ['us', 'catch-all', '[EMAIL]'],
'other_environment_defaults_to_us' => ['other', 'catch-all', '[EMAIL]'],
'eu_environment_with_plus_tag' => ['eu', 'catch-all-eu', '[EMAIL]'],
'us_environment_with_plus_tag' => ['us', 'catch-all', '[EMAIL]'],
];
}
protected function setUp(): void
{
parent::setUp();
}
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
#[DataProvider('environmentProvider')]
public function testIsForCurrentEnvironmentWithMatchingXGmOriginalToHeader(
string $deployRegion,
string $expectedAlias,
string $recipient
): void {
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = $recipient;
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');
$this->assertTrue($result);
}
#[DataProvider('environmentProvider')]
public function testIsForCurrentEnvironmentWithNonMatchingXGmOriginalToHeader(
string $deployRegion,
string $expectedAlias,
string $recipient
): void {
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentLogsRefusedMessage(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Refused message', [
'message_id' => 'msg123',
'original_to_sanitized' => '[EMAIL]',
]);
Log::shouldReceive('error')
->never()
->with('[TextRelayService] Failed to inspect message', \Mockery::type('array'));
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentFallsBackToToHeader(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertTrue($result);
}
public function testIsForCurrentEnvironmentWithEmptyHeaders(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$payload->method('getHeaders')->willReturn([]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
Log::shouldReceive('warning')
->once()
->with('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => 'msg123',
]);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentWithException(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')
->willThrowException(new \Exception('Gmail API error'));
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
Log::shouldReceive('error')
->once()
->with('[TextRelayService] Failed to inspect message', [
'message_id' => 'msg123',
'exception' => 'Gmail API error',
]);
\Sentry::shouldReceive('captureException')
->once()
->with(\Mockery::type(\Exception::class));
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentRejectsWrongHostWithMatchingPlusTag(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testSyncUsesEuAliasForEuRegion(): void
{
Config::set('jiminny.deploy_region', 'eu');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
$service = $this->createTextRelayService();
$gmailService = $this->createMock(GoogleGmail::class);
$historyResponse = $this->createMock(\Google\Service\Gmail\ListHistoryResponse::class);
$historyResponse->historyId = 12345;
$historyResponse->method('getHistory')->willReturn([]);
$historyResponse->method('getNextPageToken')->willReturn(null);
$usersHistory = $this->createMock(\Google\Service\Gmail\Resource\UsersHistory::class);
$usersHistory->method('listUsersHistory')->willReturn($historyResponse);
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
\Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
}
public function testSyncUsesUsAliasForUsRegion(): void
{
Config::set('jiminny.deploy_region', 'us');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
$service = $this->createTextRelayService();
$gmailService = $this->createMock(GoogleGmail::class);
$historyResponse = $this->createMock(\Google\Service\Gmail\ListHistoryResponse::class);
$historyResponse->historyId = 12345;
$historyResponse->method('getHistory')->willReturn([]);
$historyResponse->method('getNextPageToken')->willReturn(null);
$usersHistory = $this->createMock(\Google\Service\Gmail\Resource\UsersHistory::class);
$usersHistory->method('listUsersHistory')->willReturn($historyResponse);
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
\Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
}
public function testSyncWithEnhancedLogging(): void
{
Config::set('jiminny.deploy_region', 'eu');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
// Create a partial mock of the service to control the getHistory method
$service = $this->createPartialMock(TextRelayService::class, ['getHistory']);
// Mock getHistory to return empty array (no messages to process)
$service->method('getHistory')->willReturn([]);
// Mock Log facade - channel() returns a driver mock that accepts any method call
$logDriver = \Mockery::mock();
$logDriver->shouldIgnoreMissing();
Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Starting sync', [
'mailbox' => '[EMAIL]',
'expected_alias' => 'catch-all-eu',
'expected_host' => 'txt.jiminny.com',
]);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Sync completed', [
'mailbox' => '[EMAIL]',
'messages_processed' => 0,
'message_ids' => [],
]);
// Mock Sentry to prevent actual error reporting
\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testSyncWithRefusedMessageLogs(): void
{
Config::set('jiminny.deploy_region', 'eu');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
// Create a partial mock of the service to control the getHistory method
$service = $this->createPartialMock(TextRelayService::class, ['getHistory']);
// Mock getHistory to return empty array (no messages to process)
$service->method('getHistory')->willReturn([]);
// Mock Log facade - channel() returns a driver mock that accepts any method call
$logDriver = \Mockery::mock();
$logDriver->shouldIgnoreMissing();
Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Starting sync', [
'mailbox' => '[EMAIL]',
'expected_alias' => 'catch-all-eu',
'expected_host' => 'txt.jiminny.com',
]);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Sync completed', [
'mailbox' => '[EMAIL]',
'messages_processed' => 0,
'message_ids' => [],
]);
// Mock Sentry to prevent actual error reporting
\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetHistoryLogsErrorOnException(): void
{
Config::set('jiminny.google_text_relay_topic', 'test-topic');
Config::set('jiminny.google_text_user', '[EMAIL]');
$service = $this->createTextRelayService();
$gmailService = $this->createMock(GoogleGmail::class);
$usersHistory = $this->createMock(\Google\Service\Gmail\Resource\UsersHistory::class);
$usersHistory->method('listUsersHistory')
->willThrowException(new \Exception('Gmail API error'));
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
$logDriver = \Mockery::mock();
$logDriver->shouldIgnoreMissing();
Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);
Log::shouldReceive('error')
->once()
->with('[TextRelayService] Failed to fetch Gmail history', [
'exception' => 'Gmail API error',
]);
\Sentry::shouldReceive('captureException')
->once()
->with(\Mockery::type(\Exception::class));
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('getHistory');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
private function createTextRelayService(): TextRelayService
{
$service = new class () extends TextRelayService {
public function __construct()
{
}
};
return $service;
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
Project
Project...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20915-fix-missing-header-text-relay, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09740692,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20915-fix-missing-header-text-relay","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8374335,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"13","depth":4,"bounds":{"left":0.37333778,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"64","depth":4,"bounds":{"left":0.3849734,"top":0.10055866,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39694148,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40425533,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Mail;\n\nuse Google\\Service\\Gmail as GoogleGmail;\nuse Google\\Service\\Gmail\\Message as GmailMessage;\nuse Google\\Service\\Gmail\\MessagePart;\nuse Google\\Service\\Gmail\\MessagePartHeader;\nuse Illuminate\\Support\\Facades\\Config;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\Mail\\TextRelayService;\nuse PHPUnit\\Framework\\Attributes\\CoversClass;\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse Tests\\TestCase;\nuse ReflectionClass;\nuse Mockery;\n\n#[CoversClass(TextRelayService::class)]\nclass TextRelayServiceTest extends TestCase\n{\n public static function environmentProvider(): array\n {\n return [\n 'eu_environment' => ['eu', 'catch-all-eu', 'catch-all-eu@txt.jiminny.com'],\n 'us_environment' => ['us', 'catch-all', 'catch-all@txt.jiminny.com'],\n 'other_environment_defaults_to_us' => ['other', 'catch-all', 'catch-all@txt.jiminny.com'],\n 'eu_environment_with_plus_tag' => ['eu', 'catch-all-eu', 'catch-all-eu+447700152706.447893937879.3ndZZBqjRo@txt.jiminny.com'],\n 'us_environment_with_plus_tag' => ['us', 'catch-all', 'catch-all+447700152706.447893937879.3ndZZBqjRo@txt.jiminny.com'],\n ];\n }\n\n protected function setUp(): void\n {\n parent::setUp();\n }\n\n protected function tearDown(): void\n {\n Mockery::close();\n parent::tearDown();\n }\n\n #[DataProvider('environmentProvider')]\n public function testIsForCurrentEnvironmentWithMatchingXGmOriginalToHeader(\n string $deployRegion,\n string $expectedAlias,\n string $recipient\n ): void {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = $recipient;\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');\n\n $this->assertTrue($result);\n }\n\n #[DataProvider('environmentProvider')]\n public function testIsForCurrentEnvironmentWithNonMatchingXGmOriginalToHeader(\n string $deployRegion,\n string $expectedAlias,\n string $recipient\n ): void {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = 'other@txt.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentLogsRefusedMessage(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = 'wrong@txt.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Refused message', [\n 'message_id' => 'msg123',\n 'original_to_sanitized' => 'wrong@txt.jiminny.com',\n ]);\n\n Log::shouldReceive('error')\n ->never()\n ->with('[TextRelayService] Failed to inspect message', \\Mockery::type('array'));\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentFallsBackToToHeader(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'To';\n $header->value = 'catch-all@txt.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertTrue($result);\n }\n\n public function testIsForCurrentEnvironmentWithEmptyHeaders(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n\n $payload->method('getHeaders')->willReturn([]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n Log::shouldReceive('warning')\n ->once()\n ->with('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => 'msg123',\n ]);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentWithException(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')\n ->willThrowException(new \\Exception('Gmail API error'));\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n Log::shouldReceive('error')\n ->once()\n ->with('[TextRelayService] Failed to inspect message', [\n 'message_id' => 'msg123',\n 'exception' => 'Gmail API error',\n ]);\n\n \\Sentry::shouldReceive('captureException')\n ->once()\n ->with(\\Mockery::type(\\Exception::class));\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentRejectsWrongHostWithMatchingPlusTag(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = 'catch-all+447700152706.447893937879.3ndZZBqjRo@txt.staging.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testSyncUsesEuAliasForEuRegion(): void\n {\n Config::set('jiminny.deploy_region', 'eu');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n $service = $this->createTextRelayService();\n $gmailService = $this->createMock(GoogleGmail::class);\n\n $historyResponse = $this->createMock(\\Google\\Service\\Gmail\\ListHistoryResponse::class);\n $historyResponse->historyId = 12345;\n $historyResponse->method('getHistory')->willReturn([]);\n $historyResponse->method('getNextPageToken')->willReturn(null);\n\n $usersHistory = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersHistory::class);\n $usersHistory->method('listUsersHistory')->willReturn($historyResponse);\n $gmailService->users_history = $usersHistory;\n\n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n }\n\n public function testSyncUsesUsAliasForUsRegion(): void\n {\n Config::set('jiminny.deploy_region', 'us');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n $service = $this->createTextRelayService();\n $gmailService = $this->createMock(GoogleGmail::class);\n\n $historyResponse = $this->createMock(\\Google\\Service\\Gmail\\ListHistoryResponse::class);\n $historyResponse->historyId = 12345;\n $historyResponse->method('getHistory')->willReturn([]);\n $historyResponse->method('getNextPageToken')->willReturn(null);\n\n $usersHistory = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersHistory::class);\n $usersHistory->method('listUsersHistory')->willReturn($historyResponse);\n $gmailService->users_history = $usersHistory;\n\n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n }\n\n public function testSyncWithEnhancedLogging(): void\n {\n Config::set('jiminny.deploy_region', 'eu');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n\n // Mock Log facade - channel() returns a driver mock that accepts any method call\n $logDriver = \\Mockery::mock();\n $logDriver->shouldIgnoreMissing();\n Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Starting sync', [\n 'mailbox' => 'test@example.com',\n 'expected_alias' => 'catch-all-eu',\n 'expected_host' => 'txt.jiminny.com',\n ]);\n\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Sync completed', [\n 'mailbox' => 'test@example.com',\n 'messages_processed' => 0,\n 'message_ids' => [],\n ]);\n\n // Mock Sentry to prevent actual error reporting\n \\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testSyncWithRefusedMessageLogs(): void\n {\n Config::set('jiminny.deploy_region', 'eu');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n\n // Mock Log facade - channel() returns a driver mock that accepts any method call\n $logDriver = \\Mockery::mock();\n $logDriver->shouldIgnoreMissing();\n Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Starting sync', [\n 'mailbox' => 'test@example.com',\n 'expected_alias' => 'catch-all-eu',\n 'expected_host' => 'txt.jiminny.com',\n ]);\n\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Sync completed', [\n 'mailbox' => 'test@example.com',\n 'messages_processed' => 0,\n 'message_ids' => [],\n ]);\n\n // Mock Sentry to prevent actual error reporting\n \\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetHistoryLogsErrorOnException(): void\n {\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n Config::set('jiminny.google_text_user', 'test@example.com');\n\n $service = $this->createTextRelayService();\n $gmailService = $this->createMock(GoogleGmail::class);\n\n $usersHistory = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersHistory::class);\n $usersHistory->method('listUsersHistory')\n ->willThrowException(new \\Exception('Gmail API error'));\n $gmailService->users_history = $usersHistory;\n\n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n\n $logDriver = \\Mockery::mock();\n $logDriver->shouldIgnoreMissing();\n Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);\n Log::shouldReceive('error')\n ->once()\n ->with('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => 'Gmail API error',\n ]);\n\n \\Sentry::shouldReceive('captureException')\n ->once()\n ->with(\\Mockery::type(\\Exception::class));\n\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('getHistory');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n private function createTextRelayService(): TextRelayService\n {\n $service = new class () extends TextRelayService {\n public function __construct()\n {\n }\n };\n\n return $service;\n }\n\n}","depth":4,"bounds":{"left":0.13630319,"top":0.09736632,"width":0.27493352,"height":0.90263367},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Mail;\n\nuse Google\\Service\\Gmail as GoogleGmail;\nuse Google\\Service\\Gmail\\Message as GmailMessage;\nuse Google\\Service\\Gmail\\MessagePart;\nuse Google\\Service\\Gmail\\MessagePartHeader;\nuse Illuminate\\Support\\Facades\\Config;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\Mail\\TextRelayService;\nuse PHPUnit\\Framework\\Attributes\\CoversClass;\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse Tests\\TestCase;\nuse ReflectionClass;\nuse Mockery;\n\n#[CoversClass(TextRelayService::class)]\nclass TextRelayServiceTest extends TestCase\n{\n public static function environmentProvider(): array\n {\n return [\n 'eu_environment' => ['eu', 'catch-all-eu', 'catch-all-eu@txt.jiminny.com'],\n 'us_environment' => ['us', 'catch-all', 'catch-all@txt.jiminny.com'],\n 'other_environment_defaults_to_us' => ['other', 'catch-all', 'catch-all@txt.jiminny.com'],\n 'eu_environment_with_plus_tag' => ['eu', 'catch-all-eu', 'catch-all-eu+447700152706.447893937879.3ndZZBqjRo@txt.jiminny.com'],\n 'us_environment_with_plus_tag' => ['us', 'catch-all', 'catch-all+447700152706.447893937879.3ndZZBqjRo@txt.jiminny.com'],\n ];\n }\n\n protected function setUp(): void\n {\n parent::setUp();\n }\n\n protected function tearDown(): void\n {\n Mockery::close();\n parent::tearDown();\n }\n\n #[DataProvider('environmentProvider')]\n public function testIsForCurrentEnvironmentWithMatchingXGmOriginalToHeader(\n string $deployRegion,\n string $expectedAlias,\n string $recipient\n ): void {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = $recipient;\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');\n\n $this->assertTrue($result);\n }\n\n #[DataProvider('environmentProvider')]\n public function testIsForCurrentEnvironmentWithNonMatchingXGmOriginalToHeader(\n string $deployRegion,\n string $expectedAlias,\n string $recipient\n ): void {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = 'other@txt.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentLogsRefusedMessage(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = 'wrong@txt.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Refused message', [\n 'message_id' => 'msg123',\n 'original_to_sanitized' => 'wrong@txt.jiminny.com',\n ]);\n\n Log::shouldReceive('error')\n ->never()\n ->with('[TextRelayService] Failed to inspect message', \\Mockery::type('array'));\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentFallsBackToToHeader(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'To';\n $header->value = 'catch-all@txt.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertTrue($result);\n }\n\n public function testIsForCurrentEnvironmentWithEmptyHeaders(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n\n $payload->method('getHeaders')->willReturn([]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n Log::shouldReceive('warning')\n ->once()\n ->with('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => 'msg123',\n ]);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentWithException(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')\n ->willThrowException(new \\Exception('Gmail API error'));\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n Log::shouldReceive('error')\n ->once()\n ->with('[TextRelayService] Failed to inspect message', [\n 'message_id' => 'msg123',\n 'exception' => 'Gmail API error',\n ]);\n\n \\Sentry::shouldReceive('captureException')\n ->once()\n ->with(\\Mockery::type(\\Exception::class));\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentRejectsWrongHostWithMatchingPlusTag(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = 'catch-all+447700152706.447893937879.3ndZZBqjRo@txt.staging.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testSyncUsesEuAliasForEuRegion(): void\n {\n Config::set('jiminny.deploy_region', 'eu');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n $service = $this->createTextRelayService();\n $gmailService = $this->createMock(GoogleGmail::class);\n\n $historyResponse = $this->createMock(\\Google\\Service\\Gmail\\ListHistoryResponse::class);\n $historyResponse->historyId = 12345;\n $historyResponse->method('getHistory')->willReturn([]);\n $historyResponse->method('getNextPageToken')->willReturn(null);\n\n $usersHistory = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersHistory::class);\n $usersHistory->method('listUsersHistory')->willReturn($historyResponse);\n $gmailService->users_history = $usersHistory;\n\n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n }\n\n public function testSyncUsesUsAliasForUsRegion(): void\n {\n Config::set('jiminny.deploy_region', 'us');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n $service = $this->createTextRelayService();\n $gmailService = $this->createMock(GoogleGmail::class);\n\n $historyResponse = $this->createMock(\\Google\\Service\\Gmail\\ListHistoryResponse::class);\n $historyResponse->historyId = 12345;\n $historyResponse->method('getHistory')->willReturn([]);\n $historyResponse->method('getNextPageToken')->willReturn(null);\n\n $usersHistory = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersHistory::class);\n $usersHistory->method('listUsersHistory')->willReturn($historyResponse);\n $gmailService->users_history = $usersHistory;\n\n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n }\n\n public function testSyncWithEnhancedLogging(): void\n {\n Config::set('jiminny.deploy_region', 'eu');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n\n // Mock Log facade - channel() returns a driver mock that accepts any method call\n $logDriver = \\Mockery::mock();\n $logDriver->shouldIgnoreMissing();\n Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Starting sync', [\n 'mailbox' => 'test@example.com',\n 'expected_alias' => 'catch-all-eu',\n 'expected_host' => 'txt.jiminny.com',\n ]);\n\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Sync completed', [\n 'mailbox' => 'test@example.com',\n 'messages_processed' => 0,\n 'message_ids' => [],\n ]);\n\n // Mock Sentry to prevent actual error reporting\n \\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testSyncWithRefusedMessageLogs(): void\n {\n Config::set('jiminny.deploy_region', 'eu');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n\n // Mock Log facade - channel() returns a driver mock that accepts any method call\n $logDriver = \\Mockery::mock();\n $logDriver->shouldIgnoreMissing();\n Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Starting sync', [\n 'mailbox' => 'test@example.com',\n 'expected_alias' => 'catch-all-eu',\n 'expected_host' => 'txt.jiminny.com',\n ]);\n\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Sync completed', [\n 'mailbox' => 'test@example.com',\n 'messages_processed' => 0,\n 'message_ids' => [],\n ]);\n\n // Mock Sentry to prevent actual error reporting\n \\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetHistoryLogsErrorOnException(): void\n {\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n Config::set('jiminny.google_text_user', 'test@example.com');\n\n $service = $this->createTextRelayService();\n $gmailService = $this->createMock(GoogleGmail::class);\n\n $usersHistory = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersHistory::class);\n $usersHistory->method('listUsersHistory')\n ->willThrowException(new \\Exception('Gmail API error'));\n $gmailService->users_history = $usersHistory;\n\n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n\n $logDriver = \\Mockery::mock();\n $logDriver->shouldIgnoreMissing();\n Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);\n Log::shouldReceive('error')\n ->once()\n ->with('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => 'Gmail API error',\n ]);\n\n \\Sentry::shouldReceive('captureException')\n ->once()\n ->with(\\Mockery::type(\\Exception::class));\n\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('getHistory');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n private function createTextRelayService(): TextRelayService\n {\n $service = new class () extends TextRelayService {\n public function __construct()\n {\n }\n };\n\n return $service;\n }\n\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"","depth":4,"bounds":{"left":0.43051863,"top":0.09736632,"width":0.32280585,"height":0.31683958},"on_screen":true,"value":"","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1974320794878416893
|
-2005170756431203405
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20915-fix-missing-head Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
13
64
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Mail;
use Google\Service\Gmail as GoogleGmail;
use Google\Service\Gmail\Message as GmailMessage;
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\Mail\TextRelayService;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
use ReflectionClass;
use Mockery;
#[CoversClass(TextRelayService::class)]
class TextRelayServiceTest extends TestCase
{
public static function environmentProvider(): array
{
return [
'eu_environment' => ['eu', 'catch-all-eu', '[EMAIL]'],
'us_environment' => ['us', 'catch-all', '[EMAIL]'],
'other_environment_defaults_to_us' => ['other', 'catch-all', '[EMAIL]'],
'eu_environment_with_plus_tag' => ['eu', 'catch-all-eu', '[EMAIL]'],
'us_environment_with_plus_tag' => ['us', 'catch-all', '[EMAIL]'],
];
}
protected function setUp(): void
{
parent::setUp();
}
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
#[DataProvider('environmentProvider')]
public function testIsForCurrentEnvironmentWithMatchingXGmOriginalToHeader(
string $deployRegion,
string $expectedAlias,
string $recipient
): void {
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = $recipient;
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');
$this->assertTrue($result);
}
#[DataProvider('environmentProvider')]
public function testIsForCurrentEnvironmentWithNonMatchingXGmOriginalToHeader(
string $deployRegion,
string $expectedAlias,
string $recipient
): void {
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentLogsRefusedMessage(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Refused message', [
'message_id' => 'msg123',
'original_to_sanitized' => '[EMAIL]',
]);
Log::shouldReceive('error')
->never()
->with('[TextRelayService] Failed to inspect message', \Mockery::type('array'));
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentFallsBackToToHeader(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertTrue($result);
}
public function testIsForCurrentEnvironmentWithEmptyHeaders(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$payload->method('getHeaders')->willReturn([]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
Log::shouldReceive('warning')
->once()
->with('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => 'msg123',
]);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentWithException(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')
->willThrowException(new \Exception('Gmail API error'));
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
Log::shouldReceive('error')
->once()
->with('[TextRelayService] Failed to inspect message', [
'message_id' => 'msg123',
'exception' => 'Gmail API error',
]);
\Sentry::shouldReceive('captureException')
->once()
->with(\Mockery::type(\Exception::class));
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentRejectsWrongHostWithMatchingPlusTag(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testSyncUsesEuAliasForEuRegion(): void
{
Config::set('jiminny.deploy_region', 'eu');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
$service = $this->createTextRelayService();
$gmailService = $this->createMock(GoogleGmail::class);
$historyResponse = $this->createMock(\Google\Service\Gmail\ListHistoryResponse::class);
$historyResponse->historyId = 12345;
$historyResponse->method('getHistory')->willReturn([]);
$historyResponse->method('getNextPageToken')->willReturn(null);
$usersHistory = $this->createMock(\Google\Service\Gmail\Resource\UsersHistory::class);
$usersHistory->method('listUsersHistory')->willReturn($historyResponse);
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
\Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
}
public function testSyncUsesUsAliasForUsRegion(): void
{
Config::set('jiminny.deploy_region', 'us');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
$service = $this->createTextRelayService();
$gmailService = $this->createMock(GoogleGmail::class);
$historyResponse = $this->createMock(\Google\Service\Gmail\ListHistoryResponse::class);
$historyResponse->historyId = 12345;
$historyResponse->method('getHistory')->willReturn([]);
$historyResponse->method('getNextPageToken')->willReturn(null);
$usersHistory = $this->createMock(\Google\Service\Gmail\Resource\UsersHistory::class);
$usersHistory->method('listUsersHistory')->willReturn($historyResponse);
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
\Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
}
public function testSyncWithEnhancedLogging(): void
{
Config::set('jiminny.deploy_region', 'eu');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
// Create a partial mock of the service to control the getHistory method
$service = $this->createPartialMock(TextRelayService::class, ['getHistory']);
// Mock getHistory to return empty array (no messages to process)
$service->method('getHistory')->willReturn([]);
// Mock Log facade - channel() returns a driver mock that accepts any method call
$logDriver = \Mockery::mock();
$logDriver->shouldIgnoreMissing();
Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Starting sync', [
'mailbox' => '[EMAIL]',
'expected_alias' => 'catch-all-eu',
'expected_host' => 'txt.jiminny.com',
]);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Sync completed', [
'mailbox' => '[EMAIL]',
'messages_processed' => 0,
'message_ids' => [],
]);
// Mock Sentry to prevent actual error reporting
\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testSyncWithRefusedMessageLogs(): void
{
Config::set('jiminny.deploy_region', 'eu');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
// Create a partial mock of the service to control the getHistory method
$service = $this->createPartialMock(TextRelayService::class, ['getHistory']);
// Mock getHistory to return empty array (no messages to process)
$service->method('getHistory')->willReturn([]);
// Mock Log facade - channel() returns a driver mock that accepts any method call
$logDriver = \Mockery::mock();
$logDriver->shouldIgnoreMissing();
Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Starting sync', [
'mailbox' => '[EMAIL]',
'expected_alias' => 'catch-all-eu',
'expected_host' => 'txt.jiminny.com',
]);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Sync completed', [
'mailbox' => '[EMAIL]',
'messages_processed' => 0,
'message_ids' => [],
]);
// Mock Sentry to prevent actual error reporting
\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetHistoryLogsErrorOnException(): void
{
Config::set('jiminny.google_text_relay_topic', 'test-topic');
Config::set('jiminny.google_text_user', '[EMAIL]');
$service = $this->createTextRelayService();
$gmailService = $this->createMock(GoogleGmail::class);
$usersHistory = $this->createMock(\Google\Service\Gmail\Resource\UsersHistory::class);
$usersHistory->method('listUsersHistory')
->willThrowException(new \Exception('Gmail API error'));
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
$logDriver = \Mockery::mock();
$logDriver->shouldIgnoreMissing();
Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);
Log::shouldReceive('error')
->once()
->with('[TextRelayService] Failed to fetch Gmail history', [
'exception' => 'Gmail API error',
]);
\Sentry::shouldReceive('captureException')
->once()
->with(\Mockery::type(\Exception::class));
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('getHistory');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
private function createTextRelayService(): TextRelayService
{
$service = new class () extends TextRelayService {
public function __construct()
{
}
};
return $service;
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
Project
Project...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
81590
|
2832
|
10
|
2026-05-28T08:31:01.358563+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957061358_m1.jpg...
|
PhpStorm
|
faVsco.js – TextRelayServiceTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20915-fix-missing-head Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20915-fix-missing-header-text-relay, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20915-fix-missing-header-text-relay","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-3150964534923304660
|
-4304507549388960332
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20915-fix-missing-head Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
iTerm2ShellEditViewSessionScriptsProfilesWindowHelp‹$0A-zshscreenpipe"DOCKERO ₴1DEV (docker)worker-calendar:worker-calendar_00:startedworker-conferences:worker-conferences_00: startedworker-crm-sync:worker-crm-sync_00: startedworker-crm-update:worker-crm-update_00: startedworker-download:worker-download_00:startedworker-emails:worker-emails_00:startedworker-es-update:worker-es-update_00:startedworker-nudges:worker-nudges_00: started₴2-zshО 84-zshWhat's next:Try Docker Debug forLearnseamless, persistentdebugging tools in any container or image → docker debug docker_lamp_1moreat [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-fix-missing-header-text-relay) $csfixdocker exec -it docker_lamp_1•/vendor/bin/php-cs-fixer fix--config=.php-cs-fixer.dist.php-v--using-cache=no --diffPHP CS Fixer 3.95.1 Adalbertus by Fabien Potencier, Dariusz Ruminski and contributors.PHP runtime: 8.5.5Loaded config default from ".php-cs-fixer.dist.php".Running analysis on 7 cores with 10 files per process.5698/5698 C80100%*5ec2-user@ip-10-30-1...O 86100% <78• Thu 28 May 11:31:01T81ec2-user@ip-10-30-140-...$7Fixed 0 of 5698 files in 52.731 seconds, 799.06 MB memory usedDetected deprecations in use (they will stop working in next major release):- Rule set"@PHP74Migration" is deprecated. Use"@PHP7x4Migration" instead.- Rule set "@PHP80Migration" is deprecated. Use "@PHP8x®Migration" instead.- Rule set "@PHP81Migration" is deprecated. Use "@PHP8x1Migration" instead.- Rule set "@PHP82Migration" is deprecated. Use "@PHP8x2Migration" instead.- Rule set "@PHP83Migration" is deprecated. Use "@PHP8x3Migration"instead.- Rule set "@PHP84Migration" is deprecated. Use "@PHP8x4Migration" instead.What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image + docker debug docker_lamp_1Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-fix-missing-header-text-relay) $D...
|
81587
|
NULL
|
NULL
|
NULL
|
|
81589
|
2833
|
10
|
2026-05-28T08:31:01.257343+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957061257_m2.jpg...
|
PhpStorm
|
faVsco.js – TextRelayServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
rapstomViewNeweNNCCoocKetuciolool.WindowFV faVsco. rapstomViewNeweNNCCoocKetuciolool.WindowFV faVsco.sv$ JY-20915-fix-missing-header-text-relayProinet vAcmelpn© SyncMailbox.php© ServiceTest.php© SyncBatchRedisServiceRaseSercetest oho©) TextRelayServiceTest.php x php fiminny.php3.env.productioncionec CachedCrmServiceDecoradectarelScracrcypes="© CrmActivityServiceTest.ptc CrmConfigurationSettingsnanesoace vests Unzc services naze"CrmObiectsResolverTest.rDefaultProspectSearchStusc 6000Le Servzce 6302l a$ 16000Lc60322EmailHelperTest.phousc 6000Lc Service 6n022 nessaoc as 6ma2chessagc:© FieldValueConverterTest.gusc 6000Le Service 6na2, hessagcPana© LayoutManagerTest.phduse 6000le Service 6na2 nessagePar HeadersC MigrateProviderServiceTeuse zuminare Suppor Facades contar© OpportunityActivityMatchiuse uminate Suppors Facades, Loa:OpportunitySyncStrateay© ProspectCacheTest.phouse miinny Serusces.nu tey cel aysenuicosuse PHPUn1t\Franework\Attributes (coversclass:c Prosoccsearchstratcoyt© ProviderRecistryTest.phouse Prpnr Eranemorkrhures hara?rousderuse ests iestlase,c Recordseleclortestonouse Pe ections ass) ResoivecomoanvNamesvluse HockenyLochlehnotchandes 5iasenullocallaoc@.liminnv?ebuCommand.cho.ano/Console/Commandsphe logging.php config© PlaybackService.php app/Services© TextRelayService.php app/Services/Mai© TextRelayServiceTest.php tests/Unit/Services/Mail• Uoversionad Filas 9 GlaelTextRelayServiceTestDockerfile= laravel.log X SF (iminny@localhost)A console (STAGING]HS Jocal (jiminny@alocalhost)console [PROD# consolefFuloo nu co moy tirotur+0.rner hensewiettix @TextRelayServiceTest.phpTnodgnttor 1sResdTextRelayScrvico est.oho #L154-188Thought for 1seexKeayeMicesthontThoughtsRead TextRelayServiceTest.php #L150-199thought for 16Sasrched testistor@urrent Environment.allsBackio ohesder in teste/UntSamices Moiln extRelay Semvice est.oho.Ask annineeo<› Code SW-Td→ aSide-by-side viewerö 828865ed app/Services/Mail/TextRelayService.phgDo not ignoreyHighlight wordsx1gtry 4Snessage = Sservice->users nessages->get(Smailbox, Snessageld):Sheaders = Ssessage->getPayLoad@)->getHeaders@>forcach (Sheaders as Sheader)3if (Sheaden->nane aa= 1X-Gn-Opioinal-To') {S-atches = Sthis->natchesExoectedRecinient(Sheaden->vallue.ScxoectedAlins. ScxocctedHost)ie (1 Shatches)Santteze Pethy renouino muusctad content on ocoindOriginalTo = explode(*+', Sheader-›value)[0];Loa:-intolHextRelavSeryscel Retused nessagp"message_id' => Smessageld,'orsosnal to sanitazedl Ssans ta zeddcs osinaittolreturn Smatches:Log::warning* TextRelayService) Refused message: missing X-6m-Oniginal-To"message id' => Snessageld.catch Exception $e)Log::error(* TextRelayService Failed to inspect nessaowaitaronese1Alsasf (Sheaden-snace eue tTor) dta= Shesder-suaimaSrecipient = Soriginalto ?? Sto:if (Srecipient [== null) €Smatches = Sthis->matchesExpectedRecipient(Srecipient, SexpectedAlias, SexpectedHost)if (l Snatches) €I Sanitize PiT by removing olus-taa content for loggingSsanitizedRecipient = exolode(*+* Srecioient) (el.log:sinfo(* (TextRelavService) Refused nessage'. "'nessace id' => Smessaceid'oniginal to sanitized' => SsanitizedRecioient1):neturn Snatches:Loa:[EMAIL]. missina X-6n-0cfoinal-to and to neadenst"massage d' e> shessageld1catch (NSycention se)!Log::error('[TextRelayService) Failed to inspect message', [TAGEnactArem ditee sooI* Windsurf Teams 19:1 UTF-8 24 space:...
|
NULL
|
-8009994318258999510
|
NULL
|
click
|
ocr
|
NULL
|
rapstomViewNeweNNCCoocKetuciolool.WindowFV faVsco. rapstomViewNeweNNCCoocKetuciolool.WindowFV faVsco.sv$ JY-20915-fix-missing-header-text-relayProinet vAcmelpn© SyncMailbox.php© ServiceTest.php© SyncBatchRedisServiceRaseSercetest oho©) TextRelayServiceTest.php x php fiminny.php3.env.productioncionec CachedCrmServiceDecoradectarelScracrcypes="© CrmActivityServiceTest.ptc CrmConfigurationSettingsnanesoace vests Unzc services naze"CrmObiectsResolverTest.rDefaultProspectSearchStusc 6000Le Servzce 6302l a$ 16000Lc60322EmailHelperTest.phousc 6000Lc Service 6n022 nessaoc as 6ma2chessagc:© FieldValueConverterTest.gusc 6000Le Service 6na2, hessagcPana© LayoutManagerTest.phduse 6000le Service 6na2 nessagePar HeadersC MigrateProviderServiceTeuse zuminare Suppor Facades contar© OpportunityActivityMatchiuse uminate Suppors Facades, Loa:OpportunitySyncStrateay© ProspectCacheTest.phouse miinny Serusces.nu tey cel aysenuicosuse PHPUn1t\Franework\Attributes (coversclass:c Prosoccsearchstratcoyt© ProviderRecistryTest.phouse Prpnr Eranemorkrhures hara?rousderuse ests iestlase,c Recordseleclortestonouse Pe ections ass) ResoivecomoanvNamesvluse HockenyLochlehnotchandes 5iasenullocallaoc@.liminnv?ebuCommand.cho.ano/Console/Commandsphe logging.php config© PlaybackService.php app/Services© TextRelayService.php app/Services/Mai© TextRelayServiceTest.php tests/Unit/Services/Mail• Uoversionad Filas 9 GlaelTextRelayServiceTestDockerfile= laravel.log X SF (iminny@localhost)A console (STAGING]HS Jocal (jiminny@alocalhost)console [PROD# consolefFuloo nu co moy tirotur+0.rner hensewiettix @TextRelayServiceTest.phpTnodgnttor 1sResdTextRelayScrvico est.oho #L154-188Thought for 1seexKeayeMicesthontThoughtsRead TextRelayServiceTest.php #L150-199thought for 16Sasrched testistor@urrent Environment.allsBackio ohesder in teste/UntSamices Moiln extRelay Semvice est.oho.Ask annineeo<› Code SW-Td→ aSide-by-side viewerö 828865ed app/Services/Mail/TextRelayService.phgDo not ignoreyHighlight wordsx1gtry 4Snessage = Sservice->users nessages->get(Smailbox, Snessageld):Sheaders = Ssessage->getPayLoad@)->getHeaders@>forcach (Sheaders as Sheader)3if (Sheaden->nane aa= 1X-Gn-Opioinal-To') {S-atches = Sthis->natchesExoectedRecinient(Sheaden->vallue.ScxoectedAlins. ScxocctedHost)ie (1 Shatches)Santteze Pethy renouino muusctad content on ocoindOriginalTo = explode(*+', Sheader-›value)[0];Loa:-intolHextRelavSeryscel Retused nessagp"message_id' => Smessageld,'orsosnal to sanitazedl Ssans ta zeddcs osinaittolreturn Smatches:Log::warning* TextRelayService) Refused message: missing X-6m-Oniginal-To"message id' => Snessageld.catch Exception $e)Log::error(* TextRelayService Failed to inspect nessaowaitaronese1Alsasf (Sheaden-snace eue tTor) dta= Shesder-suaimaSrecipient = Soriginalto ?? Sto:if (Srecipient [== null) €Smatches = Sthis->matchesExpectedRecipient(Srecipient, SexpectedAlias, SexpectedHost)if (l Snatches) €I Sanitize PiT by removing olus-taa content for loggingSsanitizedRecipient = exolode(*+* Srecioient) (el.log:sinfo(* (TextRelavService) Refused nessage'. "'nessace id' => Smessaceid'oniginal to sanitized' => SsanitizedRecioient1):neturn Snatches:Loa:[EMAIL]. missina X-6n-0cfoinal-to and to neadenst"massage d' e> shessageld1catch (NSycention se)!Log::error('[TextRelayService) Failed to inspect message', [TAGEnactArem ditee sooI* Windsurf Teams 19:1 UTF-8 24 space:...
|
81588
|
NULL
|
NULL
|
NULL
|
|
81588
|
2833
|
9
|
2026-05-28T08:30:51.308687+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957051308_m2.jpg...
|
PhpStorm
|
faVsco.js – TextRelayServiceTest.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20915-fix-missing-head Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
13
64
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Mail;
use Google\Service\Gmail as GoogleGmail;
use Google\Service\Gmail\Message as GmailMessage;
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\Mail\TextRelayService;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
use ReflectionClass;
use Mockery;
#[CoversClass(TextRelayService::class)]
class TextRelayServiceTest extends TestCase
{
public static function environmentProvider(): array
{
return [
'eu_environment' => ['eu', 'catch-all-eu', '[EMAIL]'],
'us_environment' => ['us', 'catch-all', '[EMAIL]'],
'other_environment_defaults_to_us' => ['other', 'catch-all', '[EMAIL]'],
'eu_environment_with_plus_tag' => ['eu', 'catch-all-eu', '[EMAIL]'],
'us_environment_with_plus_tag' => ['us', 'catch-all', '[EMAIL]'],
];
}
protected function setUp(): void
{
parent::setUp();
}
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
#[DataProvider('environmentProvider')]
public function testIsForCurrentEnvironmentWithMatchingXGmOriginalToHeader(
string $deployRegion,
string $expectedAlias,
string $recipient
): void {
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = $recipient;
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');
$this->assertTrue($result);
}
#[DataProvider('environmentProvider')]
public function testIsForCurrentEnvironmentWithNonMatchingXGmOriginalToHeader(
string $deployRegion,
string $expectedAlias,
string $recipient
): void {
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentLogsRefusedMessage(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Refused message', [
'message_id' => 'msg123',
'original_to_sanitized' => '[EMAIL]',
]);
Log::shouldReceive('error')
->never()
->with('[TextRelayService] Failed to inspect message', \Mockery::type('array'));
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentFallsBackToToHeader(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertTrue($result);
}
public function testIsForCurrentEnvironmentWithEmptyHeaders(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$payload->method('getHeaders')->willReturn([]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
Log::shouldReceive('warning')
->once()
->with('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => 'msg123',
]);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentWithException(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')
->willThrowException(new \Exception('Gmail API error'));
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
Log::shouldReceive('error')
->once()
->with('[TextRelayService] Failed to inspect message', [
'message_id' => 'msg123',
'exception' => 'Gmail API error',
]);
\Sentry::shouldReceive('captureException')
->once()
->with(\Mockery::type(\Exception::class));
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentRejectsWrongHostWithMatchingPlusTag(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testSyncUsesEuAliasForEuRegion(): void
{
Config::set('jiminny.deploy_region', 'eu');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
$service = $this->createTextRelayService();
$gmailService = $this->createMock(GoogleGmail::class);
$historyResponse = $this->createMock(\Google\Service\Gmail\ListHistoryResponse::class);
$historyResponse->historyId = 12345;
$historyResponse->method('getHistory')->willReturn([]);
$historyResponse->method('getNextPageToken')->willReturn(null);
$usersHistory = $this->createMock(\Google\Service\Gmail\Resource\UsersHistory::class);
$usersHistory->method('listUsersHistory')->willReturn($historyResponse);
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
\Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
}
public function testSyncUsesUsAliasForUsRegion(): void
{
Config::set('jiminny.deploy_region', 'us');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
$service = $this->createTextRelayService();
$gmailService = $this->createMock(GoogleGmail::class);
$historyResponse = $this->createMock(\Google\Service\Gmail\ListHistoryResponse::class);
$historyResponse->historyId = 12345;
$historyResponse->method('getHistory')->willReturn([]);
$historyResponse->method('getNextPageToken')->willReturn(null);
$usersHistory = $this->createMock(\Google\Service\Gmail\Resource\UsersHistory::class);
$usersHistory->method('listUsersHistory')->willReturn($historyResponse);
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
\Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
}
public function testSyncWithEnhancedLogging(): void
{
Config::set('jiminny.deploy_region', 'eu');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
// Create a partial mock of the service to control the getHistory method
$service = $this->createPartialMock(TextRelayService::class, ['getHistory']);
// Mock getHistory to return empty array (no messages to process)
$service->method('getHistory')->willReturn([]);
// Mock Log facade - channel() returns a driver mock that accepts any method call
$logDriver = \Mockery::mock();
$logDriver->shouldIgnoreMissing();
Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Starting sync', [
'mailbox' => '[EMAIL]',
'expected_alias' => 'catch-all-eu',
'expected_host' => 'txt.jiminny.com',
]);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Sync completed', [
'mailbox' => '[EMAIL]',
'messages_processed' => 0,
'message_ids' => [],
]);
// Mock Sentry to prevent actual error reporting
\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testSyncWithRefusedMessageLogs(): void
{
Config::set('jiminny.deploy_region', 'eu');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
// Create a partial mock of the service to control the getHistory method
$service = $this->createPartialMock(TextRelayService::class, ['getHistory']);
// Mock getHistory to return empty array (no messages to process)
$service->method('getHistory')->willReturn([]);
// Mock Log facade - channel() returns a driver mock that accepts any method call
$logDriver = \Mockery::mock();
$logDriver->shouldIgnoreMissing();
Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Starting sync', [
'mailbox' => '[EMAIL]',
'expected_alias' => 'catch-all-eu',
'expected_host' => 'txt.jiminny.com',
]);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Sync completed', [
'mailbox' => '[EMAIL]',
'messages_processed' => 0,
'message_ids' => [],
]);
// Mock Sentry to prevent actual error reporting
\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetHistoryLogsErrorOnException(): void
{
Config::set('jiminny.google_text_relay_topic', 'test-topic');
Config::set('jiminny.google_text_user', '[EMAIL]');
$service = $this->createTextRelayService();
$gmailService = $this->createMock(GoogleGmail::class);
$usersHistory = $this->createMock(\Google\Service\Gmail\Resource\UsersHistory::class);
$usersHistory->method('listUsersHistory')
->willThrowException(new \Exception('Gmail API error'));
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
$logDriver = \Mockery::mock();
$logDriver->shouldIgnoreMissing();
Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);
Log::shouldReceive('error')
->once()
->with('[TextRelayService] Failed to fetch Gmail history', [
'exception' => 'Gmail API error',
]);
\Sentry::shouldReceive('captureException')
->once()
->with(\Mockery::type(\Exception::class));
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('getHistory');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
private function createTextRelayService(): TextRelayService
{
$service = new class () extends TextRelayService {
public function __construct()
{
}
};
return $service;
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JY-20915-fix-missing-header-text-relay, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09740692,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20915-fix-missing-header-text-relay","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8374335,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"TextRelayServiceTest","depth":6,"bounds":{"left":0.85272604,"top":0.019952115,"width":0.062832445,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'TextRelayServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"13","depth":4,"bounds":{"left":0.37333778,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"64","depth":4,"bounds":{"left":0.3849734,"top":0.10055866,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39694148,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40425533,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Mail;\n\nuse Google\\Service\\Gmail as GoogleGmail;\nuse Google\\Service\\Gmail\\Message as GmailMessage;\nuse Google\\Service\\Gmail\\MessagePart;\nuse Google\\Service\\Gmail\\MessagePartHeader;\nuse Illuminate\\Support\\Facades\\Config;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\Mail\\TextRelayService;\nuse PHPUnit\\Framework\\Attributes\\CoversClass;\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse Tests\\TestCase;\nuse ReflectionClass;\nuse Mockery;\n\n#[CoversClass(TextRelayService::class)]\nclass TextRelayServiceTest extends TestCase\n{\n public static function environmentProvider(): array\n {\n return [\n 'eu_environment' => ['eu', 'catch-all-eu', 'catch-all-eu@txt.jiminny.com'],\n 'us_environment' => ['us', 'catch-all', 'catch-all@txt.jiminny.com'],\n 'other_environment_defaults_to_us' => ['other', 'catch-all', 'catch-all@txt.jiminny.com'],\n 'eu_environment_with_plus_tag' => ['eu', 'catch-all-eu', 'catch-all-eu+447700152706.447893937879.3ndZZBqjRo@txt.jiminny.com'],\n 'us_environment_with_plus_tag' => ['us', 'catch-all', 'catch-all+447700152706.447893937879.3ndZZBqjRo@txt.jiminny.com'],\n ];\n }\n\n protected function setUp(): void\n {\n parent::setUp();\n }\n\n protected function tearDown(): void\n {\n Mockery::close();\n parent::tearDown();\n }\n\n #[DataProvider('environmentProvider')]\n public function testIsForCurrentEnvironmentWithMatchingXGmOriginalToHeader(\n string $deployRegion,\n string $expectedAlias,\n string $recipient\n ): void {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = $recipient;\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');\n\n $this->assertTrue($result);\n }\n\n #[DataProvider('environmentProvider')]\n public function testIsForCurrentEnvironmentWithNonMatchingXGmOriginalToHeader(\n string $deployRegion,\n string $expectedAlias,\n string $recipient\n ): void {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = 'other@txt.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentLogsRefusedMessage(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = 'wrong@txt.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Refused message', [\n 'message_id' => 'msg123',\n 'original_to_sanitized' => 'wrong@txt.jiminny.com',\n ]);\n\n Log::shouldReceive('error')\n ->never()\n ->with('[TextRelayService] Failed to inspect message', \\Mockery::type('array'));\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentFallsBackToToHeader(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'To';\n $header->value = 'catch-all@txt.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertTrue($result);\n }\n\n public function testIsForCurrentEnvironmentWithEmptyHeaders(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n\n $payload->method('getHeaders')->willReturn([]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n Log::shouldReceive('warning')\n ->once()\n ->with('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => 'msg123',\n ]);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentWithException(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')\n ->willThrowException(new \\Exception('Gmail API error'));\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n Log::shouldReceive('error')\n ->once()\n ->with('[TextRelayService] Failed to inspect message', [\n 'message_id' => 'msg123',\n 'exception' => 'Gmail API error',\n ]);\n\n \\Sentry::shouldReceive('captureException')\n ->once()\n ->with(\\Mockery::type(\\Exception::class));\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentRejectsWrongHostWithMatchingPlusTag(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = 'catch-all+447700152706.447893937879.3ndZZBqjRo@txt.staging.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testSyncUsesEuAliasForEuRegion(): void\n {\n Config::set('jiminny.deploy_region', 'eu');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n $service = $this->createTextRelayService();\n $gmailService = $this->createMock(GoogleGmail::class);\n\n $historyResponse = $this->createMock(\\Google\\Service\\Gmail\\ListHistoryResponse::class);\n $historyResponse->historyId = 12345;\n $historyResponse->method('getHistory')->willReturn([]);\n $historyResponse->method('getNextPageToken')->willReturn(null);\n\n $usersHistory = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersHistory::class);\n $usersHistory->method('listUsersHistory')->willReturn($historyResponse);\n $gmailService->users_history = $usersHistory;\n\n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n }\n\n public function testSyncUsesUsAliasForUsRegion(): void\n {\n Config::set('jiminny.deploy_region', 'us');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n $service = $this->createTextRelayService();\n $gmailService = $this->createMock(GoogleGmail::class);\n\n $historyResponse = $this->createMock(\\Google\\Service\\Gmail\\ListHistoryResponse::class);\n $historyResponse->historyId = 12345;\n $historyResponse->method('getHistory')->willReturn([]);\n $historyResponse->method('getNextPageToken')->willReturn(null);\n\n $usersHistory = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersHistory::class);\n $usersHistory->method('listUsersHistory')->willReturn($historyResponse);\n $gmailService->users_history = $usersHistory;\n\n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n }\n\n public function testSyncWithEnhancedLogging(): void\n {\n Config::set('jiminny.deploy_region', 'eu');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n\n // Mock Log facade - channel() returns a driver mock that accepts any method call\n $logDriver = \\Mockery::mock();\n $logDriver->shouldIgnoreMissing();\n Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Starting sync', [\n 'mailbox' => 'test@example.com',\n 'expected_alias' => 'catch-all-eu',\n 'expected_host' => 'txt.jiminny.com',\n ]);\n\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Sync completed', [\n 'mailbox' => 'test@example.com',\n 'messages_processed' => 0,\n 'message_ids' => [],\n ]);\n\n // Mock Sentry to prevent actual error reporting\n \\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testSyncWithRefusedMessageLogs(): void\n {\n Config::set('jiminny.deploy_region', 'eu');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n\n // Mock Log facade - channel() returns a driver mock that accepts any method call\n $logDriver = \\Mockery::mock();\n $logDriver->shouldIgnoreMissing();\n Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Starting sync', [\n 'mailbox' => 'test@example.com',\n 'expected_alias' => 'catch-all-eu',\n 'expected_host' => 'txt.jiminny.com',\n ]);\n\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Sync completed', [\n 'mailbox' => 'test@example.com',\n 'messages_processed' => 0,\n 'message_ids' => [],\n ]);\n\n // Mock Sentry to prevent actual error reporting\n \\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetHistoryLogsErrorOnException(): void\n {\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n Config::set('jiminny.google_text_user', 'test@example.com');\n\n $service = $this->createTextRelayService();\n $gmailService = $this->createMock(GoogleGmail::class);\n\n $usersHistory = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersHistory::class);\n $usersHistory->method('listUsersHistory')\n ->willThrowException(new \\Exception('Gmail API error'));\n $gmailService->users_history = $usersHistory;\n\n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n\n $logDriver = \\Mockery::mock();\n $logDriver->shouldIgnoreMissing();\n Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);\n Log::shouldReceive('error')\n ->once()\n ->with('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => 'Gmail API error',\n ]);\n\n \\Sentry::shouldReceive('captureException')\n ->once()\n ->with(\\Mockery::type(\\Exception::class));\n\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('getHistory');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n private function createTextRelayService(): TextRelayService\n {\n $service = new class () extends TextRelayService {\n public function __construct()\n {\n }\n };\n\n return $service;\n }\n\n}","depth":4,"bounds":{"left":0.13630319,"top":0.09736632,"width":0.27493352,"height":0.90263367},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Tests\\Unit\\Services\\Mail;\n\nuse Google\\Service\\Gmail as GoogleGmail;\nuse Google\\Service\\Gmail\\Message as GmailMessage;\nuse Google\\Service\\Gmail\\MessagePart;\nuse Google\\Service\\Gmail\\MessagePartHeader;\nuse Illuminate\\Support\\Facades\\Config;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\Mail\\TextRelayService;\nuse PHPUnit\\Framework\\Attributes\\CoversClass;\nuse PHPUnit\\Framework\\Attributes\\DataProvider;\nuse Tests\\TestCase;\nuse ReflectionClass;\nuse Mockery;\n\n#[CoversClass(TextRelayService::class)]\nclass TextRelayServiceTest extends TestCase\n{\n public static function environmentProvider(): array\n {\n return [\n 'eu_environment' => ['eu', 'catch-all-eu', 'catch-all-eu@txt.jiminny.com'],\n 'us_environment' => ['us', 'catch-all', 'catch-all@txt.jiminny.com'],\n 'other_environment_defaults_to_us' => ['other', 'catch-all', 'catch-all@txt.jiminny.com'],\n 'eu_environment_with_plus_tag' => ['eu', 'catch-all-eu', 'catch-all-eu+447700152706.447893937879.3ndZZBqjRo@txt.jiminny.com'],\n 'us_environment_with_plus_tag' => ['us', 'catch-all', 'catch-all+447700152706.447893937879.3ndZZBqjRo@txt.jiminny.com'],\n ];\n }\n\n protected function setUp(): void\n {\n parent::setUp();\n }\n\n protected function tearDown(): void\n {\n Mockery::close();\n parent::tearDown();\n }\n\n #[DataProvider('environmentProvider')]\n public function testIsForCurrentEnvironmentWithMatchingXGmOriginalToHeader(\n string $deployRegion,\n string $expectedAlias,\n string $recipient\n ): void {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = $recipient;\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');\n\n $this->assertTrue($result);\n }\n\n #[DataProvider('environmentProvider')]\n public function testIsForCurrentEnvironmentWithNonMatchingXGmOriginalToHeader(\n string $deployRegion,\n string $expectedAlias,\n string $recipient\n ): void {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = 'other@txt.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentLogsRefusedMessage(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = 'wrong@txt.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Refused message', [\n 'message_id' => 'msg123',\n 'original_to_sanitized' => 'wrong@txt.jiminny.com',\n ]);\n\n Log::shouldReceive('error')\n ->never()\n ->with('[TextRelayService] Failed to inspect message', \\Mockery::type('array'));\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentFallsBackToToHeader(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'To';\n $header->value = 'catch-all@txt.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertTrue($result);\n }\n\n public function testIsForCurrentEnvironmentWithEmptyHeaders(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n\n $payload->method('getHeaders')->willReturn([]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n Log::shouldReceive('warning')\n ->once()\n ->with('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => 'msg123',\n ]);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentWithException(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')\n ->willThrowException(new \\Exception('Gmail API error'));\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n Log::shouldReceive('error')\n ->once()\n ->with('[TextRelayService] Failed to inspect message', [\n 'message_id' => 'msg123',\n 'exception' => 'Gmail API error',\n ]);\n\n \\Sentry::shouldReceive('captureException')\n ->once()\n ->with(\\Mockery::type(\\Exception::class));\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testIsForCurrentEnvironmentRejectsWrongHostWithMatchingPlusTag(): void\n {\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n\n $message = $this->createMock(GmailMessage::class);\n $payload = $this->createMock(MessagePart::class);\n $header = $this->createMock(MessagePartHeader::class);\n\n $header->name = 'X-Gm-Original-To';\n $header->value = 'catch-all+447700152706.447893937879.3ndZZBqjRo@txt.staging.jiminny.com';\n\n $payload->method('getHeaders')->willReturn([$header]);\n $message->method('getPayload')->willReturn($payload);\n\n $usersMessages = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersMessages::class);\n $usersMessages->method('get')->willReturn($message);\n\n $gmailService = $this->createMock(GoogleGmail::class);\n $gmailService->users_messages = $usersMessages;\n\n $service = $this->createTextRelayService();\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('isForCurrentEnvironment');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');\n\n $this->assertFalse($result);\n }\n\n public function testSyncUsesEuAliasForEuRegion(): void\n {\n Config::set('jiminny.deploy_region', 'eu');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n $service = $this->createTextRelayService();\n $gmailService = $this->createMock(GoogleGmail::class);\n\n $historyResponse = $this->createMock(\\Google\\Service\\Gmail\\ListHistoryResponse::class);\n $historyResponse->historyId = 12345;\n $historyResponse->method('getHistory')->willReturn([]);\n $historyResponse->method('getNextPageToken')->willReturn(null);\n\n $usersHistory = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersHistory::class);\n $usersHistory->method('listUsersHistory')->willReturn($historyResponse);\n $gmailService->users_history = $usersHistory;\n\n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n }\n\n public function testSyncUsesUsAliasForUsRegion(): void\n {\n Config::set('jiminny.deploy_region', 'us');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n $service = $this->createTextRelayService();\n $gmailService = $this->createMock(GoogleGmail::class);\n\n $historyResponse = $this->createMock(\\Google\\Service\\Gmail\\ListHistoryResponse::class);\n $historyResponse->historyId = 12345;\n $historyResponse->method('getHistory')->willReturn([]);\n $historyResponse->method('getNextPageToken')->willReturn(null);\n\n $usersHistory = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersHistory::class);\n $usersHistory->method('listUsersHistory')->willReturn($historyResponse);\n $gmailService->users_history = $usersHistory;\n\n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n \\Cache::shouldReceive('put')->with('test-topic', 12345, \\Mockery::on(fn ($expires) => $expires instanceof \\Carbon\\Carbon));\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n }\n\n public function testSyncWithEnhancedLogging(): void\n {\n Config::set('jiminny.deploy_region', 'eu');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n\n // Mock Log facade - channel() returns a driver mock that accepts any method call\n $logDriver = \\Mockery::mock();\n $logDriver->shouldIgnoreMissing();\n Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Starting sync', [\n 'mailbox' => 'test@example.com',\n 'expected_alias' => 'catch-all-eu',\n 'expected_host' => 'txt.jiminny.com',\n ]);\n\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Sync completed', [\n 'mailbox' => 'test@example.com',\n 'messages_processed' => 0,\n 'message_ids' => [],\n ]);\n\n // Mock Sentry to prevent actual error reporting\n \\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testSyncWithRefusedMessageLogs(): void\n {\n Config::set('jiminny.deploy_region', 'eu');\n Config::set('jiminny.google_text_host', 'txt.jiminny.com');\n Config::set('jiminny.google_text_user', 'test@example.com');\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n\n // Create a partial mock of the service to control the getHistory method\n $service = $this->createPartialMock(TextRelayService::class, ['getHistory']);\n\n // Mock getHistory to return empty array (no messages to process)\n $service->method('getHistory')->willReturn([]);\n\n // Mock Log facade - channel() returns a driver mock that accepts any method call\n $logDriver = \\Mockery::mock();\n $logDriver->shouldIgnoreMissing();\n Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Starting sync', [\n 'mailbox' => 'test@example.com',\n 'expected_alias' => 'catch-all-eu',\n 'expected_host' => 'txt.jiminny.com',\n ]);\n\n Log::shouldReceive('info')\n ->once()\n ->with('[TextRelayService] Sync completed', [\n 'mailbox' => 'test@example.com',\n 'messages_processed' => 0,\n 'message_ids' => [],\n ]);\n\n // Mock Sentry to prevent actual error reporting\n \\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();\n\n $reflection = new ReflectionClass($service);\n $syncMethod = $reflection->getMethod('sync');\n $syncMethod->setAccessible(true);\n\n $result = $syncMethod->invoke($service);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n public function testGetHistoryLogsErrorOnException(): void\n {\n Config::set('jiminny.google_text_relay_topic', 'test-topic');\n Config::set('jiminny.google_text_user', 'test@example.com');\n\n $service = $this->createTextRelayService();\n $gmailService = $this->createMock(GoogleGmail::class);\n\n $usersHistory = $this->createMock(\\Google\\Service\\Gmail\\Resource\\UsersHistory::class);\n $usersHistory->method('listUsersHistory')\n ->willThrowException(new \\Exception('Gmail API error'));\n $gmailService->users_history = $usersHistory;\n\n \\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);\n\n $logDriver = \\Mockery::mock();\n $logDriver->shouldIgnoreMissing();\n Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);\n Log::shouldReceive('error')\n ->once()\n ->with('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => 'Gmail API error',\n ]);\n\n \\Sentry::shouldReceive('captureException')\n ->once()\n ->with(\\Mockery::type(\\Exception::class));\n\n $reflection = new ReflectionClass($service);\n $method = $reflection->getMethod('getHistory');\n $method->setAccessible(true);\n\n $result = $method->invoke($service, $gmailService);\n\n $this->assertIsArray($result);\n $this->assertEmpty($result);\n }\n\n private function createTextRelayService(): TextRelayService\n {\n $service = new class () extends TextRelayService {\n public function __construct()\n {\n }\n };\n\n return $service;\n }\n\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"","depth":4,"bounds":{"left":0.43051863,"top":0.09736632,"width":0.32280585,"height":0.31683958},"on_screen":true,"value":"","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3656098470495859466
|
-2005170756397648973
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20915-fix-missing-head Project: faVsco.js, menu
JY-20915-fix-missing-header-text-relay, menu
Start Listening for PHP Debug Connections
TextRelayServiceTest
Run 'TextRelayServiceTest'
Debug 'TextRelayServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
13
64
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Tests\Unit\Services\Mail;
use Google\Service\Gmail as GoogleGmail;
use Google\Service\Gmail\Message as GmailMessage;
use Google\Service\Gmail\MessagePart;
use Google\Service\Gmail\MessagePartHeader;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\Mail\TextRelayService;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
use ReflectionClass;
use Mockery;
#[CoversClass(TextRelayService::class)]
class TextRelayServiceTest extends TestCase
{
public static function environmentProvider(): array
{
return [
'eu_environment' => ['eu', 'catch-all-eu', '[EMAIL]'],
'us_environment' => ['us', 'catch-all', '[EMAIL]'],
'other_environment_defaults_to_us' => ['other', 'catch-all', '[EMAIL]'],
'eu_environment_with_plus_tag' => ['eu', 'catch-all-eu', '[EMAIL]'],
'us_environment_with_plus_tag' => ['us', 'catch-all', '[EMAIL]'],
];
}
protected function setUp(): void
{
parent::setUp();
}
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
#[DataProvider('environmentProvider')]
public function testIsForCurrentEnvironmentWithMatchingXGmOriginalToHeader(
string $deployRegion,
string $expectedAlias,
string $recipient
): void {
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = $recipient;
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');
$this->assertTrue($result);
}
#[DataProvider('environmentProvider')]
public function testIsForCurrentEnvironmentWithNonMatchingXGmOriginalToHeader(
string $deployRegion,
string $expectedAlias,
string $recipient
): void {
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', $expectedAlias, 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentLogsRefusedMessage(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Refused message', [
'message_id' => 'msg123',
'original_to_sanitized' => '[EMAIL]',
]);
Log::shouldReceive('error')
->never()
->with('[TextRelayService] Failed to inspect message', \Mockery::type('array'));
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentFallsBackToToHeader(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertTrue($result);
}
public function testIsForCurrentEnvironmentWithEmptyHeaders(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$payload->method('getHeaders')->willReturn([]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
Log::shouldReceive('warning')
->once()
->with('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => 'msg123',
]);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentWithException(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')
->willThrowException(new \Exception('Gmail API error'));
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
Log::shouldReceive('error')
->once()
->with('[TextRelayService] Failed to inspect message', [
'message_id' => 'msg123',
'exception' => 'Gmail API error',
]);
\Sentry::shouldReceive('captureException')
->once()
->with(\Mockery::type(\Exception::class));
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testIsForCurrentEnvironmentRejectsWrongHostWithMatchingPlusTag(): void
{
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
$message = $this->createMock(GmailMessage::class);
$payload = $this->createMock(MessagePart::class);
$header = $this->createMock(MessagePartHeader::class);
$header->name = 'X-Gm-Original-To';
$header->value = '[EMAIL]';
$payload->method('getHeaders')->willReturn([$header]);
$message->method('getPayload')->willReturn($payload);
$usersMessages = $this->createMock(\Google\Service\Gmail\Resource\UsersMessages::class);
$usersMessages->method('get')->willReturn($message);
$gmailService = $this->createMock(GoogleGmail::class);
$gmailService->users_messages = $usersMessages;
$service = $this->createTextRelayService();
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('isForCurrentEnvironment');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService, 'catch-all', 'msg123', 'catch-all', 'txt.jiminny.com');
$this->assertFalse($result);
}
public function testSyncUsesEuAliasForEuRegion(): void
{
Config::set('jiminny.deploy_region', 'eu');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
$service = $this->createTextRelayService();
$gmailService = $this->createMock(GoogleGmail::class);
$historyResponse = $this->createMock(\Google\Service\Gmail\ListHistoryResponse::class);
$historyResponse->historyId = 12345;
$historyResponse->method('getHistory')->willReturn([]);
$historyResponse->method('getNextPageToken')->willReturn(null);
$usersHistory = $this->createMock(\Google\Service\Gmail\Resource\UsersHistory::class);
$usersHistory->method('listUsersHistory')->willReturn($historyResponse);
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
\Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
}
public function testSyncUsesUsAliasForUsRegion(): void
{
Config::set('jiminny.deploy_region', 'us');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
$service = $this->createTextRelayService();
$gmailService = $this->createMock(GoogleGmail::class);
$historyResponse = $this->createMock(\Google\Service\Gmail\ListHistoryResponse::class);
$historyResponse->historyId = 12345;
$historyResponse->method('getHistory')->willReturn([]);
$historyResponse->method('getNextPageToken')->willReturn(null);
$usersHistory = $this->createMock(\Google\Service\Gmail\Resource\UsersHistory::class);
$usersHistory->method('listUsersHistory')->willReturn($historyResponse);
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
\Cache::shouldReceive('put')->with('test-topic', 12345, \Mockery::on(fn ($expires) => $expires instanceof \Carbon\Carbon));
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
}
public function testSyncWithEnhancedLogging(): void
{
Config::set('jiminny.deploy_region', 'eu');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
// Create a partial mock of the service to control the getHistory method
$service = $this->createPartialMock(TextRelayService::class, ['getHistory']);
// Mock getHistory to return empty array (no messages to process)
$service->method('getHistory')->willReturn([]);
// Mock Log facade - channel() returns a driver mock that accepts any method call
$logDriver = \Mockery::mock();
$logDriver->shouldIgnoreMissing();
Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Starting sync', [
'mailbox' => '[EMAIL]',
'expected_alias' => 'catch-all-eu',
'expected_host' => 'txt.jiminny.com',
]);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Sync completed', [
'mailbox' => '[EMAIL]',
'messages_processed' => 0,
'message_ids' => [],
]);
// Mock Sentry to prevent actual error reporting
\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testSyncWithRefusedMessageLogs(): void
{
Config::set('jiminny.deploy_region', 'eu');
Config::set('jiminny.google_text_host', 'txt.jiminny.com');
Config::set('jiminny.google_text_user', '[EMAIL]');
Config::set('jiminny.google_text_relay_topic', 'test-topic');
// Create a partial mock of the service to control the getHistory method
$service = $this->createPartialMock(TextRelayService::class, ['getHistory']);
// Mock getHistory to return empty array (no messages to process)
$service->method('getHistory')->willReturn([]);
// Mock Log facade - channel() returns a driver mock that accepts any method call
$logDriver = \Mockery::mock();
$logDriver->shouldIgnoreMissing();
Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Starting sync', [
'mailbox' => '[EMAIL]',
'expected_alias' => 'catch-all-eu',
'expected_host' => 'txt.jiminny.com',
]);
Log::shouldReceive('info')
->once()
->with('[TextRelayService] Sync completed', [
'mailbox' => '[EMAIL]',
'messages_processed' => 0,
'message_ids' => [],
]);
// Mock Sentry to prevent actual error reporting
\Sentry::shouldReceive('captureException')->zeroOrMoreTimes();
$reflection = new ReflectionClass($service);
$syncMethod = $reflection->getMethod('sync');
$syncMethod->setAccessible(true);
$result = $syncMethod->invoke($service);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
public function testGetHistoryLogsErrorOnException(): void
{
Config::set('jiminny.google_text_relay_topic', 'test-topic');
Config::set('jiminny.google_text_user', '[EMAIL]');
$service = $this->createTextRelayService();
$gmailService = $this->createMock(GoogleGmail::class);
$usersHistory = $this->createMock(\Google\Service\Gmail\Resource\UsersHistory::class);
$usersHistory->method('listUsersHistory')
->willThrowException(new \Exception('Gmail API error'));
$gmailService->users_history = $usersHistory;
\Cache::shouldReceive('get')->with('test-topic')->andReturn(10000);
$logDriver = \Mockery::mock();
$logDriver->shouldIgnoreMissing();
Log::shouldReceive('channel')->zeroOrMoreTimes()->andReturn($logDriver);
Log::shouldReceive('error')
->once()
->with('[TextRelayService] Failed to fetch Gmail history', [
'exception' => 'Gmail API error',
]);
\Sentry::shouldReceive('captureException')
->once()
->with(\Mockery::type(\Exception::class));
$reflection = new ReflectionClass($service);
$method = $reflection->getMethod('getHistory');
$method->setAccessible(true);
$result = $method->invoke($service, $gmailService);
$this->assertIsArray($result);
$this->assertEmpty($result);
}
private function createTextRelayService(): TextRelayService
{
$service = new class () extends TextRelayService {
public function __construct()
{
}
};
return $service;
}
}
Code changed:
Hide
Sync Changes
Hide This Notification
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
81587
|
2832
|
9
|
2026-05-28T08:30:48.975547+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957048975_m1.jpg...
|
PhpStorm
|
faVsco.js – TextRelayServiceTest.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp‹$0A-zshscreenpipe"DOCKER₴81DEV (docker)worker-calendar:worker-calendar_00:startedworker-conferences:worker-conferences_00: startedworker-crm-sync:worker-crm-sync_00: startedworker-crm-update:worker-crm-update_00: startedworker-download:worker-download_00:startedworker-emails:worker-emails_00:startedworker-es-update:worker-es-update_00:startedworker-nudges:worker-nudges_00: started₴2-zsh84-zshWhat's next:Try Docker Debug forseamless, persistentdebugging tools in any container or image → docker debug docker_lamp_1Learnmoreat [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-fix-missing-header-text-relay) $csfixdocker exec -it docker_lamp_1•/vendor/bin/php-cs-fixer fix--config=.php-cs-fixer.dist.php-v--using-cache=no --diffPHP CS Fixer 3.95.1 Adalbertus by Fabien Potencier, Dariusz Ruminski and contributors.PHP runtime: 8.5.5Loaded config default from ".php-cs-fixer.dist.php".Running analysis on 7 cores with 10 files per process.5698/5698 C80100%*5ec2-user@ip-10-30-1...O 86100% <78• Thu 28 May 11:30:48T81ec2-user@ip-10-30-140-...$7Fixed 0 of 5698 files in 52.731 seconds, 799.06 MB memory usedDetected deprecations in use (they will stop working in next major release):- Rule set"@PHP74Migration" is deprecated. Use"@PHP7x4Migration" instead.- Rule set "@PHP80Migration" is deprecated. Use "@PHP8x®Migration" instead.- Rule set "@PHP81Migration" is deprecated. Use "@PHP8x1Migration" instead.- Rule set "@PHP82Migration" is deprecated. Use "@PHP8x2Migration" instead.- Rule set "@PHP83Migration" is deprecated. Use "@PHP8x3Migration"instead.- Rule set "@PHP84Migration" is deprecated. Use "@PHP8x4Migration" instead.What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image + docker debug docker_lamp_1Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-fix-missing-header-text-relay) $D...
|
NULL
|
4275020144278462491
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp‹$0A-zshscreenpipe"DOCKER₴81DEV (docker)worker-calendar:worker-calendar_00:startedworker-conferences:worker-conferences_00: startedworker-crm-sync:worker-crm-sync_00: startedworker-crm-update:worker-crm-update_00: startedworker-download:worker-download_00:startedworker-emails:worker-emails_00:startedworker-es-update:worker-es-update_00:startedworker-nudges:worker-nudges_00: started₴2-zsh84-zshWhat's next:Try Docker Debug forseamless, persistentdebugging tools in any container or image → docker debug docker_lamp_1Learnmoreat [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-fix-missing-header-text-relay) $csfixdocker exec -it docker_lamp_1•/vendor/bin/php-cs-fixer fix--config=.php-cs-fixer.dist.php-v--using-cache=no --diffPHP CS Fixer 3.95.1 Adalbertus by Fabien Potencier, Dariusz Ruminski and contributors.PHP runtime: 8.5.5Loaded config default from ".php-cs-fixer.dist.php".Running analysis on 7 cores with 10 files per process.5698/5698 C80100%*5ec2-user@ip-10-30-1...O 86100% <78• Thu 28 May 11:30:48T81ec2-user@ip-10-30-140-...$7Fixed 0 of 5698 files in 52.731 seconds, 799.06 MB memory usedDetected deprecations in use (they will stop working in next major release):- Rule set"@PHP74Migration" is deprecated. Use"@PHP7x4Migration" instead.- Rule set "@PHP80Migration" is deprecated. Use "@PHP8x®Migration" instead.- Rule set "@PHP81Migration" is deprecated. Use "@PHP8x1Migration" instead.- Rule set "@PHP82Migration" is deprecated. Use "@PHP8x2Migration" instead.- Rule set "@PHP83Migration" is deprecated. Use "@PHP8x3Migration"instead.- Rule set "@PHP84Migration" is deprecated. Use "@PHP8x4Migration" instead.What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image + docker debug docker_lamp_1Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-fix-missing-header-text-relay) $D...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
81586
|
2833
|
8
|
2026-05-28T08:30:48.871149+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957048871_m2.jpg...
|
PhpStorm
|
Commit Changes
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20915 fix missing header
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"bounds":{"left":0.25531915,"top":0.39185953,"width":0.013962766,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"bounds":{"left":0.58211434,"top":0.15802075,"width":0.022938829,"height":0.027134877},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"bounds":{"left":0.60638297,"top":0.15802075,"width":0.042220745,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"bounds":{"left":0.25664893,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"bounds":{"left":0.26529256,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"bounds":{"left":0.27393618,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"bounds":{"left":0.28490692,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"bounds":{"left":0.56150264,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"bounds":{"left":0.57014626,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.3932846,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"JY-20915 fix missing header","depth":2,"bounds":{"left":0.2556516,"top":0.3008779,"width":0.39261967,"height":0.087789305},"on_screen":true,"value":"JY-20915 fix missing header","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"bounds":{"left":0.25531915,"top":0.28252193,"width":0.03557181,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"bounds":{"left":0.6399601,"top":0.27853152,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Git","depth":2,"bounds":{"left":0.6519282,"top":0.16280925,"width":0.0056515955,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"bounds":{"left":0.6599069,"top":0.18834797,"width":0.014960106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"bounds":{"left":0.67852396,"top":0.18834797,"width":0.09840426,"height":0.013567438},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"bounds":{"left":0.65857714,"top":0.21069433,"width":0.039893616,"height":0.019952115},"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"bounds":{"left":0.65857714,"top":0.23383878,"width":0.042220745,"height":0.019952115},"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"bounds":{"left":0.6519282,"top":0.27613726,"width":0.032247342,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"bounds":{"left":0.65857714,"top":0.29608938,"width":0.04488032,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"bounds":{"left":0.65857714,"top":0.31923383,"width":0.03956117,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"bounds":{"left":0.65857714,"top":0.3423783,"width":0.041888297,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"bounds":{"left":0.65857714,"top":0.36552274,"width":0.044215426,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"bounds":{"left":0.65857714,"top":0.3886672,"width":0.026263298,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.6888298,"top":0.39185953,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"bounds":{"left":0.65857714,"top":0.41181165,"width":0.07247341,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"bounds":{"left":0.65857714,"top":0.4349561,"width":0.05518617,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.71775264,"top":0.43814844,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"bounds":{"left":0.65857714,"top":0.45810056,"width":0.1200133,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"bounds":{"left":0.6519282,"top":0.50039905,"width":0.05418883,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"bounds":{"left":0.65857714,"top":0.5203512,"width":0.036901597,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.6994681,"top":0.5235435,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3961791803949631561
|
-1971084284939974845
|
click
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20915 fix missing header
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile...
|
81585
|
NULL
|
NULL
|
NULL
|
|
81585
|
2833
|
7
|
2026-05-28T08:30:32.835479+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957032835_m2.jpg...
|
PhpStorm
|
Commit Changes
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20915 fix missing header
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
5 differences
828865ed
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedOriginalTo = explode('+', $header->value)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedOriginalTo,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"bounds":{"left":0.25531915,"top":0.39185953,"width":0.013962766,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"bounds":{"left":0.58211434,"top":0.15802075,"width":0.022938829,"height":0.027134877},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"bounds":{"left":0.60638297,"top":0.15802075,"width":0.042220745,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"bounds":{"left":0.25664893,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"bounds":{"left":0.26529256,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"bounds":{"left":0.27393618,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"bounds":{"left":0.28490692,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"bounds":{"left":0.56150264,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"bounds":{"left":0.57014626,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.3932846,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"JY-20915 fix missing header","depth":2,"bounds":{"left":0.2556516,"top":0.3008779,"width":0.39261967,"height":0.087789305},"on_screen":true,"value":"JY-20915 fix missing header","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"bounds":{"left":0.25531915,"top":0.28252193,"width":0.03557181,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"bounds":{"left":0.6399601,"top":0.27853152,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Git","depth":2,"bounds":{"left":0.6519282,"top":0.16280925,"width":0.0056515955,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"bounds":{"left":0.6599069,"top":0.18834797,"width":0.014960106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"bounds":{"left":0.67852396,"top":0.18834797,"width":0.09840426,"height":0.013567438},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"bounds":{"left":0.65857714,"top":0.21069433,"width":0.039893616,"height":0.019952115},"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"bounds":{"left":0.65857714,"top":0.23383878,"width":0.042220745,"height":0.019952115},"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"bounds":{"left":0.6519282,"top":0.27613726,"width":0.032247342,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"bounds":{"left":0.65857714,"top":0.29608938,"width":0.04488032,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"bounds":{"left":0.65857714,"top":0.31923383,"width":0.03956117,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"bounds":{"left":0.65857714,"top":0.3423783,"width":0.041888297,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"bounds":{"left":0.65857714,"top":0.36552274,"width":0.044215426,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"bounds":{"left":0.65857714,"top":0.3886672,"width":0.026263298,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.6888298,"top":0.39185953,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"bounds":{"left":0.65857714,"top":0.41181165,"width":0.07247341,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"bounds":{"left":0.65857714,"top":0.4349561,"width":0.05518617,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.71775264,"top":0.43814844,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"bounds":{"left":0.65857714,"top":0.45810056,"width":0.1200133,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"bounds":{"left":0.6519282,"top":0.50039905,"width":0.05418883,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"bounds":{"left":0.65857714,"top":0.5203512,"width":0.036901597,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.6994681,"top":0.5235435,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"bounds":{"left":0.65857714,"top":0.5434956,"width":0.035904255,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.6984708,"top":0.54668796,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"bounds":{"left":0.6519282,"top":0.5857941,"width":0.027260639,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"bounds":{"left":0.6599069,"top":0.612929,"width":0.030585106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"bounds":{"left":0.69148934,"top":0.6065443,"width":0.07679521,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"<None>","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXCheckBox","text":"Always use selected server or group of servers","depth":2,"bounds":{"left":0.6665558,"top":0.63766956,"width":0.10538564,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"bounds":{"left":0.25664893,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"bounds":{"left":0.26529256,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"bounds":{"left":0.27393618,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"bounds":{"left":0.28490692,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"bounds":{"left":0.29355052,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"bounds":{"left":0.30219415,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"bounds":{"left":0.3118351,"top":0.41340783,"width":0.04720745,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"bounds":{"left":0.36336437,"top":0.41340783,"width":0.03557181,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"bounds":{"left":0.40093085,"top":0.41340783,"width":0.03956117,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"bounds":{"left":0.44148937,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"bounds":{"left":0.45013297,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"bounds":{"left":0.4587766,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"bounds":{"left":0.46742022,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"bounds":{"left":0.47839096,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5 differences","depth":1,"bounds":{"left":0.7513298,"top":0.41181165,"width":0.026928192,"height":0.022346368},"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"828865ed","depth":1,"bounds":{"left":0.26196808,"top":0.43415803,"width":0.024268618,"height":0.013567438},"on_screen":true,"value":"828865ed","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedOriginalTo = explode('+', $header->value)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedOriginalTo,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"bounds":{"left":0.25531915,"top":0.42218676,"width":0.29886967,"height":0.57781327},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedOriginalTo = explode('+', $header->value)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedOriginalTo,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"bounds":{"left":0.5375665,"top":0.43415803,"width":0.24069148,"height":0.013567438},"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"bounds":{"left":0.5422208,"top":0.36951315,"width":0.27992022,"height":0.63048685},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"bounds":{"left":0.25531915,"top":0.7573823,"width":0.00930851,"height":0.027134877},"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"bounds":{"left":0.7140958,"top":0.7573823,"width":0.025930852,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":1,"bounds":{"left":0.74202126,"top":0.7573823,"width":0.03756649,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":2,"bounds":{"left":0.74202126,"top":0.7573823,"width":0.027260639,"height":0.027134877},"on_screen":true,"help_text":"Show drop-down menu (⌥⇧⏎)","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Changes","depth":1,"bounds":{"left":0.49833778,"top":0.13248204,"width":0.038231384,"height":0.012769354},"on_screen":true,"role_description":"text"}]...
|
-5198892415972827284
|
-6438878625925788789
|
visual_change
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20915 fix missing header
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
5 differences
828865ed
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedOriginalTo = explode('+', $header->value)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedOriginalTo,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
81583
|
2833
|
6
|
2026-05-28T08:30:30.805982+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957030805_m2.jpg...
|
PhpStorm
|
Commit Changes
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhpStormCoocWindowPwovscors$2 JY-20915-fix-maProje PhpStormCoocWindowPwovscors$2 JY-20915-fix-maProjectv© SyneMailbox.php© ServiceTest.php© TextRelayServiceTest.php x pho jminny.phpE env.productionC SyncBatchRedisService© BaseServiceTest.phpcione© CachedCrmServiceDecora© CrmActivityServiceTest.ptdectare(Strzcrcypes=.)"© CrmConfigurationSettings© CrmObjectsResolverTest;namespace Tests\Unit|Services \Mail;C DefaultProspectSearchStrC EmailHelperTest.phpC FieldValueConverterTest,f© LayoutManagerTest.phpC MigrateProviderServiceTe© Opportunity ActivityMatchyc Opportuniysyncstratcoyt© ProspectCacheTest.phpc Prosoccsearchstratcoyt© ProviderRegistryTest.phpc Recordseleclortestono© ResolveCompanyNameBylusc 6000Le Servzce 6a02l a$ 6000Lc609227usc 6000Lc Service 6a02. nessaoc as 60022h0gusc 6000 e Servzce 6aa2. nessagcParuse 6000le Service 6na2. nessagePar Header:use zuminare Suppor Facades contaruse Illuminate\Support\Facades\Log;use miinny Serusces.nu tey cel ayseruscosuse PHPUnit\Franework\Attributes\CoversClassuse 2.2 n Eranemorkeehures bara?rousdeuse Tests\TestCase:use Pe ections assuse Mockery:LochlehnotChanges 6 [EMAIL] app/Console/Commandsphe logging.php config© PlaybackService.php app/Services© TextRelayService.php app/Services/Mail© TextRelayServiceTest.php tests/Unit/Services/Mail› Unversioned Files 9 files8828865ed app& DockerfileE laravellog X 4 SF jiminny@localhost)A HSJocal ([iminny@localhost)A console [PROD]A console [STAGING)+5GChangeist:ChangesVVDaiti• © TextRelayService.php• © PlaybackService.php>D contig 1 file2 modifiedAuthormand.commiSon-ot commCommk Messageo85x nissing headerUpdate copyrightReformat codeRearrange codeOptimize importsv DiffT40+→0side-oy-soe viewer6828865edtryDo not ignoreCAhMonsodmessaoe Sseryscessusons massadesasderiSheaders = Snessage->getPayload() ->getHeaE Current versiontry 1Smessage = Sservice-›users_messages-›get(Sheaders = Snessage->getPayLoad() -›getHduforeach (Sheaders as Sheader) (1f (Sheader->name a==*X-Gm-Original-Snatches = $this->matchesExpected186186 €SoriginalTo = null;Sto = null;1f (1 Snatches) (// Sanitize PII by renoving pDriginalTo = explodforeach (Sheaders as Sheader) (if (Sheader->name === *X-Gn-Originat:} elseif (Sheader->name a== 'To') €CancelLog:: info('[TextRelayService) Refused message', [nessage_id' => Snessageid,'orsosnal to sani tazedl Ssans ta zedirsonnatttolreturn Snatches:Log::warning(' [TextRelayService) Refused message: missing X-Gn-Original-To'nessage_id' = Snessageld,D):} catch (\Exception $e) 4Log: :error(' [TextRelayService) Failed to inspect nessage"A console (EU]Thu 28 May 11:30:30+0 .fox@TextRelayServiceTest-phprner hensewietTnodgntror 15ResdTextRelayScrvicalest.oho #L154-188Thought for 1s>eexKeayeMicesthontThoughtsRead TextRelayServiceTest.php #L150-199Sasrched testistor@urrent Environment.allsBackio ohesder in teste/UntSamices Moiln extRelay Semvice est.oho.Ask annineeo+ @ Code SWE-16Cument versioitry €Smessage = Sservice->users messages-›get(Snaslbox, Smessageld) :Sheaders = Snessage->getPayLoad() ->getHeaders):SoriginalTo = null;oe nuaesforeach (Sheaders as Sheader) (if (Sheader->nane a== "X-Ga-Original-To') (j elseif (Sheader-›nameBRE "To") (Ston Sheaderesvalme,Srecipient = Soriginalto ?? Sto1f (Srecipient [== null) €Snatches = Sthis-›natchesExpectedRecipient(Srecipient, SexpectedAlias, SexpectedHost);if (! Snatches) (// Sanitize PII by renoving plus-tag content for loggingSsanitizRecipient = explode(*+*, Srecipient) [0];Log: : info('[TextRelayService) Refused message','nessage_id' => Saessageld,"original_to_sanitized' => SsanitizedRecipsentD):5 differencesoeTiiehahensa...
|
NULL
|
-365835568447629938
|
NULL
|
click
|
ocr
|
NULL
|
PhpStormCoocWindowPwovscors$2 JY-20915-fix-maProje PhpStormCoocWindowPwovscors$2 JY-20915-fix-maProjectv© SyneMailbox.php© ServiceTest.php© TextRelayServiceTest.php x pho jminny.phpE env.productionC SyncBatchRedisService© BaseServiceTest.phpcione© CachedCrmServiceDecora© CrmActivityServiceTest.ptdectare(Strzcrcypes=.)"© CrmConfigurationSettings© CrmObjectsResolverTest;namespace Tests\Unit|Services \Mail;C DefaultProspectSearchStrC EmailHelperTest.phpC FieldValueConverterTest,f© LayoutManagerTest.phpC MigrateProviderServiceTe© Opportunity ActivityMatchyc Opportuniysyncstratcoyt© ProspectCacheTest.phpc Prosoccsearchstratcoyt© ProviderRegistryTest.phpc Recordseleclortestono© ResolveCompanyNameBylusc 6000Le Servzce 6a02l a$ 6000Lc609227usc 6000Lc Service 6a02. nessaoc as 60022h0gusc 6000 e Servzce 6aa2. nessagcParuse 6000le Service 6na2. nessagePar Header:use zuminare Suppor Facades contaruse Illuminate\Support\Facades\Log;use miinny Serusces.nu tey cel ayseruscosuse PHPUnit\Franework\Attributes\CoversClassuse 2.2 n Eranemorkeehures bara?rousdeuse Tests\TestCase:use Pe ections assuse Mockery:LochlehnotChanges 6 [EMAIL] app/Console/Commandsphe logging.php config© PlaybackService.php app/Services© TextRelayService.php app/Services/Mail© TextRelayServiceTest.php tests/Unit/Services/Mail› Unversioned Files 9 files8828865ed app& DockerfileE laravellog X 4 SF jiminny@localhost)A HSJocal ([iminny@localhost)A console [PROD]A console [STAGING)+5GChangeist:ChangesVVDaiti• © TextRelayService.php• © PlaybackService.php>D contig 1 file2 modifiedAuthormand.commiSon-ot commCommk Messageo85x nissing headerUpdate copyrightReformat codeRearrange codeOptimize importsv DiffT40+→0side-oy-soe viewer6828865edtryDo not ignoreCAhMonsodmessaoe Sseryscessusons massadesasderiSheaders = Snessage->getPayload() ->getHeaE Current versiontry 1Smessage = Sservice-›users_messages-›get(Sheaders = Snessage->getPayLoad() -›getHduforeach (Sheaders as Sheader) (1f (Sheader->name a==*X-Gm-Original-Snatches = $this->matchesExpected186186 €SoriginalTo = null;Sto = null;1f (1 Snatches) (// Sanitize PII by renoving pDriginalTo = explodforeach (Sheaders as Sheader) (if (Sheader->name === *X-Gn-Originat:} elseif (Sheader->name a== 'To') €CancelLog:: info('[TextRelayService) Refused message', [nessage_id' => Snessageid,'orsosnal to sani tazedl Ssans ta zedirsonnatttolreturn Snatches:Log::warning(' [TextRelayService) Refused message: missing X-Gn-Original-To'nessage_id' = Snessageld,D):} catch (\Exception $e) 4Log: :error(' [TextRelayService) Failed to inspect nessage"A console (EU]Thu 28 May 11:30:30+0 .fox@TextRelayServiceTest-phprner hensewietTnodgntror 15ResdTextRelayScrvicalest.oho #L154-188Thought for 1s>eexKeayeMicesthontThoughtsRead TextRelayServiceTest.php #L150-199Sasrched testistor@urrent Environment.allsBackio ohesder in teste/UntSamices Moiln extRelay Semvice est.oho.Ask annineeo+ @ Code SWE-16Cument versioitry €Smessage = Sservice->users messages-›get(Snaslbox, Smessageld) :Sheaders = Snessage->getPayLoad() ->getHeaders):SoriginalTo = null;oe nuaesforeach (Sheaders as Sheader) (if (Sheader->nane a== "X-Ga-Original-To') (j elseif (Sheader-›nameBRE "To") (Ston Sheaderesvalme,Srecipient = Soriginalto ?? Sto1f (Srecipient [== null) €Snatches = Sthis-›natchesExpectedRecipient(Srecipient, SexpectedAlias, SexpectedHost);if (! Snatches) (// Sanitize PII by renoving plus-tag content for loggingSsanitizRecipient = explode(*+*, Srecipient) [0];Log: : info('[TextRelayService) Refused message','nessage_id' => Saessageld,"original_to_sanitized' => SsanitizedRecipsentD):5 differencesoeTiiehahensa...
|
81582
|
NULL
|
NULL
|
NULL
|
|
81584
|
2832
|
8
|
2026-05-28T08:30:30.705814+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957030705_m1.jpg...
|
PhpStorm
|
Commit Changes
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback......
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-6124039779810665749
|
-2898721283685741771
|
click
|
hybrid
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
iTerm2ShellEditViewSessionScriptsProfilesWindowHelp‹$0A-zshscreenpipe"DOCKER- ₴81DEV (docker)worker-calendar:worker-calendar_00:startedworker-conferences:worker-conferences_00: startedworker-crm-sync:worker-crm-sync_00: startedworker-crm-update:worker-crm-update_00: startedworker-download:worker-download_00:startedworker-emails:worker-emails_00:startedworker-es-update:worker-es-update_00:startedworker-nudges:worker-nudges_00: started₴2-zshО 84-zshWhat's next:Try Docker Debug forseamless, persistentdebugging tools in any container or image → docker debug docker_lamp_1Learnmoreat [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-fix-missing-header-text-relay) $csfixdocker exec -it docker_lamp_1•/vendor/bin/php-cs-fixer fix--config=.php-cs-fixer.dist.php-v--using-cache=no --diffPHP CS Fixer 3.95.1 Adalbertus by Fabien Potencier, Dariusz Ruminski and contributors.PHP runtime: 8.5.5Loaded config default from ".php-cs-fixer.dist.php".Running analysis on 7 cores with 10 files per process.5698/5698 C80100%*5ec2-user@ip-10-30-1...O 86100% <78• Thu 28 May 11:30:30181ec2-user@ip-10-30-140-...$7Fixed 0 of 5698 files in 52.731 seconds, 799.06 MB memory usedDetected deprecations in use (they will stop working in next major release):- Rule set"@PHP74Migration" is deprecated. Use"@PHP7x4Migration" instead.- Rule set "@PHP80Migration" is deprecated. Use "@PHP8x®Migration" instead.- Rule set "@PHP81Migration" is deprecated. Use "@PHP8x1Migration" instead.- Rule set "@PHP82Migration" is deprecated. Use "@PHP8x2Migration" instead.- Rule set "@PHP83Migration" is deprecated. Use "@PHP8x3Migration"instead.- Rule set "@PHP84Migration" is deprecated. Use "@PHP8x4Migration" instead.What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image + docker debug docker_lamp_1Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-fix-missing-header-text-relay) $D...
|
81581
|
NULL
|
NULL
|
NULL
|
|
81582
|
2833
|
5
|
2026-05-28T08:30:24.520618+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957024520_m2.jpg...
|
PhpStorm
|
Commit Changes
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20915 fix missing header
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
5 differences
828865ed
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedOriginalTo = explode('+', $header->value)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedOriginalTo,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"bounds":{"left":0.25531915,"top":0.39185953,"width":0.013962766,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"bounds":{"left":0.38231382,"top":0.15802075,"width":0.022938829,"height":0.027134877},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"bounds":{"left":0.40658244,"top":0.15802075,"width":0.042220745,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"bounds":{"left":0.25664893,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"bounds":{"left":0.26529256,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"bounds":{"left":0.27393618,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"bounds":{"left":0.28490692,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"bounds":{"left":0.3617021,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"bounds":{"left":0.37034574,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.19348404,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"JY-20915 fix missing header","depth":2,"bounds":{"left":0.2556516,"top":0.3008779,"width":0.19281915,"height":0.087789305},"on_screen":true,"value":"JY-20915 fix missing header","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"bounds":{"left":0.25531915,"top":0.28252193,"width":0.03557181,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"bounds":{"left":0.4401596,"top":0.27853152,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Git","depth":2,"bounds":{"left":0.45212767,"top":0.16280925,"width":0.0056515955,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"bounds":{"left":0.46010637,"top":0.18834797,"width":0.014960106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"bounds":{"left":0.4787234,"top":0.18834797,"width":0.09840426,"height":0.013567438},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"bounds":{"left":0.4587766,"top":0.21069433,"width":0.039893616,"height":0.019952115},"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"bounds":{"left":0.4587766,"top":0.23383878,"width":0.042220745,"height":0.019952115},"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.27613726,"width":0.032247342,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"bounds":{"left":0.4587766,"top":0.29608938,"width":0.04488032,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"bounds":{"left":0.4587766,"top":0.31923383,"width":0.03956117,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"bounds":{"left":0.4587766,"top":0.3423783,"width":0.041888297,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"bounds":{"left":0.4587766,"top":0.36552274,"width":0.044215426,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"bounds":{"left":0.4587766,"top":0.3886672,"width":0.026263298,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.48902926,"top":0.39185953,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"bounds":{"left":0.4587766,"top":0.41181165,"width":0.07247341,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"bounds":{"left":0.4587766,"top":0.4349561,"width":0.05518617,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.51795214,"top":0.43814844,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"bounds":{"left":0.4587766,"top":0.45810056,"width":0.1200133,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.50039905,"width":0.05418883,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"bounds":{"left":0.4587766,"top":0.5203512,"width":0.036901597,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.49966756,"top":0.5235435,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"bounds":{"left":0.4587766,"top":0.5434956,"width":0.035904255,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.49867022,"top":0.54668796,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"bounds":{"left":0.45212767,"top":0.5857941,"width":0.027260639,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"bounds":{"left":0.46010637,"top":0.612929,"width":0.030585106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"bounds":{"left":0.49168882,"top":0.6065443,"width":0.07679521,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"<None>","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXCheckBox","text":"Always use selected server or group of servers","depth":2,"bounds":{"left":0.46675533,"top":0.63766956,"width":0.10538564,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"bounds":{"left":0.25664893,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"bounds":{"left":0.26529256,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"bounds":{"left":0.27393618,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"bounds":{"left":0.28490692,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"bounds":{"left":0.29355052,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"bounds":{"left":0.30219415,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"bounds":{"left":0.3118351,"top":0.41340783,"width":0.04720745,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"bounds":{"left":0.36336437,"top":0.41340783,"width":0.03557181,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"bounds":{"left":0.40093085,"top":0.41340783,"width":0.03956117,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"bounds":{"left":0.44148937,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"bounds":{"left":0.45013297,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"bounds":{"left":0.4587766,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"bounds":{"left":0.46742022,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"bounds":{"left":0.47839096,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5 differences","depth":1,"bounds":{"left":0.5515292,"top":0.41181165,"width":0.026928192,"height":0.022346368},"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"828865ed","depth":1,"bounds":{"left":0.26196808,"top":0.43415803,"width":0.024268618,"height":0.013567438},"on_screen":true,"value":"828865ed","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedOriginalTo = explode('+', $header->value)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedOriginalTo,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"bounds":{"left":0.25531915,"top":0.42697525,"width":0.29886967,"height":0.57302475},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedOriginalTo = explode('+', $header->value)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedOriginalTo,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"bounds":{"left":0.4375,"top":0.43415803,"width":0.14095744,"height":0.013567438},"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"bounds":{"left":0.44215426,"top":0.42697525,"width":0.27992022,"height":0.57302475},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"bounds":{"left":0.25531915,"top":0.65363127,"width":0.00930851,"height":0.027134877},"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"bounds":{"left":0.5142952,"top":0.65363127,"width":0.025930852,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":1,"bounds":{"left":0.5422208,"top":0.65363127,"width":0.03756649,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":2,"bounds":{"left":0.5422208,"top":0.65363127,"width":0.027260639,"height":0.027134877},"on_screen":true,"help_text":"Show drop-down menu (⌥⇧⏎)","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Changes","depth":1,"bounds":{"left":0.39827126,"top":0.13248204,"width":0.038231384,"height":0.012769354},"on_screen":true,"role_description":"text"}]...
|
-5198892415972827284
|
-6438878625925788789
|
typing_pause
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20915 fix missing header
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
5 differences
828865ed
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedOriginalTo = explode('+', $header->value)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedOriginalTo,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
81581
|
2832
|
7
|
2026-05-28T08:30:24.419492+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957024419_m1.jpg...
|
PhpStorm
|
Commit Changes
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20915 fix missing header
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
5 differences
828865ed
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedOriginalTo = explode('+', $header->value)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedOriginalTo,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2 modified","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"JY-20915 fix missing header","depth":2,"on_screen":true,"value":"JY-20915 fix missing header","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Git","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"<None>","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXCheckBox","text":"Always use selected server or group of servers","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5 differences","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"828865ed","depth":1,"on_screen":true,"value":"828865ed","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedOriginalTo = explode('+', $header->value)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedOriginalTo,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedOriginalTo = explode('+', $header->value)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedOriginalTo,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":2,"on_screen":true,"help_text":"Show drop-down menu (⌥⇧⏎)","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Changes","depth":1,"on_screen":true,"role_description":"text"}]...
|
-5198892415972827284
|
-6438878625925788789
|
typing_pause
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20915 fix missing header
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
5 differences
828865ed
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedOriginalTo = explode('+', $header->value)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedOriginalTo,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
81580
|
2832
|
6
|
2026-05-28T08:30:19.167231+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957019167_m1.jpg...
|
PhpStorm
|
Commit Changes
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20915 fix missign header
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
5 differences
828865ed
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedOriginalTo = explode('+', $header->value)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedOriginalTo,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2 modified","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"JY-20915 fix missign header","depth":2,"on_screen":true,"value":"JY-20915 fix missign header","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Git","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"<None>","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXCheckBox","text":"Always use selected server or group of servers","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5 differences","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"828865ed","depth":1,"on_screen":true,"value":"828865ed","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedOriginalTo = explode('+', $header->value)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedOriginalTo,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedOriginalTo = explode('+', $header->value)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedOriginalTo,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":2,"on_screen":true,"help_text":"Show drop-down menu (⌥⇧⏎)","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Changes","depth":1,"on_screen":true,"role_description":"text"}]...
|
2369523212794376830
|
-6438878625925788789
|
typing_pause
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20915 fix missign header
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
5 differences
828865ed
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedOriginalTo = explode('+', $header->value)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedOriginalTo,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
81578
|
NULL
|
NULL
|
NULL
|
|
81579
|
2833
|
4
|
2026-05-28T08:30:19.066741+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957019066_m2.jpg...
|
PhpStorm
|
Commit Changes
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20915 fix missign header
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
5 differences
828865ed
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedOriginalTo = explode('+', $header->value)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedOriginalTo,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"bounds":{"left":0.25531915,"top":0.39185953,"width":0.013962766,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"bounds":{"left":0.38231382,"top":0.15802075,"width":0.022938829,"height":0.027134877},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"bounds":{"left":0.40658244,"top":0.15802075,"width":0.042220745,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"bounds":{"left":0.25664893,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"bounds":{"left":0.26529256,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"bounds":{"left":0.27393618,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"bounds":{"left":0.28490692,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"bounds":{"left":0.3617021,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"bounds":{"left":0.37034574,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.19348404,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"JY-20915 fix missign header","depth":2,"bounds":{"left":0.2556516,"top":0.3008779,"width":0.19281915,"height":0.087789305},"on_screen":true,"value":"JY-20915 fix missign header","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"bounds":{"left":0.25531915,"top":0.28252193,"width":0.03557181,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"bounds":{"left":0.4401596,"top":0.27853152,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Git","depth":2,"bounds":{"left":0.45212767,"top":0.16280925,"width":0.0056515955,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"bounds":{"left":0.46010637,"top":0.18834797,"width":0.014960106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"bounds":{"left":0.4787234,"top":0.18834797,"width":0.09840426,"height":0.013567438},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"bounds":{"left":0.4587766,"top":0.21069433,"width":0.039893616,"height":0.019952115},"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"bounds":{"left":0.4587766,"top":0.23383878,"width":0.042220745,"height":0.019952115},"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.27613726,"width":0.032247342,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"bounds":{"left":0.4587766,"top":0.29608938,"width":0.04488032,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"bounds":{"left":0.4587766,"top":0.31923383,"width":0.03956117,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"bounds":{"left":0.4587766,"top":0.3423783,"width":0.041888297,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"bounds":{"left":0.4587766,"top":0.36552274,"width":0.044215426,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"bounds":{"left":0.4587766,"top":0.3886672,"width":0.026263298,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.48902926,"top":0.39185953,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"bounds":{"left":0.4587766,"top":0.41181165,"width":0.07247341,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"bounds":{"left":0.4587766,"top":0.4349561,"width":0.05518617,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.51795214,"top":0.43814844,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"bounds":{"left":0.4587766,"top":0.45810056,"width":0.1200133,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.50039905,"width":0.05418883,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"bounds":{"left":0.4587766,"top":0.5203512,"width":0.036901597,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.49966756,"top":0.5235435,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"bounds":{"left":0.4587766,"top":0.5434956,"width":0.035904255,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.49867022,"top":0.54668796,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"bounds":{"left":0.45212767,"top":0.5857941,"width":0.027260639,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"bounds":{"left":0.46010637,"top":0.612929,"width":0.030585106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"bounds":{"left":0.49168882,"top":0.6065443,"width":0.07679521,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"<None>","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXCheckBox","text":"Always use selected server or group of servers","depth":2,"bounds":{"left":0.46675533,"top":0.63766956,"width":0.10538564,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"bounds":{"left":0.25664893,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"bounds":{"left":0.26529256,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"bounds":{"left":0.27393618,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"bounds":{"left":0.28490692,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"bounds":{"left":0.29355052,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"bounds":{"left":0.30219415,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"bounds":{"left":0.3118351,"top":0.41340783,"width":0.04720745,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"bounds":{"left":0.36336437,"top":0.41340783,"width":0.03557181,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"bounds":{"left":0.40093085,"top":0.41340783,"width":0.03956117,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"bounds":{"left":0.44148937,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"bounds":{"left":0.45013297,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"bounds":{"left":0.4587766,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"bounds":{"left":0.46742022,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"bounds":{"left":0.47839096,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5 differences","depth":1,"bounds":{"left":0.5515292,"top":0.41181165,"width":0.026928192,"height":0.022346368},"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"828865ed","depth":1,"bounds":{"left":0.26196808,"top":0.43415803,"width":0.024268618,"height":0.013567438},"on_screen":true,"value":"828865ed","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedOriginalTo = explode('+', $header->value)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedOriginalTo,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"bounds":{"left":0.25531915,"top":0.42697525,"width":0.29886967,"height":0.57302475},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedOriginalTo = explode('+', $header->value)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedOriginalTo,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"bounds":{"left":0.4375,"top":0.43415803,"width":0.14095744,"height":0.013567438},"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"bounds":{"left":0.44215426,"top":0.42697525,"width":0.27992022,"height":0.57302475},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"bounds":{"left":0.25531915,"top":0.65363127,"width":0.00930851,"height":0.027134877},"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"bounds":{"left":0.5142952,"top":0.65363127,"width":0.025930852,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":1,"bounds":{"left":0.5422208,"top":0.65363127,"width":0.03756649,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":2,"bounds":{"left":0.5422208,"top":0.65363127,"width":0.027260639,"height":0.027134877},"on_screen":true,"help_text":"Show drop-down menu (⌥⇧⏎)","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Changes","depth":1,"bounds":{"left":0.39827126,"top":0.13248204,"width":0.038231384,"height":0.012769354},"on_screen":true,"role_description":"text"}]...
|
2369523212794376830
|
-6438878625925788789
|
typing_pause
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20915 fix missign header
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
5 differences
828865ed
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedOriginalTo = explode('+', $header->value)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedOriginalTo,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
81577
|
NULL
|
NULL
|
NULL
|
|
81577
|
2833
|
3
|
2026-05-28T08:30:16.810520+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957016810_m2.jpg...
|
PhpStorm
|
Commit Changes
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20915 fix
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
5 differences
828865ed
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedOriginalTo = explode('+', $header->value)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedOriginalTo,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"bounds":{"left":0.25531915,"top":0.39185953,"width":0.013962766,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"bounds":{"left":0.38231382,"top":0.15802075,"width":0.022938829,"height":0.027134877},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"bounds":{"left":0.40658244,"top":0.15802075,"width":0.042220745,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"bounds":{"left":0.25664893,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"bounds":{"left":0.26529256,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"bounds":{"left":0.27393618,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"bounds":{"left":0.28490692,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"bounds":{"left":0.3617021,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"bounds":{"left":0.37034574,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.19348404,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"JY-20915 fix","depth":2,"bounds":{"left":0.2556516,"top":0.3008779,"width":0.19281915,"height":0.087789305},"on_screen":true,"value":"JY-20915 fix","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"bounds":{"left":0.25531915,"top":0.28252193,"width":0.03557181,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"bounds":{"left":0.4401596,"top":0.27853152,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Git","depth":2,"bounds":{"left":0.45212767,"top":0.16280925,"width":0.0056515955,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"bounds":{"left":0.46010637,"top":0.18834797,"width":0.014960106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"bounds":{"left":0.4787234,"top":0.18834797,"width":0.09840426,"height":0.013567438},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"bounds":{"left":0.4587766,"top":0.21069433,"width":0.039893616,"height":0.019952115},"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"bounds":{"left":0.4587766,"top":0.23383878,"width":0.042220745,"height":0.019952115},"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.27613726,"width":0.032247342,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"bounds":{"left":0.4587766,"top":0.29608938,"width":0.04488032,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"bounds":{"left":0.4587766,"top":0.31923383,"width":0.03956117,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"bounds":{"left":0.4587766,"top":0.3423783,"width":0.041888297,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"bounds":{"left":0.4587766,"top":0.36552274,"width":0.044215426,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"bounds":{"left":0.4587766,"top":0.3886672,"width":0.026263298,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.48902926,"top":0.39185953,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"bounds":{"left":0.4587766,"top":0.41181165,"width":0.07247341,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"bounds":{"left":0.4587766,"top":0.4349561,"width":0.05518617,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.51795214,"top":0.43814844,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"bounds":{"left":0.4587766,"top":0.45810056,"width":0.1200133,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.50039905,"width":0.05418883,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"bounds":{"left":0.4587766,"top":0.5203512,"width":0.036901597,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.49966756,"top":0.5235435,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"bounds":{"left":0.4587766,"top":0.5434956,"width":0.035904255,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.49867022,"top":0.54668796,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"bounds":{"left":0.45212767,"top":0.5857941,"width":0.027260639,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"bounds":{"left":0.46010637,"top":0.612929,"width":0.030585106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"bounds":{"left":0.49168882,"top":0.6065443,"width":0.07679521,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"<None>","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXCheckBox","text":"Always use selected server or group of servers","depth":2,"bounds":{"left":0.46675533,"top":0.63766956,"width":0.10538564,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"bounds":{"left":0.25664893,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"bounds":{"left":0.26529256,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"bounds":{"left":0.27393618,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"bounds":{"left":0.28490692,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"bounds":{"left":0.29355052,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"bounds":{"left":0.30219415,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"bounds":{"left":0.3118351,"top":0.41340783,"width":0.04720745,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"bounds":{"left":0.36336437,"top":0.41340783,"width":0.03557181,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"bounds":{"left":0.40093085,"top":0.41340783,"width":0.03956117,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"bounds":{"left":0.44148937,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"bounds":{"left":0.45013297,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"bounds":{"left":0.4587766,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"bounds":{"left":0.46742022,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"bounds":{"left":0.47839096,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5 differences","depth":1,"bounds":{"left":0.5515292,"top":0.41181165,"width":0.026928192,"height":0.022346368},"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"828865ed","depth":1,"bounds":{"left":0.26196808,"top":0.43415803,"width":0.024268618,"height":0.013567438},"on_screen":true,"value":"828865ed","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedOriginalTo = explode('+', $header->value)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedOriginalTo,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"bounds":{"left":0.25531915,"top":0.42697525,"width":0.29886967,"height":0.57302475},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedOriginalTo = explode('+', $header->value)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedOriginalTo,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"bounds":{"left":0.4375,"top":0.43415803,"width":0.14095744,"height":0.013567438},"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"bounds":{"left":0.44215426,"top":0.42697525,"width":0.27992022,"height":0.57302475},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"bounds":{"left":0.25531915,"top":0.65363127,"width":0.00930851,"height":0.027134877},"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"bounds":{"left":0.5142952,"top":0.65363127,"width":0.025930852,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":1,"bounds":{"left":0.5422208,"top":0.65363127,"width":0.03756649,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":2,"bounds":{"left":0.5422208,"top":0.65363127,"width":0.027260639,"height":0.027134877},"on_screen":true,"help_text":"Show drop-down menu (⌥⇧⏎)","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Changes","depth":1,"bounds":{"left":0.39827126,"top":0.13248204,"width":0.038231384,"height":0.012769354},"on_screen":true,"role_description":"text"}]...
|
-4727986652480141900
|
-6438878557206312053
|
typing_pause
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20915 fix
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
5 differences
828865ed
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedOriginalTo = explode('+', $header->value)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedOriginalTo,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
81578
|
2832
|
5
|
2026-05-28T08:30:16.595120+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957016595_m1.jpg...
|
PhpStorm
|
Commit Changes
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20915 fix
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2 modified","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"JY-20915 fix","depth":2,"on_screen":true,"value":"JY-20915 fix","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Git","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"<None>","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXCheckBox","text":"Always use selected server or group of servers","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-2202807456618208579
|
-1968662056537927935
|
typing_pause
|
hybrid
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20915 fix
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
iTerm2ShellEditViewSessionScriptsProfilesWindowHelp‹$0A-zshscreenpipe"DOCKER₴81DEV (docker)worker-calendar:worker-calendar_00:startedworker-conferences:worker-conferences_00: startedworker-crm-sync:worker-crm-sync_00: startedworker-crm-update:worker-crm-update_00: startedworker-download:worker-download_00:startedworker-emails:worker-emails_00:startedworker-es-update:worker-es-update_00:startedworker-nudges:worker-nudges_00: started₴2-zsh-zshWhat's next:Try Docker Debug forseamless, persistentdebugging tools in any container or image → docker debug docker_lamp_1Learnmoreat [URL_WITH_CREDENTIALS] ~/jiminny/app (JY-20915-fix-missing-header-text-relay) $csfixdocker exec -it docker_lamp_1•/vendor/bin/php-cs-fixer fix--config=.php-cs-fixer.dist.php-v--using-cache=no --diffPHP CS Fixer 3.95.1 Adalbertus by Fabien Potencier, Dariusz Ruminski and contributors.PHP runtime: 8.5.5Loaded config default from ".php-cs-fixer.dist.php".Running analysis on 7 cores with 10 files per process.5698/5698 C80100%*5ec2-user@ip-10-30-1...O 6100% <78• Thu 28 May 11:30:16T81ec2-user@ip-10-30-140-...$7Fixed 0 of 5698 files in 52.731 seconds, 799.06 MB memory usedDetected deprecations in use (they will stop working in next major release):- Rule set"@PHP74Migration" is deprecated. Use"@PHP7x4Migration" instead.- Rule set "@PHP80Migration" is deprecated. Use "@PHP8x®Migration" instead.- Rule set "@PHP81Migration" is deprecated. Use "@PHP8x1Migration" instead.- Rule set "@PHP82Migration" is deprecated. Use "@PHP8x2Migration" instead.- Rule set "@PHP83Migration" is deprecated. Use "@PHP8x3Migration"instead.- Rule set "@PHP84Migration" is deprecated. Use "@PHP8x4Migration" instead.What's next:Try Docker Debug for seamless, persistent debugging tools in any container or image + docker debug docker_lamp_1Learn more at https://docs.docker.com/go/debug-cli/lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20915-fix-missing-header-text-relay) $D...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
81576
|
2833
|
2
|
2026-05-28T08:30:15.782678+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957015782_m2.jpg...
|
PhpStorm
|
Commit Changes
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20915 f
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
5 differences
828865ed
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedOriginalTo = explode('+', $header->value)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedOriginalTo,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"bounds":{"left":0.25531915,"top":0.39185953,"width":0.013962766,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"bounds":{"left":0.38231382,"top":0.15802075,"width":0.022938829,"height":0.027134877},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"bounds":{"left":0.40658244,"top":0.15802075,"width":0.042220745,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"bounds":{"left":0.25664893,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"bounds":{"left":0.26529256,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"bounds":{"left":0.27393618,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"bounds":{"left":0.28490692,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"bounds":{"left":0.3617021,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"bounds":{"left":0.37034574,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.19348404,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"JY-20915 f","depth":2,"bounds":{"left":0.2556516,"top":0.3008779,"width":0.19281915,"height":0.087789305},"on_screen":true,"value":"JY-20915 f","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"bounds":{"left":0.25531915,"top":0.28252193,"width":0.03557181,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"bounds":{"left":0.4401596,"top":0.27853152,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Git","depth":2,"bounds":{"left":0.45212767,"top":0.16280925,"width":0.0056515955,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"bounds":{"left":0.46010637,"top":0.18834797,"width":0.014960106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"bounds":{"left":0.4787234,"top":0.18834797,"width":0.09840426,"height":0.013567438},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"bounds":{"left":0.4587766,"top":0.21069433,"width":0.039893616,"height":0.019952115},"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"bounds":{"left":0.4587766,"top":0.23383878,"width":0.042220745,"height":0.019952115},"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.27613726,"width":0.032247342,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"bounds":{"left":0.4587766,"top":0.29608938,"width":0.04488032,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"bounds":{"left":0.4587766,"top":0.31923383,"width":0.03956117,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"bounds":{"left":0.4587766,"top":0.3423783,"width":0.041888297,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"bounds":{"left":0.4587766,"top":0.36552274,"width":0.044215426,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"bounds":{"left":0.4587766,"top":0.3886672,"width":0.026263298,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.48902926,"top":0.39185953,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"bounds":{"left":0.4587766,"top":0.41181165,"width":0.07247341,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"bounds":{"left":0.4587766,"top":0.4349561,"width":0.05518617,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.51795214,"top":0.43814844,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"bounds":{"left":0.4587766,"top":0.45810056,"width":0.1200133,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.50039905,"width":0.05418883,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"bounds":{"left":0.4587766,"top":0.5203512,"width":0.036901597,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.49966756,"top":0.5235435,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"bounds":{"left":0.4587766,"top":0.5434956,"width":0.035904255,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.49867022,"top":0.54668796,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"bounds":{"left":0.45212767,"top":0.5857941,"width":0.027260639,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"bounds":{"left":0.46010637,"top":0.612929,"width":0.030585106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"bounds":{"left":0.49168882,"top":0.6065443,"width":0.07679521,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"<None>","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXCheckBox","text":"Always use selected server or group of servers","depth":2,"bounds":{"left":0.46675533,"top":0.63766956,"width":0.10538564,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"bounds":{"left":0.25664893,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"bounds":{"left":0.26529256,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"bounds":{"left":0.27393618,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"bounds":{"left":0.28490692,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"bounds":{"left":0.29355052,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"bounds":{"left":0.30219415,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"bounds":{"left":0.3118351,"top":0.41340783,"width":0.04720745,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"bounds":{"left":0.36336437,"top":0.41340783,"width":0.03557181,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"bounds":{"left":0.40093085,"top":0.41340783,"width":0.03956117,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"bounds":{"left":0.44148937,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"bounds":{"left":0.45013297,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"bounds":{"left":0.4587766,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"bounds":{"left":0.46742022,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"bounds":{"left":0.47839096,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5 differences","depth":1,"bounds":{"left":0.5515292,"top":0.41181165,"width":0.026928192,"height":0.022346368},"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"828865ed","depth":1,"bounds":{"left":0.26196808,"top":0.43415803,"width":0.024268618,"height":0.013567438},"on_screen":true,"value":"828865ed","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedOriginalTo = explode('+', $header->value)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedOriginalTo,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"bounds":{"left":0.25531915,"top":0.42697525,"width":0.29886967,"height":0.57302475},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedOriginalTo = explode('+', $header->value)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedOriginalTo,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"bounds":{"left":0.4375,"top":0.43415803,"width":0.14095744,"height":0.013567438},"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"bounds":{"left":0.44215426,"top":0.42697525,"width":0.27992022,"height":0.57302475},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"bounds":{"left":0.25531915,"top":0.65363127,"width":0.00930851,"height":0.027134877},"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"bounds":{"left":0.5142952,"top":0.65363127,"width":0.025930852,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":1,"bounds":{"left":0.5422208,"top":0.65363127,"width":0.03756649,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":2,"bounds":{"left":0.5422208,"top":0.65363127,"width":0.027260639,"height":0.027134877},"on_screen":true,"help_text":"Show drop-down menu (⌥⇧⏎)","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Changes","depth":1,"bounds":{"left":0.39827126,"top":0.13248204,"width":0.038231384,"height":0.012769354},"on_screen":true,"role_description":"text"}]...
|
-5797286108512401775
|
-6438878625925788789
|
typing_pause
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20915 f
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
5 differences
828865ed
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedOriginalTo = explode('+', $header->value)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedOriginalTo,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
81574
|
NULL
|
NULL
|
NULL
|
|
81575
|
2832
|
4
|
2026-05-28T08:30:15.683398+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957015683_m1.jpg...
|
PhpStorm
|
Commit Changes
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20915
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
5 differences
828865ed
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedOriginalTo = explode('+', $header->value)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedOriginalTo,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2 modified","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"JY-20915","depth":2,"on_screen":true,"value":"JY-20915","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Git","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"<None>","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXCheckBox","text":"Always use selected server or group of servers","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5 differences","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"828865ed","depth":1,"on_screen":true,"value":"828865ed","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedOriginalTo = explode('+', $header->value)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedOriginalTo,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedOriginalTo = explode('+', $header->value)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedOriginalTo,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":2,"on_screen":true,"help_text":"Show drop-down menu (⌥⇧⏎)","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Changes","depth":1,"on_screen":true,"role_description":"text"}]...
|
8438360415843149031
|
-6438878488486835317
|
typing_pause
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20915
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
5 differences
828865ed
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedOriginalTo = explode('+', $header->value)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedOriginalTo,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
81573
|
NULL
|
NULL
|
NULL
|
|
81574
|
2833
|
1
|
2026-05-28T08:30:04.322757+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957004322_m2.jpg...
|
PhpStorm
|
Commit Changes
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20915
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
5 differences
828865ed
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedOriginalTo = explode('+', $header->value)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedOriginalTo,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"bounds":{"left":0.25531915,"top":0.39185953,"width":0.013962766,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"bounds":{"left":0.38231382,"top":0.15802075,"width":0.022938829,"height":0.027134877},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"bounds":{"left":0.40658244,"top":0.15802075,"width":0.042220745,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"bounds":{"left":0.25664893,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"bounds":{"left":0.26529256,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"bounds":{"left":0.27393618,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"bounds":{"left":0.28490692,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"bounds":{"left":0.3617021,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"bounds":{"left":0.37034574,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.19348404,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"JY-20915","depth":2,"bounds":{"left":0.2556516,"top":0.3008779,"width":0.19281915,"height":0.087789305},"on_screen":true,"value":"JY-20915","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"bounds":{"left":0.25531915,"top":0.28252193,"width":0.03557181,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"bounds":{"left":0.4401596,"top":0.27853152,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Git","depth":2,"bounds":{"left":0.45212767,"top":0.16280925,"width":0.0056515955,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"bounds":{"left":0.46010637,"top":0.18834797,"width":0.014960106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"bounds":{"left":0.4787234,"top":0.18834797,"width":0.09840426,"height":0.013567438},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"bounds":{"left":0.4587766,"top":0.21069433,"width":0.039893616,"height":0.019952115},"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"bounds":{"left":0.4587766,"top":0.23383878,"width":0.042220745,"height":0.019952115},"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.27613726,"width":0.032247342,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"bounds":{"left":0.4587766,"top":0.29608938,"width":0.04488032,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"bounds":{"left":0.4587766,"top":0.31923383,"width":0.03956117,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"bounds":{"left":0.4587766,"top":0.3423783,"width":0.041888297,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"bounds":{"left":0.4587766,"top":0.36552274,"width":0.044215426,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"bounds":{"left":0.4587766,"top":0.3886672,"width":0.026263298,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.48902926,"top":0.39185953,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"bounds":{"left":0.4587766,"top":0.41181165,"width":0.07247341,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"bounds":{"left":0.4587766,"top":0.4349561,"width":0.05518617,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.51795214,"top":0.43814844,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"bounds":{"left":0.4587766,"top":0.45810056,"width":0.1200133,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.50039905,"width":0.05418883,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"bounds":{"left":0.4587766,"top":0.5203512,"width":0.036901597,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.49966756,"top":0.5235435,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"bounds":{"left":0.4587766,"top":0.5434956,"width":0.035904255,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.49867022,"top":0.54668796,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"bounds":{"left":0.45212767,"top":0.5857941,"width":0.027260639,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"bounds":{"left":0.46010637,"top":0.612929,"width":0.030585106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"bounds":{"left":0.49168882,"top":0.6065443,"width":0.07679521,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"<None>","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXCheckBox","text":"Always use selected server or group of servers","depth":2,"bounds":{"left":0.46675533,"top":0.63766956,"width":0.10538564,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"bounds":{"left":0.25664893,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"bounds":{"left":0.26529256,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"bounds":{"left":0.27393618,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"bounds":{"left":0.28490692,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"bounds":{"left":0.29355052,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"bounds":{"left":0.30219415,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"bounds":{"left":0.3118351,"top":0.41340783,"width":0.04720745,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"bounds":{"left":0.36336437,"top":0.41340783,"width":0.03557181,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"bounds":{"left":0.40093085,"top":0.41340783,"width":0.03956117,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"bounds":{"left":0.44148937,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"bounds":{"left":0.45013297,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"bounds":{"left":0.4587766,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"bounds":{"left":0.46742022,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"bounds":{"left":0.47839096,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5 differences","depth":1,"bounds":{"left":0.5515292,"top":0.41181165,"width":0.026928192,"height":0.022346368},"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"828865ed","depth":1,"bounds":{"left":0.26196808,"top":0.43415803,"width":0.024268618,"height":0.013567438},"on_screen":true,"value":"828865ed","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedOriginalTo = explode('+', $header->value)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedOriginalTo,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"bounds":{"left":0.25531915,"top":0.42697525,"width":0.29886967,"height":0.57302475},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedOriginalTo = explode('+', $header->value)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedOriginalTo,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
2159852338092478072
|
-1827124162899571957
|
idle
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20915
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
5 differences
828865ed
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedOriginalTo = explode('+', $header->value)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedOriginalTo,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
81573
|
2832
|
3
|
2026-05-28T08:30:04.315496+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779957004315_m1.jpg...
|
PhpStorm
|
Commit Changes
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20915
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
5 differences
828865ed
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedOriginalTo = explode('+', $header->value)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedOriginalTo,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Commit...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2 modified","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"JY-20915","depth":2,"on_screen":true,"value":"JY-20915","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Git","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"<None>","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXCheckBox","text":"Always use selected server or group of servers","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5 differences","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"828865ed","depth":1,"on_screen":true,"value":"828865ed","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedOriginalTo = explode('+', $header->value)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedOriginalTo,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedOriginalTo = explode('+', $header->value)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedOriginalTo,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1483310312528614785
|
-6438878488486835317
|
idle
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20915
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
5 differences
828865ed
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedOriginalTo = explode('+', $header->value)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedOriginalTo,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Commit...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
81572
|
2832
|
2
|
2026-05-28T08:29:33.864702+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779956973864_m1.jpg...
|
PhpStorm
|
Commit Changes
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20915
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
5 differences
828865ed
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedOriginalTo = explode('+', $header->value)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedOriginalTo,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2 modified","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"JY-20915","depth":2,"on_screen":true,"value":"JY-20915","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Git","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"<None>","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXCheckBox","text":"Always use selected server or group of servers","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5 differences","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"828865ed","depth":1,"on_screen":true,"value":"828865ed","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedOriginalTo = explode('+', $header->value)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedOriginalTo,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedOriginalTo = explode('+', $header->value)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedOriginalTo,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
903430725235957857
|
-6438878488486835317
|
click
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20915
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
5 differences
828865ed
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedOriginalTo = explode('+', $header->value)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedOriginalTo,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel...
|
81570
|
NULL
|
NULL
|
NULL
|
|
81571
|
2833
|
0
|
2026-05-28T08:29:07.989769+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779956947989_m2.jpg...
|
PhpStorm
|
Commit Changes
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20915
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
5 differences
828865ed
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedOriginalTo = explode('+', $header->value)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedOriginalTo,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"bounds":{"left":0.25531915,"top":0.39185953,"width":0.013962766,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"bounds":{"left":0.38231382,"top":0.15802075,"width":0.022938829,"height":0.027134877},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"bounds":{"left":0.40658244,"top":0.15802075,"width":0.042220745,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"bounds":{"left":0.25664893,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"bounds":{"left":0.26529256,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"bounds":{"left":0.27393618,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"bounds":{"left":0.28490692,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"bounds":{"left":0.3617021,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"bounds":{"left":0.37034574,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.19348404,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"JY-20915","depth":2,"bounds":{"left":0.2556516,"top":0.3008779,"width":0.19281915,"height":0.087789305},"on_screen":true,"value":"JY-20915","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"bounds":{"left":0.25531915,"top":0.28252193,"width":0.03557181,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"bounds":{"left":0.4401596,"top":0.27853152,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Git","depth":2,"bounds":{"left":0.45212767,"top":0.16280925,"width":0.0056515955,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"bounds":{"left":0.46010637,"top":0.18834797,"width":0.014960106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"bounds":{"left":0.4787234,"top":0.18834797,"width":0.09840426,"height":0.013567438},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"bounds":{"left":0.4587766,"top":0.21069433,"width":0.039893616,"height":0.019952115},"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"bounds":{"left":0.4587766,"top":0.23383878,"width":0.042220745,"height":0.019952115},"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.27613726,"width":0.032247342,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"bounds":{"left":0.4587766,"top":0.29608938,"width":0.04488032,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"bounds":{"left":0.4587766,"top":0.31923383,"width":0.03956117,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"bounds":{"left":0.4587766,"top":0.3423783,"width":0.041888297,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"bounds":{"left":0.4587766,"top":0.36552274,"width":0.044215426,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"bounds":{"left":0.4587766,"top":0.3886672,"width":0.026263298,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.48902926,"top":0.39185953,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"bounds":{"left":0.4587766,"top":0.41181165,"width":0.07247341,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"bounds":{"left":0.4587766,"top":0.4349561,"width":0.05518617,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.51795214,"top":0.43814844,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"bounds":{"left":0.4587766,"top":0.45810056,"width":0.1200133,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.50039905,"width":0.05418883,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"bounds":{"left":0.4587766,"top":0.5203512,"width":0.036901597,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.49966756,"top":0.5235435,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"bounds":{"left":0.4587766,"top":0.5434956,"width":0.035904255,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.49867022,"top":0.54668796,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"bounds":{"left":0.45212767,"top":0.5857941,"width":0.027260639,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"bounds":{"left":0.46010637,"top":0.612929,"width":0.030585106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"bounds":{"left":0.49168882,"top":0.6065443,"width":0.07679521,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"<None>","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXCheckBox","text":"Always use selected server or group of servers","depth":2,"bounds":{"left":0.46675533,"top":0.63766956,"width":0.10538564,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"bounds":{"left":0.25664893,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"bounds":{"left":0.26529256,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"bounds":{"left":0.27393618,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"bounds":{"left":0.28490692,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"bounds":{"left":0.29355052,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"bounds":{"left":0.30219415,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"bounds":{"left":0.3118351,"top":0.41340783,"width":0.04720745,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"bounds":{"left":0.36336437,"top":0.41340783,"width":0.03557181,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"bounds":{"left":0.40093085,"top":0.41340783,"width":0.03956117,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"bounds":{"left":0.44148937,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"bounds":{"left":0.45013297,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"bounds":{"left":0.4587766,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"bounds":{"left":0.46742022,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"bounds":{"left":0.47839096,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5 differences","depth":1,"bounds":{"left":0.5515292,"top":0.41181165,"width":0.026928192,"height":0.022346368},"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"828865ed","depth":1,"bounds":{"left":0.26196808,"top":0.43415803,"width":0.024268618,"height":0.013567438},"on_screen":true,"value":"828865ed","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedOriginalTo = explode('+', $header->value)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedOriginalTo,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"bounds":{"left":0.25531915,"top":0.42697525,"width":0.29886967,"height":0.57302475},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedOriginalTo = explode('+', $header->value)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedOriginalTo,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"bounds":{"left":0.4375,"top":0.43415803,"width":0.14095744,"height":0.013567438},"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"bounds":{"left":0.44215426,"top":0.42697525,"width":0.27992022,"height":0.57302475},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"bounds":{"left":0.25531915,"top":0.65363127,"width":0.00930851,"height":0.027134877},"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"bounds":{"left":0.5142952,"top":0.65363127,"width":0.025930852,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":1,"bounds":{"left":0.5422208,"top":0.65363127,"width":0.03756649,"height":0.027134877},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":2,"bounds":{"left":0.5422208,"top":0.65363127,"width":0.027260639,"height":0.027134877},"on_screen":true,"help_text":"Show drop-down menu (⌥⇧⏎)","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Changes","depth":1,"bounds":{"left":0.39827126,"top":0.13248204,"width":0.038231384,"height":0.012769354},"on_screen":true,"role_description":"text"}]...
|
8438360415843149031
|
-6438878488486835317
|
typing_pause
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20915
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
5 differences
828865ed
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedOriginalTo = explode('+', $header->value)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedOriginalTo,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
81568
|
NULL
|
NULL
|
NULL
|
|
81570
|
2832
|
1
|
2026-05-28T08:29:07.884444+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779956947884_m1.jpg...
|
PhpStorm
|
Commit Changes
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20915
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
5 differences
828865ed
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedOriginalTo = explode('+', $header->value)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedOriginalTo,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2 modified","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"JY-20915","depth":2,"on_screen":true,"value":"JY-20915","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Git","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"<None>","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXCheckBox","text":"Always use selected server or group of servers","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5 differences","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"828865ed","depth":1,"on_screen":true,"value":"828865ed","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedOriginalTo = explode('+', $header->value)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedOriginalTo,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedOriginalTo = explode('+', $header->value)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedOriginalTo,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":2,"on_screen":true,"help_text":"Show drop-down menu (⌥⇧⏎)","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Changes","depth":1,"on_screen":true,"role_description":"text"}]...
|
8438360415843149031
|
-6438878488486835317
|
typing_pause
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20915
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
5 differences
828865ed
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedOriginalTo = explode('+', $header->value)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedOriginalTo,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
81569
|
2832
|
0
|
2026-05-28T08:29:05.631632+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779956945631_m1.jpg...
|
PhpStorm
|
Commit Changes
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
5 differences
828865ed
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedOriginalTo = explode('+', $header->value)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedOriginalTo,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2 modified","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"JY-20","depth":2,"on_screen":true,"value":"JY-20","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Git","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"<None>","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXCheckBox","text":"Always use selected server or group of servers","depth":2,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5 differences","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"828865ed","depth":1,"on_screen":true,"value":"828865ed","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedOriginalTo = explode('+', $header->value)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedOriginalTo,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedOriginalTo = explode('+', $header->value)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedOriginalTo,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":1,"on_screen":true,"help_text":"Show help contents","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Commit","depth":2,"on_screen":true,"help_text":"Show drop-down menu (⌥⇧⏎)","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Changes","depth":1,"on_screen":true,"role_description":"text"}]...
|
-4841489117713568875
|
-6438878625925788789
|
typing_pause
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
5 differences
828865ed
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedOriginalTo = explode('+', $header->value)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedOriginalTo,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Help
Cancel
Commit
Commit
Commit Changes...
|
81567
|
NULL
|
NULL
|
NULL
|
|
81568
|
NULL
|
0
|
2026-05-28T08:29:05.529880+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-28/1779 /Users/lukas/.screenpipe/data/data/2026-05-28/1779956945529_m2.jpg...
|
PhpStorm
|
Commit Changes
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
5 differences
828865ed
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedOriginalTo = explode('+', $header->value)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedOriginalTo,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Diff","depth":1,"bounds":{"left":0.25531915,"top":0.39185953,"width":0.013962766,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Changelist:","depth":1,"bounds":{"left":0.38231382,"top":0.15802075,"width":0.022938829,"height":0.027134877},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Changelist:","depth":1,"bounds":{"left":0.40658244,"top":0.15802075,"width":0.042220745,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Changes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show Diff","depth":2,"bounds":{"left":0.25664893,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Rollback...","depth":2,"bounds":{"left":0.26529256,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Refresh","depth":2,"bounds":{"left":0.27393618,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Group By","depth":2,"bounds":{"left":0.28490692,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand All","depth":2,"bounds":{"left":0.3617021,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":2,"bounds":{"left":0.37034574,"top":0.16201118,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2 modified","depth":1,"bounds":{"left":0.25531915,"top":0.254589,"width":0.19348404,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"JY-20","depth":2,"bounds":{"left":0.2556516,"top":0.3008779,"width":0.19281915,"height":0.087789305},"on_screen":true,"value":"JY-20","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Message","depth":1,"bounds":{"left":0.25531915,"top":0.28252193,"width":0.03557181,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Commit Message History","depth":2,"bounds":{"left":0.4401596,"top":0.27853152,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Git","depth":2,"bounds":{"left":0.45212767,"top":0.16280925,"width":0.0056515955,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Author:","depth":2,"bounds":{"left":0.46010637,"top":0.18834797,"width":0.014960106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Author:","depth":3,"bounds":{"left":0.4787234,"top":0.18834797,"width":0.09840426,"height":0.013567438},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Amend commit","depth":2,"bounds":{"left":0.4587766,"top":0.21069433,"width":0.039893616,"height":0.019952115},"on_screen":true,"help_text":"Merge this commit with the previous one","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Sign-off commit","depth":2,"bounds":{"left":0.4587766,"top":0.23383878,"width":0.042220745,"height":0.019952115},"on_screen":true,"help_text":"<html>Adds the following line at the end of the commit message:<br/>Signed-off by: Lukas Kovalik <kovaliklukas@gmail.com></html>","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Create extra commit with file movements","depth":2,"on_screen":false,"role_description":"checkbox","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.27613726,"width":0.032247342,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Update copyright","depth":2,"bounds":{"left":0.4587766,"top":0.29608938,"width":0.04488032,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Reformat code","depth":2,"bounds":{"left":0.4587766,"top":0.31923383,"width":0.03956117,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Rearrange code","depth":2,"bounds":{"left":0.4587766,"top":0.3423783,"width":0.041888297,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Optimize imports","depth":2,"bounds":{"left":0.4587766,"top":0.36552274,"width":0.044215426,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Cleanup","depth":2,"bounds":{"left":0.4587766,"top":0.3886672,"width":0.026263298,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.48902926,"top":0.39185953,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check malicious dependencies","depth":2,"bounds":{"left":0.4587766,"top":0.41181165,"width":0.07247341,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run external formatter","depth":2,"bounds":{"left":0.4587766,"top":0.4349561,"width":0.05518617,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.51795214,"top":0.43814844,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Perform SonarQube for IDE analysis","depth":2,"bounds":{"left":0.4587766,"top":0.45810056,"width":0.1200133,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Run Git hooks","depth":2,"on_screen":false,"help_text":"If unchecked, Git hooks will be skipped with the '--no-verify' option for the upcoming commit","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Advanced Commit Checks","depth":2,"bounds":{"left":0.45212767,"top":0.50039905,"width":0.05418883,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"Analyze code","depth":2,"bounds":{"left":0.4587766,"top":0.5203512,"width":0.036901597,"height":0.019952115},"on_screen":true,"help_text":"","role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Choose profile","depth":2,"bounds":{"left":0.49966756,"top":0.5235435,"width":0.029920213,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Check TODO","depth":2,"bounds":{"left":0.4587766,"top":0.5434956,"width":0.035904255,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Configure","depth":2,"bounds":{"left":0.49867022,"top":0.54668796,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"After Commit","depth":2,"bounds":{"left":0.45212767,"top":0.5857941,"width":0.027260639,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Upload files to:","depth":2,"bounds":{"left":0.46010637,"top":0.612929,"width":0.030585106,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Upload files to:","depth":2,"bounds":{"left":0.49168882,"top":0.6065443,"width":0.07679521,"height":0.027134877},"on_screen":true,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"<None>","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXCheckBox","text":"Always use selected server or group of servers","depth":2,"bounds":{"left":0.46675533,"top":0.63766956,"width":0.10538564,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous Difference","depth":2,"bounds":{"left":0.25664893,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Difference","depth":2,"bounds":{"left":0.26529256,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Jump to Source","depth":2,"bounds":{"left":0.27393618,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Previous File","depth":2,"bounds":{"left":0.28490692,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Compare Next File","depth":2,"bounds":{"left":0.29355052,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go to Changed File…","depth":2,"bounds":{"left":0.30219415,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Side-by-side viewer","depth":2,"bounds":{"left":0.3118351,"top":0.41340783,"width":0.04720745,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Do not ignore","depth":2,"bounds":{"left":0.36336437,"top":0.41340783,"width":0.03557181,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Highlight words","depth":2,"bounds":{"left":0.40093085,"top":0.41340783,"width":0.03956117,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse Unchanged Fragments","depth":2,"bounds":{"left":0.44148937,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Synchronize Scrolling","depth":2,"bounds":{"left":0.45013297,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Disable Editing","depth":2,"bounds":{"left":0.4587766,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Settings","depth":2,"bounds":{"left":0.46742022,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Help","depth":2,"bounds":{"left":0.47839096,"top":0.41340783,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5 differences","depth":1,"bounds":{"left":0.5515292,"top":0.41181165,"width":0.026928192,"height":0.022346368},"on_screen":true,"role_description":"text"},{"role":"AXTextField","text":"828865ed","depth":1,"bounds":{"left":0.26196808,"top":0.43415803,"width":0.024268618,"height":0.013567438},"on_screen":true,"value":"828865ed","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedOriginalTo = explode('+', $header->value)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedOriginalTo,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"bounds":{"left":0.25531915,"top":0.42697525,"width":0.29886967,"height":0.57302475},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedOriginalTo = explode('+', $header->value)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedOriginalTo,\n ]);\n }\n\n return $matches;\n }\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Current version","depth":1,"bounds":{"left":0.4375,"top":0.43415803,"width":0.14095744,"height":0.013567438},"on_screen":true,"value":"Current version","help_text":"text/plain","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","depth":2,"bounds":{"left":0.44215426,"top":0.42697525,"width":0.27992022,"height":0.57302475},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Mail;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Mailbox\\EmailTextRelay;\nuse Jiminny\\Models\\TextRelay;\nuse Google\\Service\\Gmail as GoogleGmail;\n\nclass TextRelayService\n{\n public function __construct()\n {\n $credentials = storage_path('text-relay.json');\n\n abort_unless(file_exists($credentials), 422);\n\n putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);\n }\n\n /**\n * Fetch the latest messages since the last sync.\n */\n public function sync(): array\n {\n $mailbox = config('jiminny.google_text_user');\n\n $expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';\n $expectedHost = config('jiminny.google_text_host');\n\n Log::info('[TextRelayService] Starting sync', [\n 'mailbox' => $mailbox,\n 'expected_alias' => $expectedAlias,\n 'expected_host' => $expectedHost,\n ]);\n\n $service = $this->getService($mailbox);\n\n $messageHistory = $this->getHistory($service);\n $messageIds = [];\n\n foreach ($messageHistory as $histories) {\n $messages = $histories->messagesAdded ?? [];\n\n foreach ($messages as $message) {\n $messageId = $message->message->id;\n\n if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {\n continue;\n }\n\n $relayedText = TextRelay::where('email_provider_id', $messageId)->first();\n\n if ($relayedText === null) {\n $relayedText = TextRelay::create([\n 'email_provider' => TextRelay::PROVIDER_GSUITE,\n 'email_provider_id' => $messageId,\n 'status' => TextRelay::STATUS_PROCESSING,\n ]);\n }\n\n $job = new EmailTextRelay($messageId, $relayedText);\n $job->onQueue(Constants::QUEUE_EMAILS);\n\n dispatch($job);\n\n Log::info('[TextRelayService] Successfully dispatched message', [\n 'message_id' => $messageId,\n 'text_relay_id' => $relayedText->id,\n 'queue' => Constants::QUEUE_EMAILS,\n ]);\n\n $messageIds[] = $messageId;\n }\n }\n\n Log::info('[TextRelayService] Sync completed', [\n 'mailbox' => $mailbox,\n 'messages_processed' => count($messageIds),\n 'message_ids' => array_slice($messageIds, 0, 10),\n ]);\n\n return $messageIds;\n }\n\n public function getHistory(GoogleGmail $service): array\n {\n $pageToken = null;\n $topic = config('jiminny.google_text_relay_topic');\n $messages = [];\n $historyId = \\Cache::get($topic);\n\n // If we have no history stored, WatchMailboxEvents must not have run yet :/\n if ($historyId == false) {\n $historyId = $this->refreshHistoryPoint($topic);\n }\n\n $params = [\n 'historyTypes' => 'messageAdded',\n 'startHistoryId' => $historyId,\n ];\n\n do {\n try {\n if ($pageToken) {\n $params['pageToken'] = $pageToken;\n }\n\n $historyResponse = $service->users_history->listUsersHistory(\n config('jiminny.google_text_user'),\n $params\n );\n\n $this->setHistoryPoint($topic, (int) $historyResponse->historyId);\n\n if ($historyResponse->getHistory()) {\n $messages = array_merge($messages, $historyResponse->getHistory());\n $pageToken = $historyResponse->getNextPageToken();\n }\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to fetch Gmail history', [\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n } while ($pageToken);\n\n return $messages;\n }\n\n protected function setHistoryPoint(string $topic, int $historyPoint): Carbon\n {\n $expiresAt = now()->addDay();\n\n \\Cache::put($topic, $historyPoint, $expiresAt);\n\n return $expiresAt;\n }\n\n public function getService(string $mailbox): GoogleGmail\n {\n $client = new \\Google_Client();\n $client->useApplicationDefaultCredentials();\n $client->addScope(GoogleGmail::GMAIL_MODIFY);\n $client->setAccessType('offline');\n $client->setSubject($mailbox);\n\n return new GoogleGmail($client);\n }\n\n public function refreshHistoryPoint(string $topic): int\n {\n $mailbox = config('jiminny.google_text_user');\n $service = $this->getService($mailbox);\n\n $watchRequest = new GoogleGmail\\WatchRequest();\n $watchRequest->setLabelIds(['INBOX']);\n $watchRequest->setLabelFilterAction('include');\n $watchRequest->setTopicName($topic);\n\n $watchResponse = $service->users->watch($mailbox, $watchRequest);\n\n //$expiryTimestamp = $watchResponse->expiration / 1000;\n $historyPoint = (int) $watchResponse->historyId;\n\n $this->setHistoryPoint($topic, $historyPoint);\n\n return $historyPoint;\n }\n\n private function isForCurrentEnvironment(\n GoogleGmail $service,\n string $mailbox,\n string $messageId,\n string $expectedAlias,\n string $expectedHost\n ): bool {\n try {\n $message = $service->users_messages->get($mailbox, $messageId);\n $headers = $message->getPayload()->getHeaders();\n\n $originalTo = null;\n $to = null;\n\n foreach ($headers as $header) {\n if ($header->name === 'X-Gm-Original-To') {\n $originalTo = $header->value;\n } elseif ($header->name === 'To') {\n $to = $header->value;\n }\n }\n\n $recipient = $originalTo ?? $to;\n\n if ($recipient !== null) {\n $matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);\n\n if (! $matches) {\n // Sanitize PII by removing plus-tag content for logging\n $sanitizedRecipient = explode('+', $recipient)[0];\n Log::info('[TextRelayService] Refused message', [\n 'message_id' => $messageId,\n 'original_to_sanitized' => $sanitizedRecipient,\n ]);\n }\n\n return $matches;\n }\n\n Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [\n 'message_id' => $messageId,\n ]);\n } catch (\\Exception $e) {\n Log::error('[TextRelayService] Failed to inspect message', [\n 'message_id' => $messageId,\n 'exception' => $e->getMessage(),\n ]);\n \\Sentry::captureException($e);\n }\n\n return false;\n }\n\n private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool\n {\n $parts = explode('@', $recipient, 2);\n\n if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {\n return false;\n }\n\n $localBase = explode('+', $parts[0], 2)[0];\n\n return strcasecmp($localBase, $expectedAlias) === 0;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1223364591573276281
|
-6438878625925788789
|
typing_pause
|
accessibility
|
NULL
|
Diff
Changelist:
Changelist:
Changes
Show Diff
Rol Diff
Changelist:
Changelist:
Changes
Show Diff
Rollback...
Refresh
Group By
Expand All
Collapse All
2 modified
JY-20
Commit Message
Commit Message History
Git
Author:
Author:
Amend commit
Sign-off commit
Create extra commit with file movements
Commit Checks
Update copyright
Reformat code
Rearrange code
Optimize imports
Cleanup
Choose profile
Check malicious dependencies
Run external formatter
Configure
Perform SonarQube for IDE analysis
Run Git hooks
Advanced Commit Checks
Analyze code
Choose profile
Check TODO
Configure
After Commit
Upload files to:
Upload files to:
<None>
Always use selected server or group of servers
Previous Difference
Next Difference
Jump to Source
Compare Previous File
Compare Next File
Go to Changed File…
Side-by-side viewer
Do not ignore
Highlight words
Collapse Unchanged Fragments
Synchronize Scrolling
Disable Editing
Settings
Help
5 differences
828865ed
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$matches = $this->matchesExpectedRecipient($header->value, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedOriginalTo = explode('+', $header->value)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedOriginalTo,
]);
}
return $matches;
}
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To header', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}
Current version
<?php
declare(strict_types=1);
namespace Jiminny\Services\Mail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Mailbox\EmailTextRelay;
use Jiminny\Models\TextRelay;
use Google\Service\Gmail as GoogleGmail;
class TextRelayService
{
public function __construct()
{
$credentials = storage_path('text-relay.json');
abort_unless(file_exists($credentials), 422);
putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $credentials);
}
/**
* Fetch the latest messages since the last sync.
*/
public function sync(): array
{
$mailbox = config('jiminny.google_text_user');
$expectedAlias = config('jiminny.deploy_region') === 'eu' ? 'catch-all-eu' : 'catch-all';
$expectedHost = config('jiminny.google_text_host');
Log::info('[TextRelayService] Starting sync', [
'mailbox' => $mailbox,
'expected_alias' => $expectedAlias,
'expected_host' => $expectedHost,
]);
$service = $this->getService($mailbox);
$messageHistory = $this->getHistory($service);
$messageIds = [];
foreach ($messageHistory as $histories) {
$messages = $histories->messagesAdded ?? [];
foreach ($messages as $message) {
$messageId = $message->message->id;
if (! $this->isForCurrentEnvironment($service, $mailbox, $messageId, $expectedAlias, $expectedHost)) {
continue;
}
$relayedText = TextRelay::where('email_provider_id', $messageId)->first();
if ($relayedText === null) {
$relayedText = TextRelay::create([
'email_provider' => TextRelay::PROVIDER_GSUITE,
'email_provider_id' => $messageId,
'status' => TextRelay::STATUS_PROCESSING,
]);
}
$job = new EmailTextRelay($messageId, $relayedText);
$job->onQueue(Constants::QUEUE_EMAILS);
dispatch($job);
Log::info('[TextRelayService] Successfully dispatched message', [
'message_id' => $messageId,
'text_relay_id' => $relayedText->id,
'queue' => Constants::QUEUE_EMAILS,
]);
$messageIds[] = $messageId;
}
}
Log::info('[TextRelayService] Sync completed', [
'mailbox' => $mailbox,
'messages_processed' => count($messageIds),
'message_ids' => array_slice($messageIds, 0, 10),
]);
return $messageIds;
}
public function getHistory(GoogleGmail $service): array
{
$pageToken = null;
$topic = config('jiminny.google_text_relay_topic');
$messages = [];
$historyId = \Cache::get($topic);
// If we have no history stored, WatchMailboxEvents must not have run yet :/
if ($historyId == false) {
$historyId = $this->refreshHistoryPoint($topic);
}
$params = [
'historyTypes' => 'messageAdded',
'startHistoryId' => $historyId,
];
do {
try {
if ($pageToken) {
$params['pageToken'] = $pageToken;
}
$historyResponse = $service->users_history->listUsersHistory(
config('jiminny.google_text_user'),
$params
);
$this->setHistoryPoint($topic, (int) $historyResponse->historyId);
if ($historyResponse->getHistory()) {
$messages = array_merge($messages, $historyResponse->getHistory());
$pageToken = $historyResponse->getNextPageToken();
}
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to fetch Gmail history', [
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
} while ($pageToken);
return $messages;
}
protected function setHistoryPoint(string $topic, int $historyPoint): Carbon
{
$expiresAt = now()->addDay();
\Cache::put($topic, $historyPoint, $expiresAt);
return $expiresAt;
}
public function getService(string $mailbox): GoogleGmail
{
$client = new \Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(GoogleGmail::GMAIL_MODIFY);
$client->setAccessType('offline');
$client->setSubject($mailbox);
return new GoogleGmail($client);
}
public function refreshHistoryPoint(string $topic): int
{
$mailbox = config('jiminny.google_text_user');
$service = $this->getService($mailbox);
$watchRequest = new GoogleGmail\WatchRequest();
$watchRequest->setLabelIds(['INBOX']);
$watchRequest->setLabelFilterAction('include');
$watchRequest->setTopicName($topic);
$watchResponse = $service->users->watch($mailbox, $watchRequest);
//$expiryTimestamp = $watchResponse->expiration / 1000;
$historyPoint = (int) $watchResponse->historyId;
$this->setHistoryPoint($topic, $historyPoint);
return $historyPoint;
}
private function isForCurrentEnvironment(
GoogleGmail $service,
string $mailbox,
string $messageId,
string $expectedAlias,
string $expectedHost
): bool {
try {
$message = $service->users_messages->get($mailbox, $messageId);
$headers = $message->getPayload()->getHeaders();
$originalTo = null;
$to = null;
foreach ($headers as $header) {
if ($header->name === 'X-Gm-Original-To') {
$originalTo = $header->value;
} elseif ($header->name === 'To') {
$to = $header->value;
}
}
$recipient = $originalTo ?? $to;
if ($recipient !== null) {
$matches = $this->matchesExpectedRecipient($recipient, $expectedAlias, $expectedHost);
if (! $matches) {
// Sanitize PII by removing plus-tag content for logging
$sanitizedRecipient = explode('+', $recipient)[0];
Log::info('[TextRelayService] Refused message', [
'message_id' => $messageId,
'original_to_sanitized' => $sanitizedRecipient,
]);
}
return $matches;
}
Log::warning('[TextRelayService] Refused message: missing X-Gm-Original-To and To headers', [
'message_id' => $messageId,
]);
} catch (\Exception $e) {
Log::error('[TextRelayService] Failed to inspect message', [
'message_id' => $messageId,
'exception' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return false;
}
private function matchesExpectedRecipient(string $recipient, string $expectedAlias, string $expectedHost): bool
{
$parts = explode('@', $recipient, 2);
if (count($parts) !== 2 || strcasecmp($parts[1], $expectedHost) !== 0) {
return false;
}
$localBase = explode('+', $parts[0], 2)[0];
return strcasecmp($localBase, $expectedAlias) === 0;
}
}...
|
NULL
|
NULL
|
NULL
|
NULL
|