'danger', 'log' => array('imapsync_source_apply_scope', 'edit', 'source', $_data_log), 'msg' => 'access_denied' ); return false; }; if (!in_array($scope, array('all', 'domain', 'user'))) return $deny(); // Only admins may create globally visible sources; users are always private. if ($scope == 'all' && $role != 'admin') return $deny(); if ($role == 'user') { $scope = 'user'; $domains = array(); $users = array(); } // Guardrail: an XOAUTH2 client_credentials source uses one app-wide token that can reach // every mailbox — it must never be shareable. Force it private to its creator. $src = $pdo->prepare("SELECT `auth_type`, `oauth_flow` FROM `imapsync_source` WHERE `id` = :id"); $src->execute(array(':id' => $source_id)); $src = $src->fetch(PDO::FETCH_ASSOC); if ($src && $src['auth_type'] == 'XOAUTH2' && $src['oauth_flow'] == 'client_credentials') { $scope = 'user'; $domains = array(); $users = array(); } $domains = ($scope == 'domain') ? array_unique(array_filter((array)$domains)) : array(); $users = ($scope == 'user') ? array_unique(array_filter((array)$users)) : array(); // A user-scoped source with no explicit targets is private to its creator (valid). // A domain-scoped source must name at least one domain. if ($scope == 'domain' && empty($domains)) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array('imapsync_source_apply_scope', 'edit', 'source', $_data_log), 'msg' => 'imapsync_source_scope_empty' ); return false; } foreach ($domains as $d) { if (!hasDomainAccess($username, $role, $d)) return $deny(); } foreach ($users as $u) { if (!hasMailboxObjectAccess($username, $role, $u)) return $deny(); } $pdo->prepare("DELETE FROM `imapsync_source_domain` WHERE `source_id` = :id")->execute(array(':id' => $source_id)); $pdo->prepare("DELETE FROM `imapsync_source_user` WHERE `source_id` = :id")->execute(array(':id' => $source_id)); $ins_d = $pdo->prepare("INSERT INTO `imapsync_source_domain` (`source_id`, `domain`) VALUES (:id, :domain)"); foreach ($domains as $d) { $ins_d->execute(array(':id' => $source_id, ':domain' => $d)); } $ins_u = $pdo->prepare("INSERT INTO `imapsync_source_user` (`source_id`, `username`) VALUES (:id, :username)"); foreach ($users as $u) { $ins_u->execute(array(':id' => $source_id, ':username' => $u)); } // apply_scope owns the scope column so the guardrail/coercion above stays consistent with the targets $pdo->prepare("UPDATE `imapsync_source` SET `scope` = :scope WHERE `id` = :id") ->execute(array(':scope' => $scope, ':id' => $source_id)); return true; } function imapsync_source_refresh_token_internal($source_id) { // Context-free: invoked by web UI (with ACL upstream) and by cron (no session). // Performs OAuth2 Client-Credentials Grant against source.oauth_token_endpoint, // stores the resulting access_token + absolute expiry timestamp, or records the error. global $pdo; $stmt = $pdo->prepare("SELECT * FROM `imapsync_source` WHERE `id` = :id AND `auth_type` = 'XOAUTH2'"); $stmt->execute(array(':id' => $source_id)); $src = $stmt->fetch(PDO::FETCH_ASSOC); if (!$src) return false; $body = array( 'grant_type' => 'client_credentials', 'client_id' => $src['oauth_client_id'], 'client_secret' => $src['oauth_client_secret'], 'scope' => $src['oauth_scope'], ); if (!empty($src['oauth_extra_params'])) { $extra = json_decode($src['oauth_extra_params'], true); if (is_array($extra)) { $body = array_merge($body, $extra); } } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $src['oauth_token_endpoint']); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 15); curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/x-www-form-urlencoded')); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($body)); $response = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curl_err = curl_error($ch); curl_close($ch); $record_error = function($message) use ($pdo, $source_id) { $stmt = $pdo->prepare("UPDATE `imapsync_source` SET `oauth_last_refresh_error` = :err WHERE `id` = :id"); $stmt->execute(array(':err' => $message, ':id' => $source_id)); }; if ($response === false) { $record_error("curl error: " . $curl_err); return false; } if ($http_code < 200 || $http_code >= 300) { $record_error("HTTP $http_code: " . substr($response, 0, 500)); return false; } $data = json_decode($response, true); if (!is_array($data) || empty($data['access_token']) || !isset($data['expires_in'])) { $record_error("malformed token response: " . substr($response, 0, 500)); return false; } $expires_at = time() + intval($data['expires_in']); $stmt = $pdo->prepare("UPDATE `imapsync_source` SET `oauth_access_token` = :tok, `oauth_token_expires` = :exp, `oauth_last_refresh_error` = NULL WHERE `id` = :id"); $stmt->execute(array( ':tok' => $data['access_token'], ':exp' => $expires_at, ':id' => $source_id, )); return true; } function imapsync_source_token_post($endpoint, $body) { // Shared token-endpoint POST (application/x-www-form-urlencoded). Returns the decoded // token array on success, or array('error' => '...') on failure. Used by the // authorization_code exchange and the per-user refresh_token grant. $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $endpoint); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 15); curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/x-www-form-urlencoded')); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($body)); $response = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curl_err = curl_error($ch); curl_close($ch); if ($response === false) return array('error' => "curl error: " . $curl_err); if ($http_code < 200 || $http_code >= 300) return array('error' => "HTTP $http_code: " . substr($response, 0, 500)); $data = json_decode($response, true); if (!is_array($data) || empty($data['access_token']) || !isset($data['expires_in'])) { return array('error' => "malformed token response: " . substr($response, 0, 500)); } return $data; } function imapsync_source_oauth_authorize_url($source, $redirect_uri, $state) { // Builds the provider authorization URL for the authorization_code flow. $params = array( 'client_id' => $source['oauth_client_id'], 'response_type' => 'code', 'redirect_uri' => $redirect_uri, 'response_mode' => 'query', 'scope' => $source['oauth_scope'], 'state' => $state, ); $sep = (strpos($source['oauth_authorize_endpoint'], '?') === false) ? '?' : '&'; return $source['oauth_authorize_endpoint'] . $sep . http_build_query($params); } function imapsync_source_oauth_identity($source, $token) { // Resolves the authenticated remote address from an authorization_code token response. // Prefers the id_token claims (no extra HTTP call); falls back to the userinfo endpoint. $pick = function($claims) { foreach (array('email', 'preferred_username', 'upn', 'unique_name') as $k) { if (!empty($claims[$k]) && filter_var($claims[$k], FILTER_VALIDATE_EMAIL)) return strtolower($claims[$k]); } return null; }; if (!empty($token['id_token'])) { $parts = explode('.', $token['id_token']); if (count($parts) === 3) { $claims = json_decode(base64_decode(strtr($parts[1], '-_', '+/')), true); if (is_array($claims) && ($addr = $pick($claims))) return $addr; } } if (!empty($source['oauth_userinfo_endpoint'])) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $source['oauth_userinfo_endpoint']); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 15); curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Bearer ' . $token['access_token'])); $response = curl_exec($ch); curl_close($ch); $info = json_decode((string)$response, true); if (is_array($info) && ($addr = $pick($info))) return $addr; } return null; } function imapsync_source_store_user_token($source_id, $username, $token) { // Upserts a per-user delegated token row for an authorization_code source. global $pdo; $stmt = $pdo->prepare("INSERT INTO `imapsync_source_oauth_token` (`source_id`, `username`, `access_token`, `refresh_token`, `token_expires`, `last_refresh_error`) VALUES (:sid, :user, :acc, :ref, :exp, NULL) ON DUPLICATE KEY UPDATE `access_token` = VALUES(`access_token`), `refresh_token` = VALUES(`refresh_token`), `token_expires` = VALUES(`token_expires`), `last_refresh_error` = NULL"); $stmt->execute(array( ':sid' => $source_id, ':user' => $username, ':acc' => $token['access_token'], // Microsoft only returns a refresh_token when offline_access was requested; keep old one otherwise ':ref' => $token['refresh_token'] ?? '', ':exp' => time() + intval($token['expires_in']), )); } function imapsync_source_refresh_user_token_internal($source_id, $username) { // Context-free per-user refresh (web + cron). Uses the stored refresh_token to obtain a // fresh access_token for one (source, user); records the error on the token row otherwise. global $pdo; $stmt = $pdo->prepare("SELECT s.*, t.`refresh_token` FROM `imapsync_source_oauth_token` t JOIN `imapsync_source` s ON s.`id` = t.`source_id` WHERE t.`source_id` = :sid AND t.`username` = :user AND s.`auth_type` = 'XOAUTH2' AND s.`oauth_flow` = 'authorization_code'"); $stmt->execute(array(':sid' => $source_id, ':user' => $username)); $row = $stmt->fetch(PDO::FETCH_ASSOC); if (!$row || empty($row['refresh_token'])) return false; $result = imapsync_source_token_post($row['oauth_token_endpoint'], array( 'grant_type' => 'refresh_token', 'client_id' => $row['oauth_client_id'], 'client_secret' => $row['oauth_client_secret'], 'refresh_token' => $row['refresh_token'], 'scope' => $row['oauth_scope'], )); if (isset($result['error'])) { $upd = $pdo->prepare("UPDATE `imapsync_source_oauth_token` SET `last_refresh_error` = :err WHERE `source_id` = :sid AND `username` = :user"); $upd->execute(array(':err' => $result['error'], ':sid' => $source_id, ':user' => $username)); return false; } // Some providers omit a new refresh_token on refresh — keep the existing one then if (empty($result['refresh_token'])) $result['refresh_token'] = $row['refresh_token']; imapsync_source_store_user_token($source_id, $username, $result); return true; } function syncjob($_action, $_type, $_data = null, $_extra = null) { global $pdo; global $lang; $_data_log = $_data; switch ($_action) { case 'add': switch ($_type) { case 'job': if (!isset($_SESSION['acl']['syncjobs']) || $_SESSION['acl']['syncjobs'] != "1" ) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => 'access_denied' ); return false; } if (isset($_data['username']) && filter_var($_data['username'], FILTER_VALIDATE_EMAIL)) { if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data['username'])) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => 'access_denied' ); return false; } else { $username = $_data['username']; } } elseif ($_SESSION['mailcow_cc_role'] == "user") { $username = $_SESSION['mailcow_cc_username']; } else { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => 'no_user_defined' ); return false; } $active = intval($_data['active']); $subscribeall = intval($_data['subscribeall']); $delete2duplicates = intval($_data['delete2duplicates']); $delete1 = intval($_data['delete1']); $delete2 = intval($_data['delete2']); $timeout1 = intval($_data['timeout1']); $timeout2 = intval($_data['timeout2']); $skipcrossduplicates = intval($_data['skipcrossduplicates']); $automap = intval($_data['automap']); $dry = intval($_data['dry']); $source_id = intval($_data['source_id'] ?? 0); $password1 = $_data['password1'] ?? ''; $exclude = $_data['exclude']; $maxage = $_data['maxage']; $maxbytespersecond = $_data['maxbytespersecond']; $subfolder2 = $_data['subfolder2']; $user1 = $_data['user1']; $mins_interval = $_data['mins_interval']; $custom_params = (empty(trim($_data['custom_params']))) ? '' : trim($_data['custom_params']); // Resolve and authorize the chosen sync source. Visibility is enforced by syncjob('get', 'source'). $source = syncjob('get', 'source', array('id' => $source_id)); if (empty($source) || intval($source['active']) != 1) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => 'imapsync_source_unavailable' ); return false; } if ($source['auth_type'] == 'XOAUTH2') { // OAuth2 sources do not carry a password; the runner uses a cached access_token. $password1 = ''; // authorization_code: the remote account must have completed the per-user login first. // This binds user1 to an actually-authenticated identity (no pulling foreign mailboxes). if ($source['oauth_flow'] == 'authorization_code') { $tok = $pdo->prepare("SELECT 1 FROM `imapsync_source_oauth_token` WHERE `source_id` = :sid AND `username` = :user"); $tok->execute(array(':sid' => $source_id, ':user' => $user1)); if (!$tok->fetch(PDO::FETCH_ASSOC)) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => 'imapsync_source_oauth_connect_required' ); return false; } } } elseif ($password1 === '') { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => 'password_empty' ); return false; } // validate custom params foreach (explode('-', $custom_params) as $param){ if(empty($param)) continue; // extract option if (str_contains($param, '=')) $param = explode('=', $param)[0]; else $param = rtrim($param, ' '); // remove first char if first char is - if ($param[0] == '-') $param = ltrim($param, $param[0]); if (str_contains($param, ' ')) { // bad char $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => 'bad character SPACE' ); return false; } // check if param is whitelisted if (!in_array(strtolower($param), $GLOBALS["IMAPSYNC_OPTIONS"]["whitelist"])){ // bad option $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => 'bad option '. $param ); return false; } } if (empty($subfolder2)) { $subfolder2 = ""; } if (!isset($maxage) || !filter_var($maxage, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32000)))) { $maxage = "0"; } if (!isset($timeout1) || !filter_var($timeout1, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32000)))) { $timeout1 = "600"; } if (!isset($timeout2) || !filter_var($timeout2, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32000)))) { $timeout2 = "600"; } if (!isset($maxbytespersecond) || !filter_var($maxbytespersecond, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 125000000)))) { $maxbytespersecond = "0"; } if (!filter_var($mins_interval, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 43800)))) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => 'access_denied' ); return false; } if (@preg_match("/" . $exclude . "/", null) === false) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => 'access_denied' ); return false; } $stmt = $pdo->prepare("SELECT '1' FROM `imapsync` WHERE `user2` = :user2 AND `user1` = :user1 AND `source_id` = :source_id"); $stmt->execute(array(':user1' => $user1, ':user2' => $username, ':source_id' => $source_id)); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); if ($num_results != 0) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => array('object_exists', htmlspecialchars($source['name'] . ' / ' . $user1)) ); return false; } $stmt = $pdo->prepare("INSERT INTO `imapsync` (`user2`, `exclude`, `delete1`, `delete2`, `timeout1`, `timeout2`, `automap`, `skipcrossduplicates`, `maxbytespersecond`, `subscribeall`, `dry`, `maxage`, `subfolder2`, `source_id`, `user1`, `password1`, `mins_interval`, `delete2duplicates`, `custom_params`, `active`) VALUES (:user2, :exclude, :delete1, :delete2, :timeout1, :timeout2, :automap, :skipcrossduplicates, :maxbytespersecond, :subscribeall, :dry, :maxage, :subfolder2, :source_id, :user1, :password1, :mins_interval, :delete2duplicates, :custom_params, :active)"); $stmt->execute(array( ':user2' => $username, ':custom_params' => $custom_params, ':exclude' => $exclude, ':maxage' => $maxage, ':delete1' => $delete1, ':delete2' => $delete2, ':timeout1' => $timeout1, ':timeout2' => $timeout2, ':automap' => $automap, ':skipcrossduplicates' => $skipcrossduplicates, ':maxbytespersecond' => $maxbytespersecond, ':subscribeall' => $subscribeall, ':dry' => $dry, ':subfolder2' => $subfolder2, ':source_id' => $source_id, ':user1' => $user1, ':password1' => $password1, ':mins_interval' => $mins_interval, ':delete2duplicates' => $delete2duplicates, ':active' => $active, )); $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => array('mailbox_modified', $username) ); break; case 'source': $role = $_SESSION['mailcow_cc_role'] ?? null; if (!in_array($role, array('admin', 'domainadmin', 'user'))) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log), 'msg' => 'access_denied' ); return false; } $name = trim($_data['name'] ?? ''); $description = trim($_data['description'] ?? ''); $host1 = strtolower(trim($_data['host1'] ?? '')); $port1 = intval($_data['port1'] ?? 0); $enc1 = $_data['enc1'] ?? 'TLS'; $auth_type = $_data['auth_type'] ?? 'PLAIN'; $active = isset($_data['active']) ? intval($_data['active']) : 1; // Creator owns the row; visibility is driven by scope + target tables. $created_by = $_SESSION['mailcow_cc_username']; $scope = $_data['scope'] ?? 'user'; if ($role == 'user') $scope = 'user'; $scope_domains = (array)($_data['domains'] ?? array()); $scope_users = (array)($_data['users'] ?? array()); if ($name === '' || strlen($name) > 100) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log), 'msg' => 'imapsync_source_name_invalid' ); return false; } if ($host1 === '') { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log), 'msg' => 'imapsync_source_host_invalid' ); return false; } if (!filter_var($port1, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 65535)))) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log), 'msg' => 'imapsync_source_port_invalid' ); return false; } if (!in_array($enc1, array('TLS', 'SSL', 'PLAIN'))) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log), 'msg' => 'imapsync_source_enc_invalid' ); return false; } if (!in_array($auth_type, array('PLAIN', 'LOGIN', 'CRAM-MD5', 'XOAUTH2'))) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log), 'msg' => 'imapsync_source_auth_invalid' ); return false; } $oauth_flow = 'client_credentials'; $oauth_token_endpoint = null; $oauth_authorize_endpoint = null; $oauth_userinfo_endpoint = null; $oauth_client_id = null; $oauth_client_secret = null; $oauth_scope = null; $oauth_extra_params = null; if ($auth_type == 'XOAUTH2') { $oauth_flow = ($_data['oauth_flow'] ?? '') === 'authorization_code' ? 'authorization_code' : 'client_credentials'; $oauth_token_endpoint = trim($_data['oauth_token_endpoint'] ?? ''); $oauth_authorize_endpoint = trim($_data['oauth_authorize_endpoint'] ?? ''); $oauth_userinfo_endpoint = trim($_data['oauth_userinfo_endpoint'] ?? ''); $oauth_client_id = trim($_data['oauth_client_id'] ?? ''); $oauth_client_secret = $_data['oauth_client_secret'] ?? ''; $oauth_scope = trim($_data['oauth_scope'] ?? ''); $oauth_extra_params = trim($_data['oauth_extra_params'] ?? ''); if ($oauth_token_endpoint === '' || $oauth_client_id === '' || $oauth_client_secret === '' || $oauth_scope === '') { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log), 'msg' => 'imapsync_source_oauth_required' ); return false; } // authorization_code additionally needs the authorize endpoint (user login redirect) if ($oauth_flow == 'authorization_code' && !filter_var($oauth_authorize_endpoint, FILTER_VALIDATE_URL)) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log), 'msg' => 'imapsync_source_oauth_endpoint_invalid' ); return false; } if (!filter_var($oauth_token_endpoint, FILTER_VALIDATE_URL) || ($oauth_userinfo_endpoint !== '' && !filter_var($oauth_userinfo_endpoint, FILTER_VALIDATE_URL))) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log), 'msg' => 'imapsync_source_oauth_endpoint_invalid' ); return false; } if ($oauth_extra_params !== '' && json_decode($oauth_extra_params) === null) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log), 'msg' => 'imapsync_source_oauth_extra_invalid' ); return false; } if ($oauth_extra_params === '') $oauth_extra_params = null; if ($oauth_authorize_endpoint === '') $oauth_authorize_endpoint = null; if ($oauth_userinfo_endpoint === '') $oauth_userinfo_endpoint = null; } // Uniqueness on (created_by, name) $stmt = $pdo->prepare("SELECT `id` FROM `imapsync_source` WHERE `name` = :name AND `created_by` = :created_by"); $stmt->execute(array(':name' => $name, ':created_by' => $created_by)); if ($stmt->fetch(PDO::FETCH_ASSOC)) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log), 'msg' => array('object_exists', htmlspecialchars($name)) ); return false; } $stmt = $pdo->prepare("INSERT INTO `imapsync_source` (`name`, `description`, `created_by`, `scope`, `host1`, `port1`, `enc1`, `auth_type`, `oauth_flow`, `oauth_token_endpoint`, `oauth_authorize_endpoint`, `oauth_userinfo_endpoint`, `oauth_client_id`, `oauth_client_secret`, `oauth_scope`, `oauth_extra_params`, `active`) VALUES (:name, :description, :created_by, :scope, :host1, :port1, :enc1, :auth_type, :oauth_flow, :oauth_token_endpoint, :oauth_authorize_endpoint, :oauth_userinfo_endpoint, :oauth_client_id, :oauth_client_secret, :oauth_scope, :oauth_extra_params, :active)"); $stmt->execute(array( ':name' => $name, ':description' => $description, ':created_by' => $created_by, ':scope' => in_array($scope, array('all', 'domain', 'user')) ? $scope : 'user', ':host1' => $host1, ':port1' => $port1, ':enc1' => $enc1, ':auth_type' => $auth_type, ':oauth_flow' => $oauth_flow, ':oauth_token_endpoint' => $oauth_token_endpoint, ':oauth_authorize_endpoint' => $oauth_authorize_endpoint, ':oauth_userinfo_endpoint' => $oauth_userinfo_endpoint, ':oauth_client_id' => $oauth_client_id, ':oauth_client_secret' => $oauth_client_secret, ':oauth_scope' => $oauth_scope, ':oauth_extra_params' => $oauth_extra_params, ':active' => $active, )); $source_id = $pdo->lastInsertId(); if (!imapsync_source_apply_scope($source_id, $role, $scope, $scope_domains, $scope_users, $_data_log)) { // Roll back the row so a rejected scope does not leave an orphan source $pdo->prepare("DELETE FROM `imapsync_source` WHERE `id` = :id")->execute(array(':id' => $source_id)); return false; } $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log), 'msg' => array('imapsync_source_added', $name) ); return true; break; } break; case 'edit': switch ($_type) { case 'job': if (!is_array($_data['id'])) { $ids = array(); $ids[] = $_data['id']; } else { $ids = $_data['id']; } if (!isset($_SESSION['acl']['syncjobs']) || $_SESSION['acl']['syncjobs'] != "1" ) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => 'access_denied' ); return false; } foreach ($ids as $id) { $is_now = syncjob('get', 'job', $id, array('with_password')); if (!empty($is_now)) { $username = $is_now['user2']; $user1 = (!empty($_data['user1'])) ? $_data['user1'] : $is_now['user1']; $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active']; $last_run = (isset($_data['last_run'])) ? NULL : $is_now['last_run']; $success = (isset($_data['success'])) ? NULL : $is_now['success']; $delete2duplicates = (isset($_data['delete2duplicates'])) ? intval($_data['delete2duplicates']) : $is_now['delete2duplicates']; $subscribeall = (isset($_data['subscribeall'])) ? intval($_data['subscribeall']) : $is_now['subscribeall']; $dry = (isset($_data['dry'])) ? intval($_data['dry']) : $is_now['dry']; $delete1 = (isset($_data['delete1'])) ? intval($_data['delete1']) : $is_now['delete1']; $delete2 = (isset($_data['delete2'])) ? intval($_data['delete2']) : $is_now['delete2']; $automap = (isset($_data['automap'])) ? intval($_data['automap']) : $is_now['automap']; $skipcrossduplicates = (isset($_data['skipcrossduplicates'])) ? intval($_data['skipcrossduplicates']) : $is_now['skipcrossduplicates']; $password1 = (!empty($_data['password1'])) ? $_data['password1'] : $is_now['password1']; $source_id = (!empty($_data['source_id'])) ? intval($_data['source_id']) : intval($is_now['source_id']); $subfolder2 = (isset($_data['subfolder2'])) ? $_data['subfolder2'] : $is_now['subfolder2']; $mins_interval = (!empty($_data['mins_interval'])) ? $_data['mins_interval'] : $is_now['mins_interval']; $exclude = (isset($_data['exclude'])) ? $_data['exclude'] : $is_now['exclude']; $custom_params = (isset($_data['custom_params'])) ? $_data['custom_params'] : $is_now['custom_params']; $maxage = (isset($_data['maxage']) && $_data['maxage'] != "") ? intval($_data['maxage']) : $is_now['maxage']; $maxbytespersecond = (isset($_data['maxbytespersecond']) && $_data['maxbytespersecond'] != "") ? intval($_data['maxbytespersecond']) : $is_now['maxbytespersecond']; $timeout1 = (isset($_data['timeout1']) && $_data['timeout1'] != "") ? intval($_data['timeout1']) : $is_now['timeout1']; $timeout2 = (isset($_data['timeout2']) && $_data['timeout2'] != "") ? intval($_data['timeout2']) : $is_now['timeout2']; } else { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => 'access_denied' ); continue; } // Resolve and authorize the (possibly switched) source. Visibility enforced by syncjob('get', 'source'). $source = syncjob('get', 'source', array('id' => $source_id)); if (empty($source) || intval($source['active']) != 1) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => 'imapsync_source_unavailable' ); continue; } if ($source['auth_type'] == 'XOAUTH2') { // Wipe any leftover password when bound to an OAuth2 source. $password1 = ''; } // validate custom params foreach (explode('-', $custom_params) as $param){ if(empty($param)) continue; // extract option if (str_contains($param, '=')) $param = explode('=', $param)[0]; else $param = rtrim($param, ' '); // remove first char if first char is - if ($param[0] == '-') $param = ltrim($param, $param[0]); if (str_contains($param, ' ')) { // bad char $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => 'bad character SPACE' ); return false; } // check if param is whitelisted if (!in_array(strtolower($param), $GLOBALS["IMAPSYNC_OPTIONS"]["whitelist"])){ // bad option $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => 'bad option '. $param ); return false; } } if (empty($subfolder2)) { $subfolder2 = ""; } if (!isset($maxage) || !filter_var($maxage, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32000)))) { $maxage = "0"; } if (!isset($timeout1) || !filter_var($timeout1, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32000)))) { $timeout1 = "600"; } if (!isset($timeout2) || !filter_var($timeout2, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32000)))) { $timeout2 = "600"; } if (!isset($maxbytespersecond) || !filter_var($maxbytespersecond, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 125000000)))) { $maxbytespersecond = "0"; } if (!filter_var($mins_interval, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 43800)))) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => 'access_denied' ); continue; } if (@preg_match("/" . $exclude . "/", null) === false) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => 'access_denied' ); continue; } $stmt = $pdo->prepare("UPDATE `imapsync` SET `delete1` = :delete1, `delete2` = :delete2, `automap` = :automap, `skipcrossduplicates` = :skipcrossduplicates, `maxage` = :maxage, `maxbytespersecond` = :maxbytespersecond, `subfolder2` = :subfolder2, `exclude` = :exclude, `source_id` = :source_id, `last_run` = :last_run, `success` = :success, `user1` = :user1, `password1` = :password1, `mins_interval` = :mins_interval, `delete2duplicates` = :delete2duplicates, `custom_params` = :custom_params, `timeout1` = :timeout1, `timeout2` = :timeout2, `subscribeall` = :subscribeall, `dry` = :dry, `active` = :active WHERE `id` = :id"); $stmt->execute(array( ':delete1' => $delete1, ':delete2' => $delete2, ':automap' => $automap, ':skipcrossduplicates' => $skipcrossduplicates, ':id' => $id, ':exclude' => $exclude, ':maxage' => $maxage, ':maxbytespersecond' => $maxbytespersecond, ':subfolder2' => $subfolder2, ':source_id' => $source_id, ':user1' => $user1, ':password1' => $password1, ':last_run' => $last_run, ':success' => $success, ':mins_interval' => $mins_interval, ':delete2duplicates' => $delete2duplicates, ':custom_params' => $custom_params, ':timeout1' => $timeout1, ':timeout2' => $timeout2, ':subscribeall' => $subscribeall, ':dry' => $dry, ':active' => $active, )); $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => array('mailbox_modified', $username) ); } break; case 'source': $ids = (array)($_data['id'] ?? array()); foreach ($ids as $id) { if (!is_numeric($id)) continue; $is_now = syncjob('get', 'source', array('id' => $id, 'with_secret' => true)); if (empty($is_now) || !imapsync_source_can_edit_row($is_now)) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log), 'msg' => 'access_denied' ); continue; } $name = isset($_data['name']) ? trim($_data['name']) : $is_now['name']; $description = isset($_data['description']) ? trim($_data['description']) : $is_now['description']; $host1 = isset($_data['host1']) ? strtolower(trim($_data['host1'])) : $is_now['host1']; $port1 = isset($_data['port1']) ? intval($_data['port1']) : $is_now['port1']; $enc1 = $_data['enc1'] ?? $is_now['enc1']; $auth_type = $_data['auth_type'] ?? $is_now['auth_type']; $active = isset($_data['active']) ? intval($_data['active']) : $is_now['active']; if ($name === '' || strlen($name) > 100 || $host1 === '' || !filter_var($port1, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 65535))) || !in_array($enc1, array('TLS', 'SSL', 'PLAIN')) || !in_array($auth_type, array('PLAIN', 'LOGIN', 'CRAM-MD5', 'XOAUTH2'))) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log), 'msg' => 'imapsync_source_invalid' ); continue; } $oauth_flow = $is_now['oauth_flow']; $oauth_authorize_endpoint = $is_now['oauth_authorize_endpoint']; $oauth_userinfo_endpoint = $is_now['oauth_userinfo_endpoint']; if ($auth_type == 'XOAUTH2') { $oauth_flow = ($_data['oauth_flow'] ?? $is_now['oauth_flow']) === 'authorization_code' ? 'authorization_code' : 'client_credentials'; $oauth_token_endpoint = isset($_data['oauth_token_endpoint']) ? trim($_data['oauth_token_endpoint']) : $is_now['oauth_token_endpoint']; $oauth_authorize_endpoint = isset($_data['oauth_authorize_endpoint']) ? trim($_data['oauth_authorize_endpoint']) : $is_now['oauth_authorize_endpoint']; $oauth_userinfo_endpoint = isset($_data['oauth_userinfo_endpoint']) ? trim($_data['oauth_userinfo_endpoint']) : $is_now['oauth_userinfo_endpoint']; // Empty client_secret on edit keeps the stored one (avoids accidental wipes from masked fields) $oauth_client_id = isset($_data['oauth_client_id']) ? trim($_data['oauth_client_id']) : $is_now['oauth_client_id']; $oauth_client_secret = (!empty($_data['oauth_client_secret'])) ? $_data['oauth_client_secret'] : $is_now['oauth_client_secret']; $oauth_scope = isset($_data['oauth_scope']) ? trim($_data['oauth_scope']) : $is_now['oauth_scope']; $oauth_extra_params = isset($_data['oauth_extra_params']) ? trim($_data['oauth_extra_params']) : $is_now['oauth_extra_params']; if ($oauth_extra_params === '') $oauth_extra_params = null; if (empty($oauth_token_endpoint) || empty($oauth_client_id) || empty($oauth_client_secret) || empty($oauth_scope) || !filter_var($oauth_token_endpoint, FILTER_VALIDATE_URL) || ($oauth_flow == 'authorization_code' && !filter_var($oauth_authorize_endpoint, FILTER_VALIDATE_URL)) || (!empty($oauth_userinfo_endpoint) && !filter_var($oauth_userinfo_endpoint, FILTER_VALIDATE_URL)) || ($oauth_extra_params !== null && json_decode($oauth_extra_params) === null)) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log), 'msg' => 'imapsync_source_oauth_invalid' ); continue; } if (empty($oauth_authorize_endpoint)) $oauth_authorize_endpoint = null; if (empty($oauth_userinfo_endpoint)) $oauth_userinfo_endpoint = null; } else { // Wipe OAuth fields when switching away from XOAUTH2 $oauth_flow = 'client_credentials'; $oauth_token_endpoint = null; $oauth_authorize_endpoint = null; $oauth_userinfo_endpoint = null; $oauth_client_id = null; $oauth_client_secret = null; $oauth_scope = null; $oauth_extra_params = null; } $stmt = $pdo->prepare("UPDATE `imapsync_source` SET `name` = :name, `description` = :description, `host1` = :host1, `port1` = :port1, `enc1` = :enc1, `auth_type` = :auth_type, `oauth_flow` = :oauth_flow, `oauth_token_endpoint` = :oauth_token_endpoint, `oauth_authorize_endpoint` = :oauth_authorize_endpoint, `oauth_userinfo_endpoint` = :oauth_userinfo_endpoint, `oauth_client_id` = :oauth_client_id, `oauth_client_secret` = :oauth_client_secret, `oauth_scope` = :oauth_scope, `oauth_extra_params` = :oauth_extra_params, `active` = :active WHERE `id` = :id"); $stmt->execute(array( ':name' => $name, ':description' => $description, ':host1' => $host1, ':port1' => $port1, ':enc1' => $enc1, ':auth_type' => $auth_type, ':oauth_flow' => $oauth_flow, ':oauth_token_endpoint' => $oauth_token_endpoint, ':oauth_authorize_endpoint' => $oauth_authorize_endpoint, ':oauth_userinfo_endpoint' => $oauth_userinfo_endpoint, ':oauth_client_id' => $oauth_client_id, ':oauth_client_secret' => $oauth_client_secret, ':oauth_scope' => $oauth_scope, ':oauth_extra_params' => $oauth_extra_params, ':active' => $active, ':id' => $id, )); // Scope/targets are editable; only touched when the request carries `scope`. // apply_scope also (re)writes the scope column, incl. the client_credentials guardrail. if (isset($_data['scope'])) { $scope = ($role == 'user') ? 'user' : $_data['scope']; if (!imapsync_source_apply_scope($id, $role, $scope, (array)($_data['domains'] ?? array()), (array)($_data['users'] ?? array()), $_data_log)) { continue; } } // Guardrail also on edit: switching to client_credentials must never leave a shared scope // (covers the case where the request changed the flow but did not carry `scope`). if ($auth_type == 'XOAUTH2' && $oauth_flow == 'client_credentials') { $pdo->prepare("DELETE FROM `imapsync_source_domain` WHERE `source_id` = :id")->execute(array(':id' => $id)); $pdo->prepare("DELETE FROM `imapsync_source_user` WHERE `source_id` = :id")->execute(array(':id' => $id)); $pdo->prepare("UPDATE `imapsync_source` SET `scope` = 'user' WHERE `id` = :id")->execute(array(':id' => $id)); } $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log), 'msg' => array('imapsync_source_modified', $name) ); } return true; break; case 'refresh_token': $id = is_array($_data) ? ($_data['id'] ?? null) : $_data; if (is_array($id)) $id = reset($id); if (!is_numeric($id)) return false; $is_now = syncjob('get', 'source', array('id' => $id, 'with_secret' => true)); if (empty($is_now) || !imapsync_source_can_edit_row($is_now)) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log), 'msg' => 'access_denied' ); return false; } $ok = imapsync_source_refresh_token_internal($id); if ($ok) { $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log), 'msg' => array('imapsync_source_token_refreshed', $is_now['name']) ); } else { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log), 'msg' => array('imapsync_source_token_refresh_failed', $is_now['name']) ); } return $ok; break; } break; case 'delete': switch ($_type) { case 'job': if (!is_array($_data['id'])) { $ids = array(); $ids[] = $_data['id']; } else { $ids = $_data['id']; } if (!isset($_SESSION['acl']['syncjobs']) || $_SESSION['acl']['syncjobs'] != "1" ) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => 'access_denied' ); return false; } foreach ($ids as $id) { if (!is_numeric($id)) { return false; } $stmt = $pdo->prepare("SELECT `user2` FROM `imapsync` WHERE id = :id"); $stmt->execute(array(':id' => $id)); $user2 = $stmt->fetch(PDO::FETCH_ASSOC)['user2']; if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $user2)) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => 'access_denied' ); continue; } $stmt = $pdo->prepare("DELETE FROM `imapsync` WHERE `id`= :id"); $stmt->execute(array(':id' => $id)); $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => array('deleted_syncjob', $id) ); } break; case 'source': $ids = (array)($_data['id'] ?? array()); foreach ($ids as $id) { if (!is_numeric($id)) continue; $is_now = syncjob('get', 'source', array('id' => $id)); if (empty($is_now) || !imapsync_source_can_edit_row($is_now)) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log), 'msg' => 'access_denied' ); continue; } // FK ON DELETE RESTRICT prevents removal while syncjobs reference it; emit a clean message $ref = $pdo->prepare("SELECT COUNT(*) AS c FROM `imapsync` WHERE `source_id` = :id"); $ref->execute(array(':id' => $id)); if (intval($ref->fetch(PDO::FETCH_ASSOC)['c']) > 0) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log), 'msg' => array('imapsync_source_in_use', $is_now['name']) ); continue; } $stmt = $pdo->prepare("DELETE FROM `imapsync_source` WHERE `id` = :id"); $stmt->execute(array(':id' => $id)); $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log), 'msg' => array('imapsync_source_deleted', $is_now['name']) ); } return true; break; } break; case 'get': switch ($_type) { case 'job': $syncjobdetails = array(); if (!is_numeric($_data)) { return false; } // Join imapsync_source so the UI gets host/auth-type/creator of the bound source for display, // plus token-status fields for XOAUTH2 sources. $source_select = "s.`name` AS source_name, s.`description` AS source_description, s.`host1` AS source_host, s.`port1` AS source_port, s.`enc1` AS source_enc, 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.`oauth_token_expires` AS source_oauth_token_expires, s.`oauth_last_refresh_error` AS source_oauth_last_refresh_error"; if (isset($_extra) && in_array('no_log', $_extra)) { $field_query = $pdo->query('SHOW FIELDS FROM `imapsync` WHERE FIELD NOT IN ("returned_text", "password1")'); $fields = $field_query->fetchAll(PDO::FETCH_ASSOC); while($field = array_shift($fields)) { $shown_fields[] = 'i.`' . $field['Field'] . '`'; } $stmt = $pdo->prepare("SELECT " . implode(',', (array)$shown_fields) . ", $source_select FROM `imapsync` i LEFT JOIN `imapsync_source` s ON i.`source_id` = s.`id` WHERE i.`id` = :id"); } elseif (isset($_extra) && in_array('with_password', $_extra)) { $stmt = $pdo->prepare("SELECT i.*, $source_select FROM `imapsync` i LEFT JOIN `imapsync_source` s ON i.`source_id` = s.`id` WHERE i.`id` = :id"); } else { $field_query = $pdo->query('SHOW FIELDS FROM `imapsync` WHERE FIELD NOT IN ("password1")'); $fields = $field_query->fetchAll(PDO::FETCH_ASSOC); while($field = array_shift($fields)) { $shown_fields[] = 'i.`' . $field['Field'] . '`'; } $stmt = $pdo->prepare("SELECT " . implode(',', (array)$shown_fields) . ", $source_select FROM `imapsync` i LEFT JOIN `imapsync_source` s ON i.`source_id` = s.`id` WHERE i.`id` = :id"); } $stmt->execute(array(':id' => $_data)); $syncjobdetails = $stmt->fetch(PDO::FETCH_ASSOC); if (!empty($syncjobdetails['returned_text'])) { $syncjobdetails['log'] = $syncjobdetails['returned_text']; } else { $syncjobdetails['log'] = ''; } unset($syncjobdetails['returned_text']); if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $syncjobdetails['user2'])) { return false; } return $syncjobdetails; break; case 'jobs': $syncjobdata = array(); if (isset($_data) && filter_var($_data, FILTER_VALIDATE_EMAIL)) { if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) { return false; } } else { $_data = $_SESSION['mailcow_cc_username']; } $stmt = $pdo->prepare("SELECT `id` FROM `imapsync` WHERE `user2` = :username"); $stmt->execute(array(':username' => $_data)); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); while($row = array_shift($rows)) { $syncjobdata[] = $row['id']; } return $syncjobdata; break; case 'source': $id = is_array($_data) ? ($_data['id'] ?? null) : $_data; $with_secret = is_array($_data) && !empty($_data['with_secret']); if (!is_numeric($id)) return false; $params = array(':id' => $id); $visibility = imapsync_source_visible_sql($params); $stmt = $pdo->prepare("SELECT s.* FROM `imapsync_source` s WHERE s.`id` = :id AND " . $visibility); $stmt->execute($params); $row = $stmt->fetch(PDO::FETCH_ASSOC); if (!$row) return false; // Attach scope targets for the UI (visibility column + edit form pre-fill) $dstmt = $pdo->prepare("SELECT `domain` FROM `imapsync_source_domain` WHERE `source_id` = :id ORDER BY `domain`"); $dstmt->execute(array(':id' => $id)); $row['domains'] = $dstmt->fetchAll(PDO::FETCH_COLUMN); $ustmt = $pdo->prepare("SELECT `username` FROM `imapsync_source_user` WHERE `source_id` = :id ORDER BY `username`"); $ustmt->execute(array(':id' => $id)); $row['users'] = $ustmt->fetchAll(PDO::FETCH_COLUMN); // Let the UI hide edit/delete for rows the session may not manage (server enforces too) $row['can_edit'] = imapsync_source_can_edit_row($row); if (!$with_secret) { // Never leak secrets / token cache to the UI consumer that does not explicitly request them $row['oauth_client_secret'] = ''; $row['oauth_access_token'] = ''; } return $row; break; case 'sources': // Returns a list of ids the current session may see. Used by UI tables / dropdowns. $params = array(); $visibility = imapsync_source_visible_sql($params); $stmt = $pdo->prepare("SELECT s.`id` FROM `imapsync_source` s WHERE " . $visibility . " ORDER BY FIELD(s.`scope`, 'all', 'domain', 'user'), s.`name`"); $stmt->execute($params); $ids = array(); foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { $ids[] = $row['id']; } return $ids; break; } break; } return false; }