mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-10-23 11:08:54 +00:00
[Web] Add Mailcow\ class structure and imapsync module classes
This commit is contained in:
26 files changed
+2821
No files matched your search
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\Imapsync\Repository;
|
||||
|
||||
use Mailcow\Core\Repository;
|
||||
use Mailcow\Imapsync\Model\Job;
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* SQL for sync jobs (table `imapsync`).
|
||||
*/
|
||||
class JobRepository extends Repository
|
||||
{
|
||||
/** Job queue order (C4): prio desc, never-run first, then least recently run */
|
||||
private const QUEUE_ORDER = "ORDER BY `prio` DESC, `last_run` IS NOT NULL, `last_run` ASC, `id` ASC";
|
||||
|
||||
/** Every column except returned_text (the log, loaded on demand) */
|
||||
private const COLUMNS = ['id', 'user2', 'regextrans2', 'authmd51', 'domain2', 'subfolder2', 'user1', 'password1',
|
||||
'exclude', 'maxage', 'mins_interval', 'maxbytespersecond', 'delete2duplicates', 'delete1', 'delete2', 'automap',
|
||||
'skipcrossduplicates', 'custom_params', 'timeout1', 'timeout2', 'subscribeall', 'dry', 'is_running',
|
||||
'last_run', 'success', 'exit_status', 'created', 'modified', 'active', 'source_id', 'custom_params_legacy', 'prio'];
|
||||
|
||||
/**
|
||||
* The job with display data of its source and per-user token (C1: source, owner = user2, username = user1).
|
||||
* $withLog also loads returned_text.
|
||||
*/
|
||||
public function find(int $id, bool $withLog = true): ?Job
|
||||
{
|
||||
$columns = $withLog ? [...self::COLUMNS, 'returned_text'] : self::COLUMNS;
|
||||
$row = $this->query("SELECT i.`" . implode("`, i.`", $columns) . "`,
|
||||
s.`name` AS source_name, s.`description` AS source_description, s.`host1` AS source_host1,
|
||||
s.`port1` AS source_port1, s.`enc1` AS source_enc1, s.`auth_type` AS source_auth_type,
|
||||
s.`created_by` AS source_created_by, s.`scope` AS source_scope, s.`oauth_flow` AS source_oauth_flow,
|
||||
s.`active` AS source_active, s.`oauth_token_expires` AS source_oauth_token_expires,
|
||||
s.`oauth_last_refresh_error` AS source_oauth_last_refresh_error,
|
||||
t.`token_expires` AS user_token_expires, t.`last_refresh_error` AS user_token_error,
|
||||
CASE WHEN t.`username` IS NULL THEN 0 ELSE 1 END AS user_token_exists
|
||||
FROM `imapsync` i
|
||||
LEFT JOIN `imapsync_source` s ON i.`source_id` = s.`id`
|
||||
LEFT JOIN `imapsync_source_oauth_token` t
|
||||
ON t.`source_id` = i.`source_id` AND t.`owner` = i.`user2` AND t.`username` = i.`user1`
|
||||
WHERE i.`id` = :id", ['id' => $id])->fetch(PDO::FETCH_ASSOC);
|
||||
return $row ? Job::fromRow($row) : null;
|
||||
}
|
||||
|
||||
/** @return int[] job ids of a local mailbox in queue order */
|
||||
public function idsForMailbox(string $user2): array
|
||||
{
|
||||
return array_map('intval', $this->query("SELECT `id` FROM `imapsync` WHERE `user2` = :user2 " . self::QUEUE_ORDER,
|
||||
['user2' => $user2])->fetchAll(PDO::FETCH_COLUMN));
|
||||
}
|
||||
|
||||
/** @return int[] all job ids (incl. orphaned ones) in queue order */
|
||||
public function allIds(): array
|
||||
{
|
||||
return array_map('intval', $this->query("SELECT `id` FROM `imapsync` " . self::QUEUE_ORDER)->fetchAll(PDO::FETCH_COLUMN));
|
||||
}
|
||||
|
||||
/** True when another job (not $exceptId) already syncs (source, user1) into user2 */
|
||||
public function tupleExists(int $sourceId, string $user2, string $user1, ?int $exceptId = null): bool
|
||||
{
|
||||
return (bool)$this->query("SELECT 1 FROM `imapsync`
|
||||
WHERE `user2` = :user2 AND `user1` = :user1 AND `source_id` = :source_id AND `id` != :id",
|
||||
['user2' => $user2, 'user1' => $user1, 'source_id' => $sourceId, 'id' => $exceptId ?? 0])->fetchColumn();
|
||||
}
|
||||
|
||||
public function insert(Job $job): int
|
||||
{
|
||||
return $job->id = $this->insertRow('imapsync', $this->values($job) + ['user2' => $job->user2]);
|
||||
}
|
||||
|
||||
public function update(Job $job): void
|
||||
{
|
||||
$this->updateRow('imapsync', (int)$job->id,
|
||||
$this->values($job) + ['last_run' => $job->lastRun, 'success' => $job->success, 'prio' => $job->prio]);
|
||||
}
|
||||
|
||||
public function delete(int $id): void
|
||||
{
|
||||
$this->query("DELETE FROM `imapsync` WHERE `id` = :id", ['id' => $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves idle jobs to the front of the queue: each gets a prio above the current global max
|
||||
* (lowest current prio first) and last_run/success are cleared. The rows are locked first, so the
|
||||
* runner cannot claim a job in between (its claim UPDATE waits for this transaction).
|
||||
*
|
||||
* @param int[] $ids
|
||||
* @return array{running: int[], moved: int} ids that are running (left alone) and the number of moved jobs
|
||||
*/
|
||||
public function moveToFront(array $ids): array
|
||||
{
|
||||
return $this->transaction(function () use ($ids): array {
|
||||
$rows = $this->query("SELECT `id`, `is_running` FROM `imapsync` WHERE `id` IN (" . self::placeholders($ids) . ") FOR UPDATE",
|
||||
$ids)->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
$running = array_keys(array_filter($rows, fn($isRunning): bool => (int)$isRunning === 1));
|
||||
$idle = array_values(array_diff(array_keys($rows), $running));
|
||||
$moved = 0;
|
||||
if (!empty($idle)) {
|
||||
$this->pdo->exec("SET @m := (SELECT COALESCE(MAX(`prio`), 0) FROM `imapsync`)");
|
||||
$this->pdo->exec("SET @r := 0");
|
||||
$moved = $this->query("UPDATE `imapsync` SET `prio` = @m + (@r := @r + 1), `last_run` = NULL, `success` = NULL
|
||||
WHERE `id` IN (" . self::placeholders($idle) . ") AND `is_running` = 0 ORDER BY `prio` ASC, `id` ASC", $idle)->rowCount();
|
||||
}
|
||||
return ['running' => $running, 'moved' => $moved];
|
||||
});
|
||||
}
|
||||
|
||||
/** Values of the columns a user can edit (shared by insert and update) */
|
||||
private function values(Job $job): array
|
||||
{
|
||||
$values = ['mins_interval' => $job->minsInterval, 'maxbytespersecond' => $job->maxBytesPerSecond,
|
||||
'custom_params' => $job->customParams, 'source_id' => $job->sourceId];
|
||||
// Columns with a property of the same name
|
||||
foreach (['user1', 'password1', 'subfolder2', 'exclude', 'maxage', 'timeout1', 'timeout2', 'active', 'delete1', 'delete2',
|
||||
'delete2duplicates', 'automap', 'skipcrossduplicates', 'subscribeall', 'dry'] as $column) {
|
||||
$values[$column] = $job->$column;
|
||||
}
|
||||
return $values;
|
||||
}
|
||||
|
||||
private static function placeholders(array $values): string
|
||||
{
|
||||
return implode(',', array_fill(0, count($values), '?'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\Imapsync\Repository;
|
||||
|
||||
use Mailcow\Core\Repository;
|
||||
use Mailcow\Imapsync\Model\Settings;
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* SQL for the global sync job settings (table `imapsync_settings`, name => value).
|
||||
*/
|
||||
class SettingsRepository extends Repository
|
||||
{
|
||||
/** Stored settings; keys that are not stored yet keep the defaults of the model */
|
||||
public function load(): Settings
|
||||
{
|
||||
$rows = $this->query("SELECT `name`, `value` FROM `imapsync_settings`")->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
$settings = new Settings();
|
||||
$settings->maxParallel = (int)($rows['max_parallel'] ?? $settings->maxParallel);
|
||||
$settings->maxBytesPerSecond = (int)($rows['max_bytes_per_second'] ?? $settings->maxBytesPerSecond);
|
||||
return $settings;
|
||||
}
|
||||
|
||||
public function save(Settings $settings): void
|
||||
{
|
||||
$sql = "INSERT INTO `imapsync_settings` (`name`, `value`) VALUES (:name, :value) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)";
|
||||
$this->query($sql, ['name' => 'max_parallel', 'value' => (string)$settings->maxParallel]);
|
||||
$this->query($sql, ['name' => 'max_bytes_per_second', 'value' => (string)$settings->maxBytesPerSecond]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\Imapsync\Repository;
|
||||
|
||||
use Mailcow\Core\Context;
|
||||
use Mailcow\Core\Repository;
|
||||
use Mailcow\Imapsync\Model\Source;
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* SQL for `imapsync_source` and its target tables `imapsync_source_domain` / `imapsync_source_user`.
|
||||
* No permission decisions here except the visibility filter, which is pure SQL (see visibility()).
|
||||
*/
|
||||
class SourceRepository extends Repository
|
||||
{
|
||||
/** Full source incl. secrets and targets, without visibility filter */
|
||||
public function find(int $id): ?Source
|
||||
{
|
||||
return $this->fetch('1=1', ['id' => $id]);
|
||||
}
|
||||
|
||||
/** Like find(), but only when the source is visible to the caller */
|
||||
public function findVisible(int $id, Context $context): ?Source
|
||||
{
|
||||
[$visible, $params] = $this->visibility($context);
|
||||
return $this->fetch($visible, $params + ['id' => $id]);
|
||||
}
|
||||
|
||||
/** @return int[] ids of all sources visible to the caller, ordered by name */
|
||||
public function visibleIds(Context $context): array
|
||||
{
|
||||
[$visible, $params] = $this->visibility($context);
|
||||
return array_map('intval', $this->query("SELECT s.`id` FROM `imapsync_source` s WHERE $visible ORDER BY s.`name`, s.`id`",
|
||||
$params)->fetchAll(PDO::FETCH_COLUMN));
|
||||
}
|
||||
|
||||
public function nameExists(string $name, string $createdBy, ?int $exceptId = null): bool
|
||||
{
|
||||
return (bool)$this->query("SELECT 1 FROM `imapsync_source` WHERE `name` = :name AND `created_by` = :created_by AND `id` != :id",
|
||||
['name' => $name, 'created_by' => $createdBy, 'id' => $exceptId ?? 0])->fetchColumn();
|
||||
}
|
||||
|
||||
/** True while a sync job references the source */
|
||||
public function isInUse(int $id): bool
|
||||
{
|
||||
return (bool)$this->query("SELECT 1 FROM `imapsync` WHERE `source_id` = :id LIMIT 1", ['id' => $id])->fetchColumn();
|
||||
}
|
||||
|
||||
public function isSuperAdmin(string $username): bool
|
||||
{
|
||||
return (bool)$this->query("SELECT 1 FROM `admin` WHERE `username` = :username AND `superadmin` = '1'",
|
||||
['username' => $username])->fetchColumn();
|
||||
}
|
||||
|
||||
/** Inserts the source with its targets and returns the new id */
|
||||
public function insert(Source $source): int
|
||||
{
|
||||
return $this->transaction(function () use ($source): int {
|
||||
$source->id = $this->insertRow('imapsync_source',
|
||||
$this->values($source) + ['created_by' => $source->createdBy, 'scope' => $source->scope]);
|
||||
$this->writeTargets($source);
|
||||
return $source->id;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the source. $writeTargets also rewrites scope + target lists, $resetTokens drops the shared
|
||||
* token and all per-user tokens of the source. All in one transaction.
|
||||
*/
|
||||
public function update(Source $source, bool $writeTargets, bool $resetTokens): void
|
||||
{
|
||||
$this->transaction(function () use ($source, $writeTargets, $resetTokens): void {
|
||||
$id = ['id' => $source->id];
|
||||
$this->updateRow('imapsync_source', (int)$source->id, $this->values($source));
|
||||
if ($writeTargets) {
|
||||
$this->query("DELETE FROM `imapsync_source_domain` WHERE `source_id` = :id", $id);
|
||||
$this->query("DELETE FROM `imapsync_source_user` WHERE `source_id` = :id", $id);
|
||||
$this->writeTargets($source);
|
||||
}
|
||||
if ($resetTokens) {
|
||||
$this->query("UPDATE `imapsync_source` SET `oauth_access_token` = NULL, `oauth_token_expires` = NULL,
|
||||
`oauth_last_refresh_error` = NULL, `oauth_last_refresh_attempt` = NULL WHERE `id` = :id", $id);
|
||||
$this->query("DELETE FROM `imapsync_source_oauth_token` WHERE `source_id` = :id", $id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Deletes the source; targets and per-user tokens go via FK ON DELETE CASCADE */
|
||||
public function delete(int $id): void
|
||||
{
|
||||
$this->query("DELETE FROM `imapsync_source` WHERE `id` = :id", ['id' => $id]);
|
||||
}
|
||||
|
||||
/** Deletes the source and its child rows explicitly, for callers running with FOREIGN_KEY_CHECKS = 0 (mailbox rename) */
|
||||
public function deleteWithChildren(int $id): void
|
||||
{
|
||||
foreach (['imapsync_source_oauth_token', 'imapsync_source_user', 'imapsync_source_domain'] as $table) {
|
||||
$this->query("DELETE FROM `$table` WHERE `source_id` = :id", ['id' => $id]);
|
||||
}
|
||||
$this->delete($id);
|
||||
}
|
||||
|
||||
/** @return Source[] sources created by an account (id and name only) */
|
||||
public function listCreatedBy(string $username): array
|
||||
{
|
||||
return array_map([Source::class, 'fromRow'], $this->query("SELECT `id`, `name` FROM `imapsync_source` WHERE `created_by` = :username",
|
||||
['username' => $username])->fetchAll(PDO::FETCH_ASSOC));
|
||||
}
|
||||
|
||||
/** Hands a source over to the admins (created_by '') under the given name */
|
||||
public function handOver(int $id, string $name): void
|
||||
{
|
||||
$this->query("UPDATE `imapsync_source` SET `created_by` = '', `name` = :name WHERE `id` = :id", ['name' => $name, 'id' => $id]);
|
||||
}
|
||||
|
||||
/** Follows an account rename: creator and user targets */
|
||||
public function renameAccount(string $old, string $new): void
|
||||
{
|
||||
$this->query("UPDATE `imapsync_source` SET `created_by` = :new WHERE `created_by` = :old", ['new' => $new, 'old' => $old]);
|
||||
$this->query("UPDATE `imapsync_source_user` SET `username` = :new WHERE `username` = :old", ['new' => $new, 'old' => $old]);
|
||||
}
|
||||
|
||||
/** Removes a deleted account from all user target lists */
|
||||
public function deleteUserTarget(string $username): void
|
||||
{
|
||||
$this->query("DELETE FROM `imapsync_source_user` WHERE `username` = :username", ['username' => $username]);
|
||||
}
|
||||
|
||||
private function fetch(string $where, array $params): ?Source
|
||||
{
|
||||
$row = $this->query("SELECT s.* FROM `imapsync_source` s WHERE s.`id` = :id AND $where", $params)->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$row) {
|
||||
return null;
|
||||
}
|
||||
$source = Source::fromRow($row);
|
||||
$id = ['id' => $source->id];
|
||||
$source->domains = $this->query("SELECT `domain` FROM `imapsync_source_domain` WHERE `source_id` = :id ORDER BY `domain`",
|
||||
$id)->fetchAll(PDO::FETCH_COLUMN);
|
||||
$source->users = $this->query("SELECT `username` FROM `imapsync_source_user` WHERE `source_id` = :id ORDER BY `username`",
|
||||
$id)->fetchAll(PDO::FETCH_COLUMN);
|
||||
return $source;
|
||||
}
|
||||
|
||||
/**
|
||||
* WHERE fragment over `imapsync_source s` that limits rows to what the caller may see, and its parameters.
|
||||
* admin everything
|
||||
* domainadmin own + 'all' + sources targeting his domains or their mailboxes + sources created by them
|
||||
* user own + 'all' + sources targeting him or his domain
|
||||
*
|
||||
* @return array{0: string, 1: array}
|
||||
*/
|
||||
private function visibility(Context $context): array
|
||||
{
|
||||
$username = $context->username;
|
||||
if ($context->isAdmin()) {
|
||||
return ['1=1', []];
|
||||
}
|
||||
// Native prepares: a placeholder may appear only once, so the username is bound under one name per use
|
||||
$managed = "SELECT `domain` FROM `domain_admins` WHERE `active` = '1' AND `username` = ";
|
||||
if ($context->isDomainAdmin()) {
|
||||
return ["(s.`created_by` = :vis_self OR s.`scope` = 'all'
|
||||
OR (s.`scope` = 'domain' AND EXISTS (SELECT 1 FROM `imapsync_source_domain` t
|
||||
WHERE t.`source_id` = s.`id` AND t.`domain` IN ($managed :vis_d1)))
|
||||
OR (s.`scope` = 'user' AND EXISTS (SELECT 1 FROM `imapsync_source_user` t JOIN `mailbox` mb ON mb.`username` = t.`username`
|
||||
WHERE t.`source_id` = s.`id` AND mb.`domain` IN ($managed :vis_d2)))
|
||||
OR s.`created_by` IN (SELECT `username` FROM `mailbox` WHERE `domain` IN ($managed :vis_d3)))",
|
||||
['vis_self' => $username, 'vis_d1' => $username, 'vis_d2' => $username, 'vis_d3' => $username]];
|
||||
}
|
||||
if ($context->isUser()) {
|
||||
return ["(s.`created_by` = :vis_self OR s.`scope` = 'all'
|
||||
OR (s.`scope` = 'user' AND EXISTS (SELECT 1 FROM `imapsync_source_user` t
|
||||
WHERE t.`source_id` = s.`id` AND t.`username` = :vis_u1))
|
||||
OR (s.`scope` = 'domain' AND EXISTS (SELECT 1 FROM `imapsync_source_domain` t JOIN `mailbox` mb ON mb.`domain` = t.`domain`
|
||||
WHERE t.`source_id` = s.`id` AND mb.`username` = :vis_u2)))",
|
||||
['vis_self' => $username, 'vis_u1' => $username, 'vis_u2' => $username]];
|
||||
}
|
||||
return ['0=1', []];
|
||||
}
|
||||
|
||||
private function writeTargets(Source $source): void
|
||||
{
|
||||
foreach ($source->domains as $domain) {
|
||||
$this->query("INSERT INTO `imapsync_source_domain` (`source_id`, `domain`) VALUES (:id, :domain)",
|
||||
['id' => $source->id, 'domain' => $domain]);
|
||||
}
|
||||
foreach ($source->users as $username) {
|
||||
$this->query("INSERT INTO `imapsync_source_user` (`source_id`, `username`) VALUES (:id, :username)",
|
||||
['id' => $source->id, 'username' => $username]);
|
||||
}
|
||||
$this->query("UPDATE `imapsync_source` SET `scope` = :scope WHERE `id` = :id", ['scope' => $source->scope, 'id' => $source->id]);
|
||||
}
|
||||
|
||||
private function values(Source $source): array
|
||||
{
|
||||
return [
|
||||
'name' => $source->name,
|
||||
'description' => $source->description,
|
||||
'host1' => $source->host,
|
||||
'port1' => $source->port,
|
||||
'enc1' => $source->enc,
|
||||
'auth_type' => $source->authType,
|
||||
'oauth_flow' => $source->oauthFlow,
|
||||
'oauth_token_endpoint' => $source->tokenEndpoint,
|
||||
'oauth_authorize_endpoint' => $source->authorizeEndpoint,
|
||||
'oauth_userinfo_endpoint' => $source->userinfoEndpoint,
|
||||
'oauth_client_id' => $source->clientId,
|
||||
'oauth_client_secret' => $source->clientSecret,
|
||||
'oauth_scope' => $source->oauthScope,
|
||||
'oauth_extra_params' => $source->extraParams,
|
||||
'active' => $source->active,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\Imapsync\Repository;
|
||||
|
||||
use Mailcow\Core\Repository;
|
||||
use Mailcow\Imapsync\Model\OAuthToken;
|
||||
use Mailcow\Imapsync\Model\Source;
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* SQL for OAuth tokens: the shared token of a client_credentials source (columns of `imapsync_source`)
|
||||
* and the per-user tokens of authorization_code sources (`imapsync_source_oauth_token`).
|
||||
*/
|
||||
class TokenRepository extends Repository
|
||||
{
|
||||
private const USER_TOKEN = "`source_id` = :sid AND `owner` = :owner AND `username` = :user";
|
||||
|
||||
public function exists(int $sourceId, string $owner, string $username): bool
|
||||
{
|
||||
return (bool)$this->query("SELECT 1 FROM `imapsync_source_oauth_token` WHERE " . self::USER_TOKEN,
|
||||
['sid' => $sourceId, 'owner' => $owner, 'user' => $username])->fetchColumn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the token of a finished browser sign-in. An existing refresh token is kept when the
|
||||
* provider sent none (Microsoft only sends one with offline_access).
|
||||
*/
|
||||
public function saveUserToken(OAuthToken $token): void
|
||||
{
|
||||
$this->query("INSERT INTO `imapsync_source_oauth_token`
|
||||
(`source_id`, `owner`, `username`, `access_token`, `refresh_token`, `token_expires`, `last_refresh_error`, `oauth_last_refresh_attempt`)
|
||||
VALUES (:sid, :owner, :user, :acc, :ref, :exp, NULL, NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`access_token` = VALUES(`access_token`),
|
||||
`refresh_token` = IF(VALUES(`refresh_token`) = '', `refresh_token`, VALUES(`refresh_token`)),
|
||||
`token_expires` = VALUES(`token_expires`),
|
||||
`last_refresh_error` = NULL,
|
||||
`oauth_last_refresh_attempt` = NOW()",
|
||||
$this->key($token) + ['acc' => $token->accessToken, 'ref' => (string)$token->refreshToken, 'exp' => $token->tokenExpires]);
|
||||
}
|
||||
|
||||
/** Stores a refreshed per-user token and clears its last error */
|
||||
public function storeUserRefresh(OAuthToken $token): void
|
||||
{
|
||||
$this->query("UPDATE `imapsync_source_oauth_token` SET `access_token` = :acc, `refresh_token` = :ref,
|
||||
`token_expires` = :exp, `last_refresh_error` = NULL, `oauth_last_refresh_attempt` = NOW() WHERE " . self::USER_TOKEN,
|
||||
$this->key($token) + ['acc' => $token->accessToken, 'ref' => $token->refreshToken, 'exp' => $token->tokenExpires]);
|
||||
}
|
||||
|
||||
public function storeUserError(OAuthToken $token, string $error): void
|
||||
{
|
||||
$this->query("UPDATE `imapsync_source_oauth_token` SET `last_refresh_error` = :err, `oauth_last_refresh_attempt` = NOW()
|
||||
WHERE " . self::USER_TOKEN, $this->key($token) + ['err' => $error]);
|
||||
}
|
||||
|
||||
public function storeSourceToken(int $sourceId, string $accessToken, int $expires): void
|
||||
{
|
||||
$this->query("UPDATE `imapsync_source` SET `oauth_access_token` = :tok, `oauth_token_expires` = :exp,
|
||||
`oauth_last_refresh_error` = NULL, `oauth_last_refresh_attempt` = NOW() WHERE `id` = :id",
|
||||
['tok' => $accessToken, 'exp' => $expires, 'id' => $sourceId]);
|
||||
}
|
||||
|
||||
public function storeSourceError(int $sourceId, string $error): void
|
||||
{
|
||||
$this->query("UPDATE `imapsync_source` SET `oauth_last_refresh_error` = :err, `oauth_last_refresh_attempt` = NOW()
|
||||
WHERE `id` = :id", ['err' => $error, 'id' => $sourceId]);
|
||||
}
|
||||
|
||||
/** @return Source[] active client_credentials sources (incl. secrets), each with refreshAttemptAge */
|
||||
public function refreshableSources(): array
|
||||
{
|
||||
return array_map([Source::class, 'fromRow'], $this->query("SELECT *,
|
||||
TIMESTAMPDIFF(SECOND, `oauth_last_refresh_attempt`, NOW()) AS `refresh_attempt_age`
|
||||
FROM `imapsync_source`
|
||||
WHERE `active` = 1 AND `auth_type` = 'XOAUTH2' AND `oauth_flow` = 'client_credentials'
|
||||
ORDER BY `id`")->fetchAll(PDO::FETCH_ASSOC));
|
||||
}
|
||||
|
||||
/** @return OAuthToken[] per-user tokens with a refresh token on active authorization_code sources, with refreshAttemptAge */
|
||||
public function refreshableUserTokens(): array
|
||||
{
|
||||
return array_map([OAuthToken::class, 'fromRow'], $this->query("SELECT t.*,
|
||||
TIMESTAMPDIFF(SECOND, t.`oauth_last_refresh_attempt`, NOW()) AS `refresh_attempt_age`
|
||||
FROM `imapsync_source_oauth_token` t
|
||||
JOIN `imapsync_source` s ON s.`id` = t.`source_id`
|
||||
WHERE s.`active` = 1 AND s.`auth_type` = 'XOAUTH2' AND s.`oauth_flow` = 'authorization_code'
|
||||
AND t.`refresh_token` <> ''
|
||||
ORDER BY t.`source_id`, t.`owner`, t.`username`")->fetchAll(PDO::FETCH_ASSOC));
|
||||
}
|
||||
|
||||
/** Drops a per-user token once no job uses (source, owner, username) anymore */
|
||||
public function deleteIfUnused(int $sourceId, string $owner, string $username): void
|
||||
{
|
||||
$this->query("DELETE FROM `imapsync_source_oauth_token` WHERE " . self::USER_TOKEN . "
|
||||
AND NOT EXISTS (SELECT 1 FROM `imapsync` WHERE `source_id` = :sid2 AND `user2` = :owner2 AND `user1` = :user2)",
|
||||
['sid' => $sourceId, 'owner' => $owner, 'user' => $username, 'sid2' => $sourceId, 'owner2' => $owner, 'user2' => $username]);
|
||||
}
|
||||
|
||||
/** Drops per-user tokens that no job uses and that are older than $seconds. Returns the number of rows. */
|
||||
public function deleteUnusedOlderThan(int $seconds): int
|
||||
{
|
||||
return $this->query("DELETE t FROM `imapsync_source_oauth_token` t
|
||||
LEFT JOIN `imapsync` i ON i.`source_id` = t.`source_id` AND i.`user2` = t.`owner` AND i.`user1` = t.`username`
|
||||
WHERE i.`id` IS NULL AND t.`created` < NOW() - INTERVAL :seconds SECOND", ['seconds' => $seconds])->rowCount();
|
||||
}
|
||||
|
||||
public function deleteByOwner(string $owner): void
|
||||
{
|
||||
$this->query("DELETE FROM `imapsync_source_oauth_token` WHERE `owner` = :owner", ['owner' => $owner]);
|
||||
}
|
||||
|
||||
public function renameOwner(string $old, string $new): void
|
||||
{
|
||||
$this->query("UPDATE `imapsync_source_oauth_token` SET `owner` = :new WHERE `owner` = :old", ['new' => $new, 'old' => $old]);
|
||||
}
|
||||
|
||||
private function key(OAuthToken $token): array
|
||||
{
|
||||
return ['sid' => $token->sourceId, 'owner' => $token->owner, 'user' => $token->username];
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user