MailProvider::class, 'client_secret' => 'encrypted', 'access_token' => 'encrypted', 'refresh_token' => 'encrypted', 'token_expires_at' => 'datetime', 'last_refreshed_at' => 'datetime', 'broken_notified_at' => 'datetime', ]; } /** * The failure is over: the error and the record of having alarmed * about it go together, because they describe one state. * * One method rather than two nulls at each call site. The three * places that end a failure — a successful refresh, a disconnect, a * changed client id — must never clear one and keep the other: a * connection that is healthy but still marked "already told them" * would go quiet the next time it dies, which is the shape of the * bug this column was added to close. */ public function clearFailure(): void { $this->last_error = null; $this->broken_notified_at = null; } public static function for(MailProvider $provider): self { return static::query()->firstOrNew(['provider' => $provider->value]); } /** * Whether the connect flow can be started: the admin has entered the * app registration, even if no mailbox is connected yet. */ public function configured(): bool { return $this->filled('client_id') && $this->filled('client_secret'); } /** * Whether transports can send through this connection. A half-torn * state (configured but never connected, or tokens cleared by a * disconnect) behaves as "not usable" rather than failing inside a * queued job — the same rule SocialSettings::usable() follows. */ public function usable(): bool { return $this->configured() && $this->filled('refresh_token'); } private function filled(string $attribute): bool { $value = $this->getAttribute($attribute); return is_string($value) && trim($value) !== ''; } }