get()->filter( fn (MailOAuthConnection $connection): bool => $connection->usable(), ); if ($connections->isEmpty()) { $this->info('No connected OAuth mailboxes; nothing to refresh.'); return self::SUCCESS; } foreach ($connections as $connection) { $hadError = $connection->last_error !== null; try { // Serialised against sends: refresh() on its own is the // other half of the race freshAccessToken()'s lock is // there to stop. $refreshed = $brokers->for($connection->provider)->refreshSerially($connection); // Standing aside is a healthy outcome, not a silent one: // somebody else is refreshing this very connection, which // slides the window just as well. Saying "Refreshed" for // it would describe a token request that never happened. $this->info($refreshed ? "Refreshed {$connection->provider->value} ({$connection->account_email})." : "Skipped {$connection->provider->value} ({$connection->account_email}): a refresh is already in progress."); // Back from the dead (an admin fixed things upstream // without reconnecting): the applier may have been // resolving "not ready" and must see the recovery. if ($hadError) { $mailConfig->flush(); } } catch (MailOAuthException $e) { $this->error("Could not refresh {$connection->provider->value}: {$e->getMessage()}"); if (! $e->needsReconnect) { continue; } // Only on the transition into the broken state — the // notification would otherwise repeat daily for as long // as nobody reconnects, and a nagging alert trains // people to ignore the one that matters. if (! $hadError) { $recipients = array_values(User::query()->where('type', UserType::Staff)->get() ->filter(fn (User $staff): bool => $permissions->allows($staff, Permission::EditSettings)) ->all()); $notifier->send('mail_oauth_connection_broken', $recipients, data: [ 'provider' => $connection->provider->label(), 'account' => (string) $connection->account_email, ]); } $mailConfig->flush(); } } return self::SUCCESS; } }