[Web][Dovecot] Rework imapsync: sync sources, OAuth2 (XOAUTH2), worker pool, Python runner

This commit is contained in:
FreddleSpl0it committed 2026-10-22 10:07:52 +02:00
1 parent e756b7857e
commit 7c62a57910
39 files changed
+3582 -1358

No files matched your search

+1 -1
View File
@@ -6,7 +6,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
}
if (isset($_GET['id']) && is_numeric($_GET['id'])) {
if ($details = mailbox('get', 'syncjob_details', intval($_GET['id']))) {
if ($details = \Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->getJob(intval($_GET['id']))) {
echo (empty($details['log'])) ? '-' : $details['log'];
}
}
+3 -2
View File
@@ -33,11 +33,11 @@ if (array_key_exists('pending_tfa_methods', $_SESSION)) {
if (isset($pending_tfa_authmechs['webauthn'])) {
$pending_tfa_authmechs['webauthn'] = true;
}
if (!isset($pending_tfa_authmechs['webauthn'])
if (!isset($pending_tfa_authmechs['webauthn'])
&& isset($pending_tfa_authmechs['yubi_otp'])) {
$pending_tfa_authmechs['yubi_otp'] = true;
}
if (!isset($pending_tfa_authmechs['webauthn'])
if (!isset($pending_tfa_authmechs['webauthn'])
&& !isset($pending_tfa_authmechs['yubi_otp'])
&& isset($pending_tfa_authmechs['totp'])) {
$pending_tfa_authmechs['totp'] = true;
@@ -72,6 +72,7 @@ $globalVariables = [
'lang_fido2' => json_encode($lang['fido2']),
'lang_success' => json_encode($lang['success']),
'lang_danger' => json_encode($lang['danger']),
'lang_syncjobs' => json_encode($lang['syncjobs']),
'docker_timeout' => $DOCKER_TIMEOUT,
'session_lifetime' => (int)$SESSION_LIFETIME,
'csrf_token' => $_SESSION['CSRF']['TOKEN'],
+5 -1
View File
@@ -103,7 +103,7 @@ function admin($_action, $_data = null) {
continue;
}
}
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username_new))) {
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username_new)) || $username_new == 'API') {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
@@ -179,6 +179,9 @@ function admin($_action, $_data = null) {
$stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username");
$stmt->execute(array(':username_new' => $username_new, ':username' => $username));
}
if ($username_new != $username) {
\Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->accountChanged($username, $username_new);
}
}
$_SESSION['return'][] = array(
'type' => 'success',
@@ -211,6 +214,7 @@ function admin($_action, $_data = null) {
$stmt->execute(array(
':username' => $username,
));
\Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->accountChanged($username);
$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
$stmt->execute(array(
':username' => $username,
+6 -1
View File
@@ -156,7 +156,7 @@ function domain_admin($_action, $_data = null) {
}
}
}
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username_new))) {
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username_new)) || $username_new == 'API') {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
@@ -183,6 +183,9 @@ function domain_admin($_action, $_data = null) {
':username_new' => $username_new,
':username' => $username
));
if ($username_new != $username) {
\Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->accountChanged($username, $username_new);
}
if (!empty($domains)) {
foreach ($domains as $domain) {
$stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
@@ -331,6 +334,8 @@ function domain_admin($_action, $_data = null) {
$stmt->execute(array(
':username' => $username,
));
// sync sources created by this account must not pass to a re-created account of the same name
\Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->accountChanged($username);
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data_log),
+9 -484
View File
@@ -302,192 +302,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
'msg' => array('mailbox_modified', $username)
);
break;
case 'syncjob':
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']);
$port1 = $_data['port1'];
$host1 = strtolower($_data['host1']);
$password1 = $_data['password1'];
$exclude = $_data['exclude'];
$maxage = $_data['maxage'];
$maxbytespersecond = $_data['maxbytespersecond'];
$subfolder2 = $_data['subfolder2'];
$user1 = $_data['user1'];
$mins_interval = $_data['mins_interval'];
$enc1 = $_data['enc1'];
$custom_params = (empty(trim($_data['custom_params']))) ? '' : trim($_data['custom_params']);
// 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($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, $_attr),
'msg' => 'access_denied'
);
return false;
}
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 (!is_valid_domain_name($host1)) {
// $_SESSION['return'][] = array(
// 'type' => 'danger',
// 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
// 'msg' => 'access_denied'
// );
// return false;
// }
if ($enc1 != "TLS" && $enc1 != "SSL" && $enc1 != "PLAIN") {
$_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 `host1` = :host1");
$stmt->execute(array(':user1' => $user1, ':user2' => $username, ':host1' => $host1));
$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($host1 . ' / ' . $user1))
);
return false;
}
$stmt = $pdo->prepare("INSERT INTO `imapsync` (`user2`, `exclude`, `delete1`, `delete2`, `timeout1`, `timeout2`, `automap`, `skipcrossduplicates`, `maxbytespersecond`, `subscribeall`, `dry`, `maxage`, `subfolder2`, `host1`, `authmech1`, `user1`, `password1`, `mins_interval`, `port1`, `enc1`, `delete2duplicates`, `custom_params`, `active`)
VALUES (:user2, :exclude, :delete1, :delete2, :timeout1, :timeout2, :automap, :skipcrossduplicates, :maxbytespersecond, :subscribeall, :dry, :maxage, :subfolder2, :host1, :authmech1, :user1, :password1, :mins_interval, :port1, :enc1, :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,
':host1' => $host1,
':authmech1' => 'PLAIN',
':user1' => $user1,
':password1' => $password1,
':mins_interval' => $mins_interval,
':port1' => $port1,
':enc1' => $enc1,
':delete2duplicates' => $delete2duplicates,
':active' => $active,
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('mailbox_modified', $username)
);
break;
case 'domain':
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
@@ -2280,202 +2094,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
);
}
break;
case 'syncjob':
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 = mailbox('get', 'syncjob_details', $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'];
$port1 = (!empty($_data['port1'])) ? $_data['port1'] : $is_now['port1'];
$password1 = (!empty($_data['password1'])) ? $_data['password1'] : $is_now['password1'];
$host1 = (!empty($_data['host1'])) ? $_data['host1'] : $is_now['host1'];
$subfolder2 = (isset($_data['subfolder2'])) ? $_data['subfolder2'] : $is_now['subfolder2'];
$enc1 = (!empty($_data['enc1'])) ? $_data['enc1'] : $is_now['enc1'];
$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;
}
// 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($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, $_attr),
'msg' => 'access_denied'
);
continue;
}
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 (!is_valid_domain_name($host1)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
if ($enc1 != "TLS" && $enc1 != "SSL" && $enc1 != "PLAIN") {
$_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,
`host1` = :host1,
`last_run` = :last_run,
`success` = :success,
`user1` = :user1,
`password1` = :password1,
`mins_interval` = :mins_interval,
`port1` = :port1,
`enc1` = :enc1,
`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,
':host1' => $host1,
':user1' => $user1,
':password1' => $password1,
':last_run' => $last_run,
':success' => $success,
':mins_interval' => $mins_interval,
':port1' => $port1,
':enc1' => $enc1,
':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 'filter':
if (!isset($_SESSION['acl']['filters']) || $_SESSION['acl']['filters'] != "1" ) {
$_SESSION['return'][] = array(
@@ -3652,6 +3270,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
}
}
// Sync sources created by / targeting the mailbox and its OAuth tokens (owner); a PDOException rolls back below
\Mailcow\Imapsync\ImapsyncModule::fromGlobals()->sources->onAccountRenamed($old_username, $new_username);
// Update c_uid, c_name and mail in _sogo_static_view table
$pdo->prepare("UPDATE _sogo_static_view SET c_uid = :new_username, c_name = :new_username2, mail = :new_username3 WHERE c_uid = :old_username")
->execute([
@@ -4603,68 +4224,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
}
return false;
break;
case 'syncjob_details':
$syncjobdetails = array();
if (!is_numeric($_data)) {
return false;
}
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[] = $field['Field'];
}
$stmt = $pdo->prepare("SELECT " . implode(',', (array)$shown_fields) . ",
`active`
FROM `imapsync` WHERE id = :id");
}
elseif (isset($_extra) && in_array('with_password', $_extra)) {
$stmt = $pdo->prepare("SELECT *,
`active`
FROM `imapsync` WHERE 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[] = $field['Field'];
}
$stmt = $pdo->prepare("SELECT " . implode(',', (array)$shown_fields) . ",
`active`
FROM `imapsync` WHERE 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 'syncjobs':
$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 'spam_score':
$curl = curl_init();
curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
@@ -5494,46 +5053,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
break;
case 'delete':
switch ($_type) {
case 'syncjob':
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 'filter':
if (!is_array($_data['id'])) {
$ids = array();
@@ -5764,6 +5283,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$stmt->execute(array(
':domain' => $domain,
));
$stmt = $pdo->prepare("DELETE FROM `imapsync_source_domain` WHERE `domain` = :domain");
$stmt->execute(array(
':domain' => $domain,
));
$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `domain` = :domain");
$stmt->execute(array(
':domain' => $domain,
@@ -6071,6 +5594,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$stmt->execute(array(
':username' => $username
));
// Tokens, scope targets and own sync sources of the mailbox
\Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->accountChanged($username);
$stmt = $pdo->prepare("DELETE FROM `filterconf` WHERE `object` = :username");
$stmt->execute(array(
':username' => $username
+293 -19
View File
@@ -4,7 +4,7 @@ function init_db_schema()
try {
global $pdo;
$db_version = "18082026_1200";
$db_version = "16102026_1200";
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -755,35 +755,153 @@ function init_db_schema()
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
),
"imapsync_source" => array(
"cols" => array(
"id" => "INT NOT NULL AUTO_INCREMENT",
"name" => "VARCHAR(100) NOT NULL",
"description" => "VARCHAR(255) NOT NULL DEFAULT ''",
"created_by" => "VARCHAR(255) NOT NULL DEFAULT ''",
"scope" => "ENUM('all','domain','user') NOT NULL DEFAULT 'user'",
"host1" => "VARCHAR(255) NOT NULL",
"port1" => "SMALLINT UNSIGNED NOT NULL",
"enc1" => "ENUM('TLS','SSL','PLAIN') DEFAULT 'TLS'",
"auth_type" => "ENUM('PLAIN','LOGIN','CRAM-MD5','XOAUTH2') DEFAULT 'PLAIN'",
"oauth_flow" => "ENUM('client_credentials','authorization_code') NOT NULL DEFAULT 'client_credentials'",
"oauth_token_endpoint" => "VARCHAR(500) DEFAULT NULL",
"oauth_authorize_endpoint" => "VARCHAR(500) DEFAULT NULL",
"oauth_userinfo_endpoint" => "VARCHAR(500) DEFAULT NULL",
"oauth_client_id" => "VARCHAR(500) DEFAULT NULL",
"oauth_client_secret" => "VARCHAR(500) DEFAULT NULL",
"oauth_scope" => "VARCHAR(500) DEFAULT NULL",
"oauth_extra_params" => "TEXT DEFAULT NULL",
"oauth_access_token" => "TEXT DEFAULT NULL",
"oauth_token_expires" => "BIGINT UNSIGNED DEFAULT NULL",
"oauth_last_refresh_error" => "TEXT DEFAULT NULL",
"oauth_last_refresh_attempt" => "DATETIME NULL DEFAULT NULL",
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
"modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP",
"active" => "TINYINT(1) NOT NULL DEFAULT '1'"
),
"keys" => array(
"primary" => array(
"" => array("id")
),
"unique" => array(
"uniq_creator_name" => array("created_by", "name")
),
"key" => array(
"idx_created_by" => array("created_by"),
"idx_scope" => array("scope")
)
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
),
"imapsync_source_domain" => array(
"cols" => array(
"source_id" => "INT NOT NULL",
"domain" => "VARCHAR(255) NOT NULL"
),
"keys" => array(
"primary" => array(
"" => array("source_id", "domain")
),
"key" => array(
"idx_domain" => array("domain")
),
"fkey" => array(
"fk_imapsync_source_domain" => array(
"col" => "source_id",
"ref" => "imapsync_source.id",
"delete" => "CASCADE",
"update" => "NO ACTION"
)
)
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
),
"imapsync_source_user" => array(
"cols" => array(
"source_id" => "INT NOT NULL",
"username" => "VARCHAR(255) NOT NULL"
),
"keys" => array(
"primary" => array(
"" => array("source_id", "username")
),
"key" => array(
"idx_username" => array("username")
),
"fkey" => array(
"fk_imapsync_source_user" => array(
"col" => "source_id",
"ref" => "imapsync_source.id",
"delete" => "CASCADE",
"update" => "NO ACTION"
)
)
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
),
"imapsync_source_oauth_token" => array(
"cols" => array(
"source_id" => "INT NOT NULL",
"owner" => "VARCHAR(255) NOT NULL",
"username" => "VARCHAR(255) NOT NULL",
"access_token" => "TEXT DEFAULT NULL",
"refresh_token" => "TEXT DEFAULT NULL",
"token_expires" => "BIGINT UNSIGNED DEFAULT NULL",
"last_refresh_error" => "TEXT DEFAULT NULL",
"oauth_last_refresh_attempt" => "DATETIME NULL DEFAULT NULL",
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
"modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP"
),
"keys" => array(
"primary" => array(
"" => array("source_id", "owner", "username")
),
"key" => array(
"idx_owner" => array("owner")
),
"fkey" => array(
"fk_imapsync_source_oauth_token" => array(
"col" => "source_id",
"ref" => "imapsync_source.id",
"delete" => "CASCADE",
"update" => "NO ACTION"
)
)
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
),
"imapsync" => array(
"cols" => array(
"id" => "INT NOT NULL AUTO_INCREMENT",
"user2" => "VARCHAR(255) NOT NULL",
"host1" => "VARCHAR(255) NOT NULL",
"authmech1" => "ENUM('PLAIN','LOGIN','CRAM-MD5') DEFAULT 'PLAIN'",
"source_id" => "INT NOT NULL",
"regextrans2" => "VARCHAR(255) DEFAULT ''",
"authmd51" => "TINYINT(1) NOT NULL DEFAULT 0",
"domain2" => "VARCHAR(255) NOT NULL DEFAULT ''",
"subfolder2" => "VARCHAR(255) NOT NULL DEFAULT ''",
"user1" => "VARCHAR(255) NOT NULL",
"password1" => "VARCHAR(255) NOT NULL",
"password1" => "VARCHAR(255) NOT NULL DEFAULT ''",
"exclude" => "VARCHAR(500) NOT NULL DEFAULT ''",
"maxage" => "SMALLINT NOT NULL DEFAULT '0'",
"mins_interval" => "SMALLINT UNSIGNED NOT NULL DEFAULT '0'",
"maxbytespersecond" => "VARCHAR(50) NOT NULL DEFAULT '0'",
"port1" => "SMALLINT UNSIGNED NOT NULL",
"enc1" => "ENUM('TLS','SSL','PLAIN') DEFAULT 'TLS'",
"delete2duplicates" => "TINYINT(1) NOT NULL DEFAULT '1'",
"delete1" => "TINYINT(1) NOT NULL DEFAULT '0'",
"delete2" => "TINYINT(1) NOT NULL DEFAULT '0'",
"automap" => "TINYINT(1) NOT NULL DEFAULT '0'",
"skipcrossduplicates" => "TINYINT(1) NOT NULL DEFAULT '0'",
"custom_params" => "VARCHAR(512) NOT NULL DEFAULT ''",
"custom_params" => "TEXT",
// original legacy custom_params string, only set when options were dropped during migration (never used by the runner)
"custom_params_legacy" => "TEXT DEFAULT NULL",
"timeout1" => "SMALLINT NOT NULL DEFAULT '600'",
"timeout2" => "SMALLINT NOT NULL DEFAULT '600'",
"subscribeall" => "TINYINT(1) NOT NULL DEFAULT '1'",
"dry" => "TINYINT(1) NOT NULL DEFAULT '0'",
"is_running" => "TINYINT(1) NOT NULL DEFAULT '0'",
"prio" => "INT NOT NULL DEFAULT 0",
"returned_text" => "LONGTEXT",
"last_run" => "TIMESTAMP NULL DEFAULT NULL",
"success" => "TINYINT(1) UNSIGNED DEFAULT NULL",
@@ -795,6 +913,32 @@ function init_db_schema()
"keys" => array(
"primary" => array(
"" => array("id")
),
"key" => array(
"idx_source_id" => array("source_id"),
"idx_prio" => array("prio")
),
"fkey" => array(
"fk_imapsync_source" => array(
"col" => "source_id",
"ref" => "imapsync_source.id",
"delete" => "RESTRICT",
"update" => "NO ACTION"
)
)
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
),
"imapsync_settings" => array(
"cols" => array(
"name" => "VARCHAR(64) NOT NULL",
"value" => "VARCHAR(255) NOT NULL",
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
"modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP"
),
"keys" => array(
"primary" => array(
"" => array("name")
)
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
@@ -1184,11 +1328,128 @@ function init_db_schema()
}
}
// Migrate legacy imapsync jobs (host/port/enc per job) to domain-scoped legacy sources
// Re-entrant: only rows with source_id IS NULL are migrated, the legacy columns are kept while any are left
if ($table == 'imapsync') {
$stmt = $pdo->query("SHOW TABLES LIKE 'imapsync'");
if (count($stmt->fetchAll(PDO::FETCH_ASSOC)) != 0) {
$imapsync_cols = array_column($pdo->query("SHOW COLUMNS FROM `imapsync`")->fetchAll(PDO::FETCH_ASSOC), 'Field', 'Field');
if (isset($imapsync_cols['host1'])) {
if (!isset($imapsync_cols['source_id'])) {
$pdo->exec("ALTER TABLE `imapsync` ADD COLUMN `source_id` INT DEFAULT NULL");
}
if (!isset($imapsync_cols['custom_params_legacy'])) {
$pdo->exec("ALTER TABLE `imapsync` ADD COLUMN `custom_params_legacy` TEXT DEFAULT NULL");
}
// Converted JSON may not fit into the old VARCHAR(512)
$pdo->exec("ALTER TABLE `imapsync` MODIFY COLUMN `custom_params` TEXT");
$imapsync_allow = \Mailcow\Imapsync\Model\CustomParams::allowlist($GLOBALS['IMAPSYNC_OPTIONS'] ?? [], $GLOBALS['IMAPSYNC_OPTIONS_DEFAULT'] ?? []);
$source_sel = $pdo->prepare("SELECT `id` FROM `imapsync_source` WHERE `created_by` = '' AND `name` = :name");
$source_ins = $pdo->prepare("INSERT IGNORE INTO `imapsync_source` (`name`, `description`, `created_by`, `scope`, `host1`, `port1`, `enc1`, `auth_type`, `active`)
VALUES (:name, '', '', 'domain', :host, :port, :enc, :auth, 1)");
$source_domain_ins = $pdo->prepare("INSERT IGNORE INTO `imapsync_source_domain` (`source_id`, `domain`) VALUES (:id, :domain)");
$job_upd = $pdo->prepare("UPDATE `imapsync` SET `source_id` = :source_id, `custom_params` = :cp, `custom_params_legacy` = :cp_legacy,
`dry` = GREATEST(`dry`, :dry) WHERE `id` = :id AND `source_id` IS NULL");
$stmt = $pdo->query("SELECT `id`, `user2`, `host1`, `port1`, `enc1`, `custom_params` FROM `imapsync` WHERE `source_id` IS NULL");
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
// Parse the legacy custom_params string like the old runner did (all tokens up to the next option are its value)
// into pairs valid under $IMAPSYNC_OPTIONS, a token after a flag is never its value.
// --authmech1 PLAIN|LOGIN|CRAM-MD5 becomes the source auth type, --dry the job's dry flag (never run a simulation for real).
// Anything else that does not fit is dropped, the original string is then kept in custom_params_legacy.
$pairs = array();
$auth = null;
$dry = false;
$dropped = false;
$tokens = preg_split('/\s+/', trim((string)$row['custom_params']), -1, PREG_SPLIT_NO_EMPTY);
for ($i = 0, $n = count($tokens); $i < $n; $i++) {
if ($tokens[$i][0] !== '-') {
// stray value without option
$dropped = true;
continue;
}
@list($opt, $val) = explode('=', ltrim($tokens[$i], '-'), 2);
$opt = strtolower($opt);
$inline = ($val !== null);
$rest = array();
while ($i + 1 < $n && $tokens[$i + 1][0] !== '-') {
$rest[] = $tokens[++$i];
}
$rule = ($opt === 'authmech1') ? true : ($imapsync_allow[$opt] ?? null);
if ($opt === 'dry' || $rule === false) {
if ($inline || !empty($rest)) $dropped = true;
if ($opt === 'dry') {
$dry = true;
} elseif (!$inline) {
$pairs[] = array('o' => $opt, 'v' => '');
}
continue;
}
// unknown option, or "--opt=a b" (the old runner cut the value at the first space)
if ($rule === null || ($inline && !empty($rest))) {
$dropped = true;
continue;
}
$val = $inline ? $val : implode(' ', $rest);
if ($opt === 'authmech1' && $auth === null && in_array(strtoupper($val), array('PLAIN', 'LOGIN', 'CRAM-MD5'), true)) {
$auth = strtoupper($val);
} elseif ($opt === 'authmech1' || trim($val) === ''
|| (is_string($rule) && preg_match('~^(?:' . str_replace('~', '\~', $rule) . ')$~D', $val) !== 1)) {
$dropped = true;
} else {
$pairs[] = array('o' => $opt, 'v' => $val);
}
}
// Old runner ignored the authmech1 column, only a custom --authmech1 was effective
$auth = $auth ?? 'PLAIN';
$host = strtolower(trim((string)$row['host1']));
$port = (intval($row['port1']) >= 1 && intval($row['port1']) <= 65535) ? intval($row['port1']) : 143;
$enc = in_array($row['enc1'], array('TLS', 'SSL', 'PLAIN'), true) ? $row['enc1'] : 'TLS';
$at = strrchr((string)$row['user2'], '@');
$domain = ($at === false) ? '' : strtolower(substr($at, 1));
// One source per (host, port, enc, auth, domain), name always unique and <= 100 chars
$name = 'legacy-' . mb_substr($host, 0, 60) . '-' . $port . '-' . substr(sha1($host . '|' . $port . '|' . $enc . '|' . $auth . '|' . $domain), 0, 8);
$source_sel->execute(array(':name' => $name));
if (($source_id = $source_sel->fetchColumn()) === false) {
$source_ins->execute(array(':name' => $name, ':host' => $host, ':port' => $port, ':enc' => $enc, ':auth' => $auth));
$source_sel->execute(array(':name' => $name));
if (($source_id = $source_sel->fetchColumn()) === false) continue;
}
if ($domain !== '') {
$source_domain_ins->execute(array(':id' => $source_id, ':domain' => $domain));
}
$job_upd->execute(array(
':source_id' => $source_id,
':cp' => json_encode($pairs, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
':cp_legacy' => $dropped ? $row['custom_params'] : null,
':dry' => $dry ? 1 : 0,
':id' => $row['id']
));
}
// Never throw on leftovers: keep source_id nullable and the legacy columns, retried with the next db_version
$pending = intval($pdo->query("SELECT COUNT(*) FROM `imapsync` WHERE `source_id` IS NULL")->fetchColumn());
if ($pending > 0) {
$properties['cols']['source_id'] = "INT DEFAULT NULL";
$legacy_col_types = array(
'host1' => "VARCHAR(255) DEFAULT NULL",
'port1' => "SMALLINT UNSIGNED DEFAULT NULL",
'enc1' => "ENUM('TLS','SSL','PLAIN') DEFAULT NULL",
'authmech1' => "ENUM('PLAIN','LOGIN','CRAM-MD5') DEFAULT NULL"
);
foreach ($legacy_col_types as $col => $type) {
if (isset($imapsync_cols[$col])) $properties['cols'][$col] = $type;
}
error_log('mailcow: imapsync migration: ' . $pending . ' sync job(s) could not be assigned to a source and will not run');
}
}
}
}
$stmt = $pdo->query("SHOW TABLES LIKE '" . $table . "'");
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if ($num_results != 0) {
$stmt = $pdo->prepare("SELECT CONCAT('ALTER TABLE `', `table_schema`, '`.', `table_name`, ' DROP FOREIGN KEY ', `constraint_name`, ';') AS `FKEY_DROP` FROM `information_schema`.`table_constraints`
WHERE `constraint_type` = 'FOREIGN KEY' AND `table_name` = :table;");
WHERE `constraint_type` = 'FOREIGN KEY' AND `table_schema` = DATABASE() AND `table_name` = :table;");
$stmt->execute(array(':table' => $table));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($row = array_shift($rows)) {
@@ -1349,20 +1610,29 @@ function init_db_schema()
AND (JSON_CONTAINS_PATH(`call`, 'one', '$[2].password')
OR JSON_CONTAINS_PATH(`call`, 'one', '$[2].password2'));");
// Mitigate imapsync argument injection issue
$pdo->query("UPDATE `imapsync` SET `custom_params` = ''
WHERE `custom_params` LIKE '%pipemess%'
OR custom_params LIKE '%skipmess%'
OR custom_params LIKE '%delete2foldersonly%'
OR custom_params LIKE '%delete2foldersbutnot%'
OR custom_params LIKE '%regexflag%'
OR custom_params LIKE '%pipemess%'
OR custom_params LIKE '%regextrans2%'
OR custom_params LIKE '%maxlinelengthcmd%';");
// Mask syncjob secrets in old log entries (data and attr arguments of the logged call)
$mask_paths = array();
$unmasked = array();
foreach (array(3, 4) as $idx) {
foreach (array('password1', 'password2', 'oauth_client_secret', 'oauth_extra_params') as $key) {
$mask_paths[] = "'$[" . $idx . "]." . $key . "'";
// only rows still holding a value other than the string '*' (objects/arrays/numbers included), missing paths are NULL
$unmasked[] = "JSON_UNQUOTE(JSON_EXTRACT(`call`, '$[" . $idx . "]." . $key . "')) <> '*'";
}
}
$pdo->exec("UPDATE `logs` SET `call` = JSON_REPLACE(`call`, " . implode(", '*', ", $mask_paths) . ", '*')
WHERE JSON_VALID(`call`)
AND (JSON_VALUE(`call`, '$[0]') = 'syncjob'
OR JSON_VALUE(`call`, '$[0]') LIKE 'imapsync%'
OR (JSON_VALUE(`call`, '$[0]') = 'mailbox' AND JSON_VALUE(`call`, '$[2]') = 'syncjob'))
AND (" . implode(" OR ", $unmasked) . ");");
// Migrate webauthn tfa
$stmt = $pdo->query("ALTER TABLE `tfa` MODIFY COLUMN `authmech` ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')");
// Syncjobs: seed global settings
$pdo->query("INSERT IGNORE INTO `imapsync_settings` (`name`, `value`) VALUES ('max_parallel', '1')");
// Inject admin if not exists
$stmt = $pdo->query("SELECT NULL FROM `admin`");
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -1544,6 +1814,10 @@ function init_db_schema()
}
if (php_sapi_name() == "cli") {
include '/web/inc/vars.inc.php';
if (file_exists('/web/inc/vars.local.inc.php')) {
include '/web/inc/vars.local.inc.php';
}
require_once '/web/inc/lib/vendor/autoload.php';
include '/web/inc/functions.inc.php';
include '/web/inc/functions.docker.inc.php';
// $now = new DateTime();
@@ -1561,7 +1835,7 @@ if (php_sapi_name() == "cli") {
//PDO::MYSQL_ATTR_INIT_COMMAND => "SET time_zone = '" . $offset . "', group_concat_max_len = 3423543543;",
];
$pdo = new PDO($dsn, $database_user, $database_pass, $opt);
$stmt = $pdo->query("SELECT COUNT('OK') AS OK_C FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'sogo_view' OR TABLE_NAME = '_sogo_static_view';");
$stmt = $pdo->query("SELECT COUNT('OK') AS OK_C FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND (TABLE_NAME = 'sogo_view' OR TABLE_NAME = '_sogo_static_view');");
$res = $stmt->fetch(PDO::FETCH_ASSOC);
if (intval($res['OK_C']) === 2) {
// Be more precise when replacing into _sogo_static_view, col orders may change
+94 -127
View File
@@ -287,131 +287,98 @@ $RSPAMD_MAPS = array(
);
$IMAPSYNC_OPTIONS = array(
'whitelist' => array(
'abort',
'authmd51',
'authmd52',
'authmech1',
'authmech2',
'authuser1',
'authuser2',
'debug',
'debugcontent',
'debugcrossduplicates',
'debugflags',
'debugfolders',
'debugimap',
'debugimap1',
'debugimap2',
'debugmemory',
'debugssl',
'delete1emptyfolders',
'delete2folders',
'disarmreadreceipts',
'domain1',
'domain2',
'domino1',
'domino2',
'dry',
'errorsmax',
'exchange1',
'exchange2',
'exitwhenover',
'expunge1',
'f1f2',
'filterbuggyflags',
'folder',
'folderfirst',
'folderlast',
'folderrec',
'gmail1',
'gmail2',
'idatefromheader',
'include',
'inet4',
'inet6',
'justconnect',
'justfolders',
'justfoldersizes',
'justlogin',
'keepalive1',
'keepalive2',
'log',
'logdir',
'logfile',
'maxbytesafter',
'maxlinelength',
'maxmessagespersecond',
'maxsize',
'maxsleep',
'minage',
'minsize',
'noabletosearch',
'noabletosearch1',
'noabletosearch2',
'noexpunge1',
'noexpunge2',
'nofoldersizesatend',
'noid',
'nolog',
'nomixfolders',
'noresyncflags',
'nossl1',
'nossl2',
'nosyncacls',
'notls1',
'notls2',
'nouidexpunge2',
'nousecache',
'oauthaccesstoken1',
'oauthaccesstoken2',
'oauthdirect1',
'oauthdirect2',
'office1',
'office2',
'pidfile',
'pidfilelocking',
'prefix1',
'prefix2',
'proxyauth1',
'proxyauth2',
'resyncflags',
'resynclabels',
'search',
'search1',
'search2',
'sep1',
'sep2',
'showpasswords',
'skipemptyfolders',
'ssl2',
'sslargs1',
'sslargs2',
'subfolder1',
'subscribe',
'subscribed',
'syncacls',
'syncduplicates',
'syncinternaldates',
'synclabels',
'tests',
'testslive',
'testslive6',
'tls2',
'truncmess',
'usecache',
'useheader',
'useuid'
),
'blacklist' => array(
'skipmess',
'delete2foldersonly',
'delete2foldersbutnot',
'regexflag',
'regexmess',
'pipemess',
'regextrans2',
'maxlinelengthcmd'
)
// Allowed imapsync options for a sync job's custom_params (key = option name, lowercase, without dashes):
// true = option requires a non-empty value
// false = pure flag, must NOT have a value
// string = PCRE (no delimiters) the whole non-empty value must match
// Anything not listed here is rejected when a job is saved (the runner passes stored options as they are).
// Override in vars.local.inc.php per option, e.g. $IMAPSYNC_OPTIONS['someoption'] = true; or unset($IMAPSYNC_OPTIONS['debug']);
// The old format $IMAPSYNC_OPTIONS['whitelist'] (list of names) is no longer supported and ignored (logged).
// NEVER add (RCE): 'delete2foldersonly', 'delete2foldersbutnot', 'regexflag', 'regexmess', 'pipemess', 'regextrans2', 'maxlinelengthcmd'
// NEVER add options taking local file paths (e.g. 'logfile', 'logdir', 'pidfile', 'passfile1', 'sslargs1' with SSL_ca_file)
$IMAPSYNC_OPTIONS_DEFAULT = array(
'abort' => false,
'authmd51' => false,
'authuser1' => true,
'debug' => false,
'debugcontent' => false,
'debugcrossduplicates' => false,
'debugflags' => false,
'debugfolders' => false,
'debugimap' => false,
'debugimap1' => false,
'debugmemory' => false,
'debugssl' => true,
'delete1emptyfolders' => false,
'delete2folders' => false,
'disarmreadreceipts' => false,
'domain1' => true,
'domino1' => false,
'errorsmax' => true,
'exchange1' => false,
'exitwhenover' => true,
'expunge1' => false,
'f1f2' => true,
'filterbuggyflags' => false,
'folder' => true,
'folderfirst' => true,
'folderlast' => true,
'folderrec' => true,
'gmail1' => false,
'idatefromheader' => false,
'include' => true,
'inet4' => false,
'inet6' => false,
'justconnect' => false,
'justfolders' => false,
'justfoldersizes' => false,
'justlogin' => false,
'keepalive1' => false,
'log' => false,
'maxbytesafter' => true,
'maxlinelength' => true,
'maxmessagespersecond' => true,
'maxsize' => true,
'maxsleep' => true,
'minage' => true,
'minsize' => true,
'noabletosearch' => false,
'noabletosearch1' => false,
'noexpunge1' => false,
'nofoldersizesatend' => false,
'noid' => false,
'nolog' => false,
'nomixfolders' => false,
'noresyncflags' => false,
'nossl1' => false,
'nosyncacls' => false,
'notls1' => false,
'nousecache' => false,
'office1' => false,
'prefix1' => true,
'prefix2' => true,
'proxyauth1' => false,
'resyncflags' => false,
'resynclabels' => false,
'search' => true,
'search1' => true,
'sep1' => true,
'sep2' => true,
'skipemptyfolders' => false,
'sslargs1' => '^SSL_verify_mode=[01]$',
'subfolder1' => true,
'subscribe' => false,
'subscribed' => false,
'syncacls' => false,
'syncduplicates' => false,
'syncinternaldates' => false,
'synclabels' => false,
'tests' => false,
'testslive' => false,
'testslive6' => false,
'truncmess' => true,
'usecache' => false,
'useheader' => true,
'useuid' => false,
);
$IMAPSYNC_OPTIONS = $IMAPSYNC_OPTIONS_DEFAULT;