From 10dfd0a4433df4ad519115e8dc380b58543b383b Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Wed, 21 Aug 2024 10:10:34 +0200 Subject: [PATCH 1/8] [Web][DockerApi] Add the ability to rename the local part of a mailbox --- .../dockerapi/modules/DockerApi.py | 44 +++- data/web/inc/functions.inc.php | 62 ++--- data/web/inc/functions.mailbox.inc.php | 238 ++++++++++++++---- data/web/inc/triggers.inc.php | 29 ++- data/web/js/site/edit.js | 5 + data/web/js/site/mailbox.js | 6 +- data/web/json_api.php | 5 +- data/web/lang/lang.de-de.json | 7 +- data/web/lang/lang.en-gb.json | 5 + data/web/templates/edit/mailbox.twig | 58 ++++- docker-compose.yml | 4 +- 11 files changed, 356 insertions(+), 107 deletions(-) diff --git a/data/Dockerfiles/dockerapi/modules/DockerApi.py b/data/Dockerfiles/dockerapi/modules/DockerApi.py index 5601990916..9fdd039e47 100644 --- a/data/Dockerfiles/dockerapi/modules/DockerApi.py +++ b/data/Dockerfiles/dockerapi/modules/DockerApi.py @@ -159,7 +159,7 @@ def container_post__exec__mailq__deliver(self, request_json, **kwargs): postqueue_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix') # todo: check each exit code res = { 'type': 'success', 'msg': 'Scheduled immediate delivery'} - return Response(content=json.dumps(res, indent=4), media_type="application/json") + return Response(content=json.dumps(res, indent=4), media_type="application/json") # api call: container_post - post_action: exec - cmd: mailq - task: list def container_post__exec__mailq__list(self, request_json, **kwargs): if 'container_id' in kwargs: @@ -318,7 +318,7 @@ def container_post__exec__sieve__print(self, request_json, **kwargs): if 'username' in request_json and 'script_name' in request_json: for container in self.sync_docker_client.containers.list(filters=filters): - cmd = ["/bin/bash", "-c", "/usr/bin/doveadm sieve get -u '" + request_json['username'].replace("'", "'\\''") + "' '" + request_json['script_name'].replace("'", "'\\''") + "'"] + cmd = ["/bin/bash", "-c", "/usr/bin/doveadm sieve get -u '" + request_json['username'].replace("'", "'\\''") + "' '" + request_json['script_name'].replace("'", "'\\''") + "'"] sieve_return = container.exec_run(cmd) return self.exec_run_handler('utf8_text_only', sieve_return) # api call: container_post - post_action: exec - cmd: maildir - task: cleanup @@ -342,6 +342,30 @@ def container_post__exec__maildir__cleanup(self, request_json, **kwargs): cmd = ["/bin/bash", "-c", cmd_vmail] maildir_cleanup = container.exec_run(cmd, user='vmail') return self.exec_run_handler('generic', maildir_cleanup) + # api call: container_post - post_action: exec - cmd: maildir - task: move + def container_post__exec__maildir__move(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + if 'old_maildir' in request_json and 'new_maildir' in request_json: + for container in self.sync_docker_client.containers.list(filters=filters): + vmail_name = request_json['old_maildir'].replace("'", "'\\''") + new_vmail_name = request_json['new_maildir'].replace("'", "'\\''") + cmd_vmail = "if [[ -d '/var/vmail/" + vmail_name + "' ]]; then /bin/mv '/var/vmail/" + vmail_name + "' '/var/vmail/" + new_vmail_name + "'; fi" + + index_name = request_json['old_maildir'].split("/") + new_index_name = request_json['new_maildir'].split("/") + if len(index_name) > 1 and len(new_index_name) > 1: + index_name = index_name[1].replace("'", "'\\''") + "@" + index_name[0].replace("'", "'\\''") + new_index_name = new_index_name[1].replace("'", "'\\''") + "@" + new_index_name[0].replace("'", "'\\''") + cmd_vmail_index = "if [[ -d '/var/vmail_index/" + index_name + "' ]]; then /bin/mv '/var/vmail_index/" + index_name + "' '/var/vmail_index/" + new_index_name + "_index'; fi" + cmd = ["/bin/bash", "-c", cmd_vmail + " && " + cmd_vmail_index] + else: + cmd = ["/bin/bash", "-c", cmd_vmail] + maildir_move = container.exec_run(cmd, user='vmail') + return self.exec_run_handler('generic', maildir_move) # api call: container_post - post_action: exec - cmd: rspamd - task: worker_password def container_post__exec__rspamd__worker_password(self, request_json, **kwargs): if 'container_id' in kwargs: @@ -374,6 +398,20 @@ def container_post__exec__rspamd__worker_password(self, request_json, **kwargs): self.logger.error('failed changing Rspamd password') res = { 'type': 'danger', 'msg': 'command did not complete' } return Response(content=json.dumps(res, indent=4), media_type="application/json") + # api call: container_post - post_action: exec - cmd: sogo - task: rename + def container_post__exec__sogo__rename_user(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + if 'old_username' in request_json and 'new_username' in request_json: + for container in self.sync_docker_client.containers.list(filters=filters): + old_username = request_json['old_username'].replace("'", "'\\''") + new_username = request_json['new_username'].replace("'", "'\\''") + + sogo_return = container.exec_run(['sogo-tool', 'rename-user', old_username, new_username], user='sogo') + return self.exec_run_handler('generic', sogo_return) # Collect host stats async def get_host_stats(self, wait=5): @@ -462,7 +500,7 @@ def recv_socket_data(c_socket, timeout): except: pass return ''.join(total_data) - + try : socket = container.exec_run([shell_cmd], stdin=True, socket=True, user=user).output._sock if not cmd.endswith("\n"): diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 25d08b9fe5..fd6c7fc2f3 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -939,10 +939,10 @@ function check_login($user, $pass, $app_passwd_data = false) { $stmt->execute(array(':user' => $user)); $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC)); } - foreach ($rows as $row) { + foreach ($rows as $row) { // verify password if (verify_hash($row['password'], $pass) !== false) { - if (!array_key_exists("app_passwd_id", $row)){ + if (!array_key_exists("app_passwd_id", $row)){ // password is not a app password // check for tfa authenticators $authenticators = get_tfa($user); @@ -953,11 +953,6 @@ function check_login($user, $pass, $app_passwd_data = false) { $_SESSION['pending_mailcow_cc_role'] = "user"; $_SESSION['pending_tfa_methods'] = $authenticators['additional']; unset($_SESSION['ldelay']); - $_SESSION['return'][] = array( - 'type' => 'success', - 'log' => array(__FUNCTION__, $user, '*'), - 'msg' => array('logged_in_as', $user) - ); return "pending"; } else if (!isset($authenticators['additional']) || !is_array($authenticators['additional']) || count($authenticators['additional']) == 0) { // no authenticators found, login successfull @@ -966,6 +961,11 @@ function check_login($user, $pass, $app_passwd_data = false) { $stmt->execute(array(':user' => $user)); unset($_SESSION['ldelay']); + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $user, '*'), + 'msg' => array('logged_in_as', $user) + ); return "user"; } } elseif ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) { @@ -1028,7 +1028,7 @@ function update_sogo_static_view($mailbox = null) { // Check if the mailbox exists $stmt = $pdo->prepare("SELECT username FROM mailbox WHERE username = :mailbox AND active = '1'"); $stmt->execute(array(':mailbox' => $mailbox)); - $row = $stmt->fetch(PDO::FETCH_ASSOC); + $row = $stmt->fetch(PDO::FETCH_ASSOC); if ($row){ $mailbox_exists = true; } @@ -1056,7 +1056,7 @@ function update_sogo_static_view($mailbox = null) { LEFT OUTER JOIN grouped_sender_acl_external external_acl ON external_acl.username = mailbox.username WHERE mailbox.active = '1'"; - + if ($mailbox_exists) { $query .= " AND mailbox.username = :mailbox"; $stmt = $pdo->prepare($query); @@ -1065,9 +1065,9 @@ function update_sogo_static_view($mailbox = null) { $query .= " GROUP BY mailbox.username"; $stmt = $pdo->query($query); } - + $stmt = $pdo->query("DELETE FROM _sogo_static_view WHERE `c_uid` NOT IN (SELECT `username` FROM `mailbox` WHERE `active` = '1');"); - + flush_memcached(); } function edit_user_account($_data) { @@ -1100,7 +1100,7 @@ function edit_user_account($_data) { AND `username` = :user"); $stmt->execute(array(':user' => $username)); $row = $stmt->fetch(PDO::FETCH_ASSOC); - + if (!verify_hash($row['password'], $password_old)) { $_SESSION['return'][] = array( 'type' => 'danger', @@ -1109,7 +1109,7 @@ function edit_user_account($_data) { ); return false; } - + $password_new = $_data['user_new_pass']; $password_new2 = $_data['user_new_pass2']; if (password_check($password_new, $password_new2) !== true) { @@ -1124,7 +1124,7 @@ function edit_user_account($_data) { ':password_hashed' => $password_hashed, ':username' => $username )); - + update_sogo_static_view(); } // edit password recovery email @@ -1374,7 +1374,7 @@ function set_tfa($_data) { $_data['registration']->certificate, 0 )); - + $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $_data_log), @@ -1544,7 +1544,7 @@ function unset_tfa_key($_data) { try { if (!is_numeric($id)) $access_denied = true; - + // set access_denied error if ($access_denied){ $_SESSION['return'][] = array( @@ -1553,7 +1553,7 @@ function unset_tfa_key($_data) { 'msg' => 'access_denied' ); return false; - } + } // check if it's last key $stmt = $pdo->prepare("SELECT COUNT(*) AS `keys` FROM `tfa` @@ -1602,7 +1602,7 @@ function get_tfa($username = null, $id = null) { WHERE `username` = :username AND `active` = '1'"); $stmt->execute(array(':username' => $username)); $results = $stmt->fetchAll(PDO::FETCH_ASSOC); - + // no tfa methods found if (count($results) == 0) { $data['name'] = 'none'; @@ -1810,8 +1810,8 @@ function verify_tfa_login($username, $_data) { 'msg' => array('webauthn_authenticator_failed') ); return false; - } - + } + if (empty($process_webauthn['publicKey']) || $process_webauthn['publicKey'] === false) { $_SESSION['return'][] = array( 'type' => 'danger', @@ -2173,7 +2173,7 @@ function cors($action, $data = null) { 'msg' => 'access_denied' ); return false; - } + } $allowed_origins = isset($data['allowed_origins']) ? $data['allowed_origins'] : array($_SERVER['SERVER_NAME']); $allowed_origins = !is_array($allowed_origins) ? array_filter(array_map('trim', explode("\n", $allowed_origins))) : $allowed_origins; @@ -2206,7 +2206,7 @@ function cors($action, $data = null) { $redis->hMSet('CORS_SETTINGS', array( 'allowed_origins' => implode(', ', $allowed_origins), 'allowed_methods' => implode(', ', $allowed_methods) - )); + )); } catch (RedisException $e) { $_SESSION['return'][] = array( 'type' => 'danger', @@ -2258,10 +2258,10 @@ function cors($action, $data = null) { header('Access-Control-Allow-Headers: Accept, Content-Type, X-Api-Key, Origin'); // Access-Control settings requested, this is just a preflight request - if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS' && + if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS' && isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD']) && isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) { - + $allowed_methods = explode(', ', $cors_settings["allowed_methods"]); if (in_array($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'], $allowed_methods, true)) // method allowed send 200 OK @@ -2315,7 +2315,7 @@ function reset_password($action, $data = null) { break; case 'issue': $username = $data; - + // perform cleanup $stmt = $pdo->prepare("DELETE FROM `reset_password` WHERE created < DATE_SUB(NOW(), INTERVAL :lifetime MINUTE);"); $stmt->execute(array(':lifetime' => $PW_RESET_TOKEN_LIFETIME)); @@ -2397,8 +2397,8 @@ function reset_password($action, $data = null) { $request_date = new DateTime(); $locale_date = locale_get_default(); $date_formatter = new IntlDateFormatter( - $locale_date, - IntlDateFormatter::FULL, + $locale_date, + IntlDateFormatter::FULL, IntlDateFormatter::FULL ); $formatted_request_date = $date_formatter->format($request_date); @@ -2514,7 +2514,7 @@ function reset_password($action, $data = null) { $stmt->execute(array( ':username' => $username )); - + $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $action, $_data_log), @@ -2557,7 +2557,7 @@ function reset_password($action, $data = null) { $text = $data['text']; $html = $data['html']; $subject = $data['subject']; - + if (!filter_var($from, FILTER_VALIDATE_EMAIL)) { $_SESSION['return'][] = array( 'type' => 'danger', @@ -2590,7 +2590,7 @@ function reset_password($action, $data = null) { ); return false; } - + ini_set('max_execution_time', 0); ini_set('max_input_time', 0); $mail = new PHPMailer; @@ -2622,7 +2622,7 @@ function reset_password($action, $data = null) { return false; } $mail->ClearAllRecipients(); - + return true; break; } diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index c927ce4947..35e21f1c83 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -1233,7 +1233,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ':active' => $active )); - + if (isset($_data['acl'])) { $_data['acl'] = (array)$_data['acl']; $_data['spam_alias'] = (in_array('spam_alias', $_data['acl'])) ? 1 : 0; @@ -1265,14 +1265,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $_data['quarantine_attachments'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine_attachments']); $_data['quarantine_notification'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine_notification']); $_data['quarantine_category'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine_category']); - $_data['app_passwds'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_app_passwds']); - $_data['pw_reset'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_pw_reset']); + $_data['app_passwds'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_app_passwds']); + $_data['pw_reset'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_pw_reset']); } try { - $stmt = $pdo->prepare("INSERT INTO `user_acl` + $stmt = $pdo->prepare("INSERT INTO `user_acl` (`username`, `spam_alias`, `tls_policy`, `spam_score`, `spam_policy`, `delimiter_action`, `syncjobs`, `eas_reset`, `sogo_profile_reset`, - `pushover`, `quarantine`, `quarantine_attachments`, `quarantine_notification`, `quarantine_category`, `app_passwds`, `pw_reset`) + `pushover`, `quarantine`, `quarantine_attachments`, `quarantine_notification`, `quarantine_category`, `app_passwds`, `pw_reset`) VALUES (:username, :spam_alias, :tls_policy, :spam_score, :spam_policy, :delimiter_action, :syncjobs, :eas_reset, :sogo_profile_reset, :pushover, :quarantine, :quarantine_attachments, :quarantine_notification, :quarantine_category, :app_passwds, :pw_reset) "); $stmt->execute(array( @@ -1467,7 +1467,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); return false; } - + // check attributes $attr = array(); $attr['tags'] = (isset($_data['tags'])) ? $_data['tags'] : array(); @@ -1557,7 +1557,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0; $attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0; $attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0; - } + } else { $attr['imap_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['imap_access']); $attr['pop3_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']); @@ -2109,7 +2109,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); return false; } - + // check if param is whitelisted if (!in_array(strtolower($param), $GLOBALS["IMAPSYNC_OPTIONS"]["whitelist"])){ // bad option @@ -2802,11 +2802,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { // check name if ($is_now["template"] == "Default" && $is_now["template"] != $_data["template"]){ // keep template name of Default template - $_data["template"] = $is_now["template"]; + $_data["template"] = $is_now["template"]; } else { - $_data["template"] = (isset($_data["template"])) ? $_data["template"] : $is_now["template"]; - } + $_data["template"] = (isset($_data["template"])) ? $_data["template"] : $is_now["template"]; + } // check attributes $attr = array(); $attr['tags'] = (isset($_data['tags'])) ? $_data['tags'] : array(); @@ -2833,10 +2833,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ":id" => $id , ":template" => $_data["template"] , ":attributes" => json_encode($attr) - )); + )); } - + $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), @@ -3192,7 +3192,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ':tag_name' => $tag, )); } - + $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), @@ -3203,6 +3203,146 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { } return true; break; + case 'mailbox_rename': + $domain = $_data['domain']; + $old_local_part = $_data['old_local_part']; + $old_username = $old_local_part . "@" . $domain; + $new_local_part = $_data['new_local_part']; + $new_username = $new_local_part . "@" . $domain; + $create_alias = intval($_data['create_alias']); + + if (!filter_var($old_username, FILTER_VALIDATE_EMAIL)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => array('username_invalid', $old_username) + ); + return false; + } + if (!filter_var($new_username, FILTER_VALIDATE_EMAIL)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => array('username_invalid', $new_username) + ); + return false; + } + + $is_now = mailbox('get', 'mailbox_details', $old_username); + if (empty($is_now)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => 'access_denied' + ); + return false; + } + + if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $is_now['domain'])) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => 'access_denied' + ); + return false; + } + + // rename username in sql + try { + $pdo->beginTransaction(); + $pdo->exec('SET FOREIGN_KEY_CHECKS = 0'); + + $pdo->prepare('UPDATE mailbox SET username = :new_username, local_part = :new_local_part WHERE username = :old_username') + ->execute([ + ':new_username' => $new_username, + ':new_local_part' => $new_local_part, + ':old_username' => $old_username + ]); + + // Update the username in all related tables + $tables = [ + 'tags_mailbox' => 'username', + 'sieve_filters' => 'username', + 'app_passwd' => 'mailbox', + 'user_acl' => 'username', + 'da_acl' => 'username', + 'quota2' => 'username', + 'quota2replica' => 'username', + 'pushover' => 'username' + ]; + + foreach ($tables as $table => $column) { + $pdo->prepare("UPDATE $table SET $column = :new_username WHERE $column = :old_username") + ->execute([ + ':new_username' => $new_username, + ':old_username' => $old_username + ]); + } + + $pdo->prepare("UPDATE _sogo_static_view SET c_uid = :new_username, c_name = :new_username2, mail = :new_username3 WHERE c_uid = :old_username") + ->execute([ + ':new_username' => $new_username, + ':new_username2' => $new_username, + ':new_username3' => $new_username, + ':old_username' => $old_username + ]); + + $pdo->prepare("UPDATE alias SET address = :new_username, goto = :new_username2 WHERE address = :old_username") + ->execute([ + ':new_username' => $new_username, + ':new_username2' => $new_username, + ':old_username' => $old_username + ]); + + + // Re-enable foreign key checks + $pdo->exec('SET FOREIGN_KEY_CHECKS = 1'); + $pdo->commit(); + } catch (PDOException $e) { + // Rollback the transaction if something goes wrong + $pdo->rollBack(); + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => $e->getMessage() + ); + } + + // move maildir + $exec_fields = array( + 'cmd' => 'maildir', + 'task' => 'move', + 'old_maildir' => $domain . '/' . $old_local_part, + 'new_maildir' => $domain . '/' . $new_local_part + ); + docker('post', 'dovecot-mailcow', 'exec', $exec_fields); + + // rename username in sogo + $exec_fields = array( + 'cmd' => 'sogo', + 'task' => 'rename_user', + 'old_username' => $old_username, + 'new_username' => $new_username + ); + docker('post', 'sogo-mailcow', 'exec', $exec_fields); + + // create alias + if ($create_alias == 1) { + mailbox("add", "alias", array( + "address" => $old_username, + "goto" => $new_username, + "active" => 1, + "sogo_visible" => 1, + "private_comment" => sprintf($lang['success']['mailbox_renamed'], $old_username, $new_username) + )); + } + + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => array('mailbox_renamed', $old_username, $new_username) + ); + break; case 'mailbox_templates': if ($_SESSION['mailcow_cc_role'] != "admin") { $_SESSION['return'][] = array( @@ -3235,11 +3375,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { // check name if ($is_now["template"] == "Default" && $is_now["template"] != $_data["template"]){ // keep template name of Default template - $_data["template"] = $is_now["template"]; + $_data["template"] = $is_now["template"]; } else { - $_data["template"] = (isset($_data["template"])) ? $_data["template"] : $is_now["template"]; - } + $_data["template"] = (isset($_data["template"])) ? $_data["template"] : $is_now["template"]; + } // check attributes $attr = array(); $attr["quota"] = isset($_data['quota']) ? intval($_data['quota']) * 1048576 : 0; @@ -3259,11 +3399,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0; $attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0; $attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0; - } - else { + } + else { foreach ($is_now as $key => $value){ $attr[$key] = $is_now[$key]; - } + } } if (isset($_data['acl'])) { $_data['acl'] = (array)$_data['acl']; @@ -3282,10 +3422,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $attr['acl_quarantine_category'] = (in_array('quarantine_category', $_data['acl'])) ? 1 : 0; $attr['acl_app_passwds'] = (in_array('app_passwds', $_data['acl'])) ? 1 : 0; $attr['acl_pw_reset'] = (in_array('pw_reset', $_data['acl'])) ? 1 : 0; - } else { + } else { foreach ($is_now as $key => $value){ $attr[$key] = $is_now[$key]; - } + } } @@ -3297,7 +3437,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ":id" => $id , ":template" => $_data["template"] , ":attributes" => json_encode($attr) - )); + )); } @@ -3326,7 +3466,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); continue; } - $is_now = mailbox('get', 'mailbox_details', $mailbox); + $is_now = mailbox('get', 'mailbox_details', $mailbox); if(!empty($is_now)){ if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $is_now['domain'])) { $_SESSION['return'][] = array( @@ -3353,15 +3493,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $stmt->execute(array( ":username" => $mailbox, ":custom_attributes" => json_encode($attributes) - )); - + )); + $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => array('mailbox_modified', $mailbox) ); } - + return true; break; case 'resource': @@ -3443,7 +3583,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); } break; - case 'domain_wide_footer': + case 'domain_wide_footer': if (!is_array($_data['domains'])) { $domains = array(); $domains[] = $_data['domains']; @@ -3696,7 +3836,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { // prepend domain to array $params = array(); - foreach ($tags as $key => $val){ + foreach ($tags as $key => $val){ array_push($params, '%'.$_data.'%'); array_push($params, '%'.$val.'%'); } @@ -3705,7 +3845,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); while($row = array_shift($rows)) { - if (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], explode('@', $row['username'])[1])) + if (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], explode('@', $row['username'])[1])) $mailboxes[] = $row['username']; } } @@ -4260,7 +4400,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { while($row = array_shift($rows)) { if ($_SESSION['mailcow_cc_role'] == "admin") $domains[] = $row['domain']; - elseif (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['domain'])) + elseif (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['domain'])) $domains[] = $row['domain']; } } else { @@ -4420,19 +4560,19 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { } $_data = (isset($_data)) ? intval($_data) : null; - if (isset($_data)){ - $stmt = $pdo->prepare("SELECT * FROM `templates` + if (isset($_data)){ + $stmt = $pdo->prepare("SELECT * FROM `templates` WHERE `id` = :id AND type = :type"); $stmt->execute(array( ":id" => $_data, ":type" => "domain" )); $row = $stmt->fetch(PDO::FETCH_ASSOC); - + if (empty($row)){ return false; } - + $row["attributes"] = json_decode($row["attributes"], true); return $row; } @@ -4440,11 +4580,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $stmt = $pdo->prepare("SELECT * FROM `templates` WHERE `type` = 'domain'"); $stmt->execute(); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - + if (empty($rows)){ return false; } - + foreach($rows as $key => $row){ $rows[$key]["attributes"] = json_decode($row["attributes"], true); } @@ -4610,19 +4750,19 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { } $_data = (isset($_data)) ? intval($_data) : null; - if (isset($_data)){ - $stmt = $pdo->prepare("SELECT * FROM `templates` + if (isset($_data)){ + $stmt = $pdo->prepare("SELECT * FROM `templates` WHERE `id` = :id AND type = :type"); $stmt->execute(array( ":id" => $_data, ":type" => "mailbox" )); $row = $stmt->fetch(PDO::FETCH_ASSOC); - + if (empty($row)){ return false; } - + $row["attributes"] = json_decode($row["attributes"], true); return $row; } @@ -5064,7 +5204,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $ids = $_data['ids']; } - + foreach ($ids as $id) { // delete template $stmt = $pdo->prepare("DELETE FROM `templates` @@ -5377,7 +5517,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); continue; } - + update_sogo_static_view($username); $_SESSION['return'][] = array( 'type' => 'success', @@ -5404,7 +5544,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $ids = $_data['ids']; } - + foreach ($ids as $id) { // delete template $stmt = $pdo->prepare("DELETE FROM `templates` @@ -5413,7 +5553,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ":id" => $id, ":type" => "mailbox", ":template" => "Default" - )); + )); } $_SESSION['return'][] = array( @@ -5487,7 +5627,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); } break; - case 'tags_domain': + case 'tags_domain': if (!is_array($_data['domain'])) { $domains = array(); $domains[] = $_data['domain']; @@ -5500,7 +5640,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $wasModified = false; - foreach ($domains as $domain) { + foreach ($domains as $domain) { if (!is_valid_domain_name($domain)) { $_SESSION['return'][] = array( 'type' => 'danger', @@ -5517,7 +5657,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); return false; } - + foreach($tags as $tag){ // delete tag $wasModified = true; @@ -5572,7 +5712,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { // delete tags foreach($tags as $tag){ $wasModified = true; - + $stmt = $pdo->prepare("DELETE FROM `tags_mailbox` WHERE `username` = :username AND `tag_name` = :tag_name"); $stmt->execute(array( ':username' => $username, diff --git a/data/web/inc/triggers.inc.php b/data/web/inc/triggers.inc.php index 5c625e4142..34e47a5449 100644 --- a/data/web/inc/triggers.inc.php +++ b/data/web/inc/triggers.inc.php @@ -18,7 +18,7 @@ if (isset($_POST["pw_reset"])) { $username = reset_password("check", $_POST['token']); $reset_result = reset_password("reset", array( - 'new_password' => $_POST['new_password'], + 'new_password' => $_POST['new_password'], 'new_password2' => $_POST['new_password2'], 'token' => $_POST['token'], 'username' => $username, @@ -52,7 +52,7 @@ unset($_SESSION['pending_mailcow_cc_username']); unset($_SESSION['pending_mailcow_cc_role']); unset($_SESSION['pending_tfa_methods']); - + header("Location: /user"); } } else { @@ -89,27 +89,30 @@ if ($as == "admin") { $_SESSION['mailcow_cc_username'] = $login_user; $_SESSION['mailcow_cc_role'] = "admin"; - header("Location: /admin"); + header("Location: /debug"); + die(); } elseif ($as == "domainadmin") { $_SESSION['mailcow_cc_username'] = $login_user; $_SESSION['mailcow_cc_role'] = "domainadmin"; header("Location: /mailbox"); + die(); } elseif ($as == "user") { $_SESSION['mailcow_cc_username'] = $login_user; $_SESSION['mailcow_cc_role'] = "user"; - $http_parameters = explode('&', $_SESSION['index_query_string']); - unset($_SESSION['index_query_string']); - if (in_array('mobileconfig', $http_parameters)) { - if (in_array('only_email', $http_parameters)) { - header("Location: /mobileconfig.php?only_email"); - die(); - } - header("Location: /mobileconfig.php"); - die(); - } + $http_parameters = explode('&', $_SESSION['index_query_string']); + unset($_SESSION['index_query_string']); + if (in_array('mobileconfig', $http_parameters)) { + if (in_array('only_email', $http_parameters)) { + header("Location: /mobileconfig.php?only_email"); + die(); + } + header("Location: /mobileconfig.php"); + die(); + } header("Location: /user"); + die(); } elseif ($as != "pending") { unset($_SESSION['pending_mailcow_cc_username']); diff --git a/data/web/js/site/edit.js b/data/web/js/site/edit.js index d689549893..f9fe707c63 100644 --- a/data/web/js/site/edit.js +++ b/data/web/js/site/edit.js @@ -58,6 +58,11 @@ $(document).ready(function() { $('input[name=multiple_bookings]').val($("#multiple_bookings_custom").val()); }); + $("#show_mailbox_rename_form").click(function() { + $("#rename_warning").hide(); + $("#rename_form").removeClass("d-none"); + }); + // load tags if ($('#tags').length){ var tagsEl = $('#tags').parent().find('.tag-values')[0]; diff --git a/data/web/js/site/mailbox.js b/data/web/js/site/mailbox.js index 51dbcf4354..2c9fba8f58 100644 --- a/data/web/js/site/mailbox.js +++ b/data/web/js/site/mailbox.js @@ -2354,7 +2354,7 @@ jQuery(function($){ else $(tab).find(".table_collapse_option").hide(); } - + function filterByDomain(json, column, table){ var tableId = $(table.table().container()).attr('id'); // Create the `select` element @@ -2377,12 +2377,12 @@ jQuery(function($){ } }); }); - + // get unique domain list domains = domains.filter(function(value, index, array) { return array.indexOf(value) === index; }); - + // add domains to select domains.forEach(function(domain) { select.append($('')); diff --git a/data/web/json_api.php b/data/web/json_api.php index e14dd99625..66054e6d33 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -509,7 +509,7 @@ function process_get_return($data, $object = true) { print(json_encode($getArgs)); $_SESSION['challenge'] = $WebAuthn->getChallenge(); return; - break; + break; case "fail2ban": if (!isset($_SESSION['mailcow_cc_role'])){ switch ($object) { @@ -2020,6 +2020,9 @@ function process_edit_return($return) { case "rl-mbox": process_edit_return(ratelimit('edit', 'mailbox', array_merge(array('object' => $items), $attr))); break; + case "rename-mbox": + process_edit_return(mailbox('edit', 'mailbox_rename', array_merge(array('mailbox' => $items), $attr))); + break; case "user-acl": process_edit_return(acl('edit', 'user', array_merge(array('username' => $items), $attr))); break; diff --git a/data/web/lang/lang.de-de.json b/data/web/lang/lang.de-de.json index 189774eecf..79a6dc3833 100644 --- a/data/web/lang/lang.de-de.json +++ b/data/web/lang/lang.de-de.json @@ -641,6 +641,10 @@ "mailbox": "Mailbox bearbeiten", "mailbox_quota_def": "Standard-Quota einer Mailbox", "mailbox_relayhost_info": "Wird auf eine Mailbox und direkte Alias-Adressen angewendet. Überschreibt die Einstellung einer Domain.", + "mailbox_rename": "Mailbox umbennnen", + "mailbox_rename_agree": "Ich habe ein Backup erstellt.", + "mailbox_rename_warning": "WICHTIG! Vor dem Umbenennen der Mailbox ein Backup erstellen.", + "mailbox_rename_alias": "Alias automatisch erstellen", "max_aliases": "Max. Aliasse", "max_mailboxes": "Max. Mailboxanzahl", "max_quota": "Max. Größe per Mailbox (MiB)", @@ -764,7 +768,7 @@ "login": "Anmelden", "mobileconfig_info": "Bitte als Mailbox-Benutzer einloggen, um das Verbindungsprofil herunterzuladen.", "new_password": "Neues Passwort", - "new_password_confirm": "Neues Passwort bestätigen", + "new_password_confirm": "Neues Passwort bestätigen", "other_logins": "Key Login", "password": "Passwort", "reset_password": "Passwort zurücksetzen", @@ -1084,6 +1088,7 @@ "mailbox_added": "Mailbox %s wurde angelegt", "mailbox_modified": "Änderungen an Mailbox %s wurden gespeichert", "mailbox_removed": "Mailbox %s wurde entfernt", + "mailbox_renamed": "Mailbox wurde von %s in %s umbenannt", "nginx_reloaded": "Nginx wurde neu geladen", "object_modified": "Änderungen an Objekt %s wurden gespeichert", "password_policy_saved": "Passwortrichtlinie wurde erfolgreich gespeichert", diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json index 600441809d..7833d7e43f 100644 --- a/data/web/lang/lang.en-gb.json +++ b/data/web/lang/lang.en-gb.json @@ -641,6 +641,10 @@ "mailbox": "Edit mailbox", "mailbox_quota_def": "Default mailbox quota", "mailbox_relayhost_info": "Applied to the mailbox and direct aliases only, does override a domain relayhost.", + "mailbox_rename": "Rename mailbox", + "mailbox_rename_agree": "I have created a backup.", + "mailbox_rename_warning": "IMPORTANT! Create a backup before renaming the mailbox.", + "mailbox_rename_alias": "Create alias automatically", "max_aliases": "Max. aliases", "max_mailboxes": "Max. possible mailboxes", "max_quota": "Max. quota per mailbox (MiB)", @@ -1091,6 +1095,7 @@ "mailbox_added": "Mailbox %s has been added", "mailbox_modified": "Changes to mailbox %s have been saved", "mailbox_removed": "Mailbox %s has been removed", + "mailbox_renamed": "Mailbox was renamed from %s to %s", "nginx_reloaded": "Nginx was reloaded", "object_modified": "Changes to object %s have been saved", "password_policy_saved": "Password policy was saved successfully", diff --git a/data/web/templates/edit/mailbox.twig b/data/web/templates/edit/mailbox.twig index 8de0095f28..b21ff2eecc 100644 --- a/data/web/templates/edit/mailbox.twig +++ b/data/web/templates/edit/mailbox.twig @@ -9,6 +9,7 @@ +
@@ -287,10 +288,10 @@
-
+
@@ -465,10 +466,10 @@ {% include 'mailbox/rl-frame.twig' %} - +
-
+

{{ lang.edit.mbox_rl_info }}

@@ -477,6 +478,55 @@
+
+
+
+ +
+
+
+
+

{{ lang.edit.mailbox_rename_warning }}

+
+
+
+ +
+
+
+
+
+ + + +
+
+
+ + @{{ result.domain }} +
+
+
+
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+
{% else %} diff --git a/docker-compose.yml b/docker-compose.yml index cf0a028ffb..86a0332364 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -534,7 +534,7 @@ services: - watchdog dockerapi-mailcow: - image: mailcow/dockerapi:2.08 + image: mailcow/dockerapi:2.09 security_opt: - label=disable restart: always @@ -552,7 +552,7 @@ services: aliases: - dockerapi - + ##### Will be removed soon ##### solr-mailcow: image: mailcow/solr:1.8.3 From be5a181be5b4717c080faf05e47965e8273ea9ea Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Thu, 22 Aug 2024 10:10:05 +0200 Subject: [PATCH 2/8] [Web][DockerApi] migrate imap acl on mbox rename --- data/Dockerfiles/dockerapi/main.py | 8 +- .../dockerapi/modules/DockerApi.py | 81 ++++++++++++++++++- data/web/inc/functions.mailbox.inc.php | 32 ++++++++ 3 files changed, 115 insertions(+), 6 deletions(-) diff --git a/data/Dockerfiles/dockerapi/main.py b/data/Dockerfiles/dockerapi/main.py index fca61bb020..6f7a6042cc 100644 --- a/data/Dockerfiles/dockerapi/main.py +++ b/data/Dockerfiles/dockerapi/main.py @@ -90,7 +90,7 @@ async def get_container(container_id : str): if container._id == container_id: container_info = await container.show() return Response(content=json.dumps(container_info, indent=4), media_type="application/json") - + res = { "type": "danger", "msg": "no container found" @@ -130,7 +130,7 @@ async def get_containers(): async def post_containers(container_id : str, post_action : str, request: Request): global dockerapi - try : + try: request_json = await request.json() except Exception as err: request_json = {} @@ -191,7 +191,7 @@ async def post_container_update_stats(container_id : str): stats = json.loads(await dockerapi.redis_client.get(container_id + '_stats')) return Response(content=json.dumps(stats, indent=4), media_type="application/json") - + # PubSub Handler async def handle_pubsub_messages(channel: aioredis.client.PubSub): @@ -244,7 +244,7 @@ async def handle_pubsub_messages(channel: aioredis.client.PubSub): dockerapi.logger.error("Unknwon PubSub recieved - %s" % json.dumps(data_json)) else: dockerapi.logger.error("Unknwon PubSub recieved - %s" % json.dumps(data_json)) - + await asyncio.sleep(0.0) except asyncio.TimeoutError: pass diff --git a/data/Dockerfiles/dockerapi/modules/DockerApi.py b/data/Dockerfiles/dockerapi/modules/DockerApi.py index 9fdd039e47..da0cc34ba5 100644 --- a/data/Dockerfiles/dockerapi/modules/DockerApi.py +++ b/data/Dockerfiles/dockerapi/modules/DockerApi.py @@ -353,14 +353,14 @@ def container_post__exec__maildir__move(self, request_json, **kwargs): for container in self.sync_docker_client.containers.list(filters=filters): vmail_name = request_json['old_maildir'].replace("'", "'\\''") new_vmail_name = request_json['new_maildir'].replace("'", "'\\''") - cmd_vmail = "if [[ -d '/var/vmail/" + vmail_name + "' ]]; then /bin/mv '/var/vmail/" + vmail_name + "' '/var/vmail/" + new_vmail_name + "'; fi" + cmd_vmail = f"if [[ -d '/var/vmail/{vmail_name}' ]]; then /bin/mv '/var/vmail/{vmail_name}' '/var/vmail/{new_vmail_name}'; fi" index_name = request_json['old_maildir'].split("/") new_index_name = request_json['new_maildir'].split("/") if len(index_name) > 1 and len(new_index_name) > 1: index_name = index_name[1].replace("'", "'\\''") + "@" + index_name[0].replace("'", "'\\''") new_index_name = new_index_name[1].replace("'", "'\\''") + "@" + new_index_name[0].replace("'", "'\\''") - cmd_vmail_index = "if [[ -d '/var/vmail_index/" + index_name + "' ]]; then /bin/mv '/var/vmail_index/" + index_name + "' '/var/vmail_index/" + new_index_name + "_index'; fi" + cmd_vmail_index = f"if [[ -d '/var/vmail_index/{index_name}' ]]; then /bin/mv '/var/vmail_index/{index_name}' '/var/vmail_index/{new_index_name}_index'; fi" cmd = ["/bin/bash", "-c", cmd_vmail + " && " + cmd_vmail_index] else: cmd = ["/bin/bash", "-c", cmd_vmail] @@ -412,6 +412,83 @@ def container_post__exec__sogo__rename_user(self, request_json, **kwargs): sogo_return = container.exec_run(['sogo-tool', 'rename-user', old_username, new_username], user='sogo') return self.exec_run_handler('generic', sogo_return) + # api call: container_post - post_action: exec - cmd: dovecot - task: get_acl + def container_post__exec__dovecot__get_acl(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + for container in self.sync_docker_client.containers.list(filters=filters): + vmail_name = request_json['maildir'].replace("'", "'\\''") + shared_folders = container.exec_run(["/bin/bash", "-c", f"find /var/vmail/{vmail_name}/Maildir/Shared -mindepth 2 -type d | sed 's:^/var/vmail/{vmail_name}/Maildir/Shared/::' | tr '/' '\\\\'"]) + shared_folders = shared_folders.output.decode('utf-8') + formatted_acls = [] + if shared_folders: + shared_folders = shared_folders.splitlines() + for shared_folder in shared_folders: + if "\\." not in shared_folder: + continue + shared_folder = shared_folder.split("\\.") + acls = container.exec_run(["/bin/bash", "-c", f"doveadm acl get -u {shared_folder[0]} {shared_folder[1]}"]) + acls = acls.output.decode('utf-8').strip().splitlines() + if len(acls) >= 2: + for acl in acls[1:]: + id, rights = acls[1].split(maxsplit=1) + id = id.replace("user=", "") + formatted_acls.append({ 'user': shared_folder[0], 'id': id, 'mailbox': shared_folder[1], 'rights': rights.split() }) + + return Response(content=json.dumps(formatted_acls, indent=4), media_type="application/json") + # api call: container_post - post_action: exec - cmd: dovecot - task: delete_acl + def container_post__exec__dovecot__delete_acl(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + for container in self.sync_docker_client.containers.list(filters=filters): + user = request_json['user'].replace("'", "'\\''") + mailbox = request_json['mailbox'].replace("'", "'\\''") + id = request_json['id'].replace("'", "'\\''") + + if user and mailbox and id: + acl_delete_return = container.exec_run(["/bin/bash", "-c", f'doveadm acl delete -u {user} {mailbox} "user={id}"']) + return self.exec_run_handler('generic', acl_delete_return) + # api call: container_post - post_action: exec - cmd: dovecot - task: set_acl + def container_post__exec__dovecot__set_acl(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + for container in self.sync_docker_client.containers.list(filters=filters): + user = request_json['user'].replace("'", "'\\''") + mailbox = request_json['mailbox'].replace("'", "'\\''") + id = request_json['id'].replace("'", "'\\''") + rights = "" + + available_rights = [ + "admin", + "create", + "delete", + "expunge", + "insert", + "lookup", + "post", + "read", + "write", + "write-deleted", + "write-seen" + ] + for right in request_json['rights']: + right = right.replace("'", "'\\''").lower() + if right in available_rights: + rights += right + " " + + if user and mailbox and id and rights: + acl_set_return = container.exec_run(["/bin/bash", "-c", f'doveadm acl set -u {user} {mailbox} "user={id}" {rights}']) + return self.exec_run_handler('generic', acl_set_return) + # Collect host stats async def get_host_stats(self, wait=5): diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 35e21f1c83..762f233ebe 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -3247,6 +3247,25 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { return false; } + // get imap acls + $exec_fields = array( + 'cmd' => 'dovecot', + 'task' => 'get_acl', + 'maildir' => $domain . '/' . $old_local_part, + ); + $imap_acls = json_decode(docker('post', 'dovecot-mailcow', 'exec', $exec_fields), true); + // delete imap acls + foreach ($imap_acls as $imap_acl) { + $exec_fields = array( + 'cmd' => 'dovecot', + 'task' => 'delete_acl', + 'user' => $imap_acl['user'], + 'mailbox' => $imap_acl['mailbox'], + 'id' => $imap_acl['id'] + ); + docker('post', 'dovecot-mailcow', 'exec', $exec_fields); + } + // rename username in sql try { $pdo->beginTransaction(); @@ -3326,6 +3345,19 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); docker('post', 'sogo-mailcow', 'exec', $exec_fields); + // set imap acls + foreach ($imap_acls as $imap_acl) { + $exec_fields = array( + 'cmd' => 'dovecot', + 'task' => 'set_acl', + 'user' => $imap_acl['user'], + 'mailbox' => $imap_acl['mailbox'], + 'id' => $new_username, + 'rights' => $imap_acl['rights'] + ); + docker('post', 'dovecot-mailcow', 'exec', $exec_fields); + } + // create alias if ($create_alias == 1) { mailbox("add", "alias", array( From 8e7b27aae469d5dbe757779cd11df6eb11430b8c Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Fri, 23 Aug 2024 09:30:23 +0200 Subject: [PATCH 3/8] [DockerApi] rework doveadm__get_acl function --- .../dockerapi/modules/DockerApi.py | 52 +++++++++++-------- data/web/inc/functions.mailbox.inc.php | 8 +-- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/data/Dockerfiles/dockerapi/modules/DockerApi.py b/data/Dockerfiles/dockerapi/modules/DockerApi.py index da0cc34ba5..909ac28727 100644 --- a/data/Dockerfiles/dockerapi/modules/DockerApi.py +++ b/data/Dockerfiles/dockerapi/modules/DockerApi.py @@ -412,35 +412,45 @@ def container_post__exec__sogo__rename_user(self, request_json, **kwargs): sogo_return = container.exec_run(['sogo-tool', 'rename-user', old_username, new_username], user='sogo') return self.exec_run_handler('generic', sogo_return) - # api call: container_post - post_action: exec - cmd: dovecot - task: get_acl - def container_post__exec__dovecot__get_acl(self, request_json, **kwargs): + # api call: container_post - post_action: exec - cmd: doveadm - task: get_acl + def container_post__exec__doveadm__get_acl(self, request_json, **kwargs): if 'container_id' in kwargs: filters = {"id": kwargs['container_id']} elif 'container_name' in kwargs: filters = {"name": kwargs['container_name']} for container in self.sync_docker_client.containers.list(filters=filters): - vmail_name = request_json['maildir'].replace("'", "'\\''") - shared_folders = container.exec_run(["/bin/bash", "-c", f"find /var/vmail/{vmail_name}/Maildir/Shared -mindepth 2 -type d | sed 's:^/var/vmail/{vmail_name}/Maildir/Shared/::' | tr '/' '\\\\'"]) + id = request_json['id'].replace("'", "'\\''") + + shared_folders = container.exec_run(["/bin/bash", "-c", f"doveadm mailbox list -u {id}"]) shared_folders = shared_folders.output.decode('utf-8') + shared_folders = shared_folders.splitlines() + formatted_acls = [] - if shared_folders: - shared_folders = shared_folders.splitlines() - for shared_folder in shared_folders: - if "\\." not in shared_folder: - continue - shared_folder = shared_folder.split("\\.") - acls = container.exec_run(["/bin/bash", "-c", f"doveadm acl get -u {shared_folder[0]} {shared_folder[1]}"]) - acls = acls.output.decode('utf-8').strip().splitlines() - if len(acls) >= 2: - for acl in acls[1:]: - id, rights = acls[1].split(maxsplit=1) - id = id.replace("user=", "") - formatted_acls.append({ 'user': shared_folder[0], 'id': id, 'mailbox': shared_folder[1], 'rights': rights.split() }) + mailbox_seen = [] + for shared_folder in shared_folders: + if "Shared" not in shared_folder and "/" not in shared_folder: + continue + shared_folder = shared_folder.split("/") + if len(shared_folder) < 3: + continue + + user = shared_folder[1] + mailbox = '/'.join(shared_folder[2:]) + if mailbox in mailbox_seen: + continue + + acls = container.exec_run(["/bin/bash", "-c", f"doveadm acl get -u {user} {mailbox}"]) + acls = acls.output.decode('utf-8').strip().splitlines() + if len(acls) >= 2: + for acl in acls[1:]: + _, rights = acls[1].split(maxsplit=1) + mailbox_seen.append(mailbox) + formatted_acls.append({ 'user': user, 'id': id, 'mailbox': mailbox, 'rights': rights.split() }) return Response(content=json.dumps(formatted_acls, indent=4), media_type="application/json") - # api call: container_post - post_action: exec - cmd: dovecot - task: delete_acl - def container_post__exec__dovecot__delete_acl(self, request_json, **kwargs): + # api call: container_post - post_action: exec - cmd: doveadm - task: delete_acl + def container_post__exec__doveadm__delete_acl(self, request_json, **kwargs): if 'container_id' in kwargs: filters = {"id": kwargs['container_id']} elif 'container_name' in kwargs: @@ -454,8 +464,8 @@ def container_post__exec__dovecot__delete_acl(self, request_json, **kwargs): if user and mailbox and id: acl_delete_return = container.exec_run(["/bin/bash", "-c", f'doveadm acl delete -u {user} {mailbox} "user={id}"']) return self.exec_run_handler('generic', acl_delete_return) - # api call: container_post - post_action: exec - cmd: dovecot - task: set_acl - def container_post__exec__dovecot__set_acl(self, request_json, **kwargs): + # api call: container_post - post_action: exec - cmd: doveadm - task: set_acl + def container_post__exec__doveadm__set_acl(self, request_json, **kwargs): if 'container_id' in kwargs: filters = {"id": kwargs['container_id']} elif 'container_name' in kwargs: diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 762f233ebe..baf635dda5 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -3249,15 +3249,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { // get imap acls $exec_fields = array( - 'cmd' => 'dovecot', + 'cmd' => 'doveadm', 'task' => 'get_acl', - 'maildir' => $domain . '/' . $old_local_part, + 'id' => $old_username ); $imap_acls = json_decode(docker('post', 'dovecot-mailcow', 'exec', $exec_fields), true); // delete imap acls foreach ($imap_acls as $imap_acl) { $exec_fields = array( - 'cmd' => 'dovecot', + 'cmd' => 'doveadm', 'task' => 'delete_acl', 'user' => $imap_acl['user'], 'mailbox' => $imap_acl['mailbox'], @@ -3348,7 +3348,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { // set imap acls foreach ($imap_acls as $imap_acl) { $exec_fields = array( - 'cmd' => 'dovecot', + 'cmd' => 'doveadm', 'task' => 'set_acl', 'user' => $imap_acl['user'], 'mailbox' => $imap_acl['mailbox'], From 822d9a7de6e72efbfecc882245d47629ba26cc9e Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Tue, 27 Aug 2024 10:07:07 +0200 Subject: [PATCH 4/8] [Web] rename goto in alias table --- data/web/inc/functions.mailbox.inc.php | 22 ++++++++++++---------- data/web/lang/lang.de-de.json | 1 + data/web/lang/lang.en-gb.json | 1 + data/web/templates/edit/mailbox.twig | 9 ++++++--- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index baf635dda5..6c24c13f58 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -3271,6 +3271,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $pdo->beginTransaction(); $pdo->exec('SET FOREIGN_KEY_CHECKS = 0'); + // Update username in mailbox table $pdo->prepare('UPDATE mailbox SET username = :new_username, local_part = :new_local_part WHERE username = :old_username') ->execute([ ':new_username' => $new_username, @@ -3278,6 +3279,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ':old_username' => $old_username ]); + $pdo->prepare("UPDATE alias SET address = :new_username, goto = :new_username2 WHERE address = :old_username") + ->execute([ + ':new_username' => $new_username, + ':new_username2' => $new_username, + ':old_username' => $old_username + ]); + // Update the username in all related tables $tables = [ 'tags_mailbox' => 'username', @@ -3287,9 +3295,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { 'da_acl' => 'username', 'quota2' => 'username', 'quota2replica' => 'username', - 'pushover' => 'username' + 'pushover' => 'username', + 'alias' => 'goto' ]; - foreach ($tables as $table => $column) { $pdo->prepare("UPDATE $table SET $column = :new_username WHERE $column = :old_username") ->execute([ @@ -3298,6 +3306,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ]); } + // 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([ ':new_username' => $new_username, @@ -3306,14 +3315,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ':old_username' => $old_username ]); - $pdo->prepare("UPDATE alias SET address = :new_username, goto = :new_username2 WHERE address = :old_username") - ->execute([ - ':new_username' => $new_username, - ':new_username2' => $new_username, - ':old_username' => $old_username - ]); - - // Re-enable foreign key checks $pdo->exec('SET FOREIGN_KEY_CHECKS = 1'); $pdo->commit(); @@ -3325,6 +3326,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => $e->getMessage() ); + return false; } // move maildir diff --git a/data/web/lang/lang.de-de.json b/data/web/lang/lang.de-de.json index 79a6dc3833..d22187c6ad 100644 --- a/data/web/lang/lang.de-de.json +++ b/data/web/lang/lang.de-de.json @@ -645,6 +645,7 @@ "mailbox_rename_agree": "Ich habe ein Backup erstellt.", "mailbox_rename_warning": "WICHTIG! Vor dem Umbenennen der Mailbox ein Backup erstellen.", "mailbox_rename_alias": "Alias automatisch erstellen", + "mailbox_rename_title": "Neuer Lokaler Mailbox Name", "max_aliases": "Max. Aliasse", "max_mailboxes": "Max. Mailboxanzahl", "max_quota": "Max. Größe per Mailbox (MiB)", diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json index 7833d7e43f..6e898099bb 100644 --- a/data/web/lang/lang.en-gb.json +++ b/data/web/lang/lang.en-gb.json @@ -645,6 +645,7 @@ "mailbox_rename_agree": "I have created a backup.", "mailbox_rename_warning": "IMPORTANT! Create a backup before renaming the mailbox.", "mailbox_rename_alias": "Create alias automatically", + "mailbox_rename_title": "New local mailbox name", "max_aliases": "Max. aliases", "max_mailboxes": "Max. possible mailboxes", "max_quota": "Max. quota per mailbox (MiB)", diff --git a/data/web/templates/edit/mailbox.twig b/data/web/templates/edit/mailbox.twig index b21ff2eecc..04be194c19 100644 --- a/data/web/templates/edit/mailbox.twig +++ b/data/web/templates/edit/mailbox.twig @@ -504,7 +504,10 @@
-
+
+ {{ lang.edit.mailbox_rename_title }} +
+
@{{ result.domain }} @@ -512,12 +515,12 @@
-
+
-
+
From d21c1bfa72b761507ff3ba47ff3454315356aa50 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Wed, 28 Aug 2024 10:48:44 +0200 Subject: [PATCH 5/8] [Web] add error handling for get_acl call --- data/web/inc/functions.mailbox.inc.php | 35 ++++++++++++++++---------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 6c24c13f58..9a1ae5777d 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -3248,22 +3248,31 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { } // get imap acls - $exec_fields = array( - 'cmd' => 'doveadm', - 'task' => 'get_acl', - 'id' => $old_username - ); - $imap_acls = json_decode(docker('post', 'dovecot-mailcow', 'exec', $exec_fields), true); - // delete imap acls - foreach ($imap_acls as $imap_acl) { + try { $exec_fields = array( 'cmd' => 'doveadm', - 'task' => 'delete_acl', - 'user' => $imap_acl['user'], - 'mailbox' => $imap_acl['mailbox'], - 'id' => $imap_acl['id'] + 'task' => 'get_acl', + 'id' => $old_username ); - docker('post', 'dovecot-mailcow', 'exec', $exec_fields); + $imap_acls = json_decode(docker('post', 'dovecot-mailcow', 'exec', $exec_fields), true); + // delete imap acls + foreach ($imap_acls as $imap_acl) { + $exec_fields = array( + 'cmd' => 'doveadm', + 'task' => 'delete_acl', + 'user' => $imap_acl['user'], + 'mailbox' => $imap_acl['mailbox'], + 'id' => $imap_acl['id'] + ); + docker('post', 'dovecot-mailcow', 'exec', $exec_fields); + } + } catch (Exception $e) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => $e->getMessage() + ); + return false; } // rename username in sql From 4f9e37c0c3f8543406d83b452e91b22298cb0550 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Wed, 28 Aug 2024 11:16:29 +0200 Subject: [PATCH 6/8] [Web] rename user in bcc_maps, recipient_maps and imapsync table --- data/web/inc/functions.mailbox.inc.php | 35 +++++++++++++++----------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 9a1ae5777d..9c01e78e1b 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -3297,22 +3297,27 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { // Update the username in all related tables $tables = [ - 'tags_mailbox' => 'username', - 'sieve_filters' => 'username', - 'app_passwd' => 'mailbox', - 'user_acl' => 'username', - 'da_acl' => 'username', - 'quota2' => 'username', - 'quota2replica' => 'username', - 'pushover' => 'username', - 'alias' => 'goto' + 'tags_mailbox' => ['username'], + 'sieve_filters' => ['username'], + 'app_passwd' => ['mailbox'], + 'user_acl' => ['username'], + 'da_acl' => ['username'], + 'quota2' => ['username'], + 'quota2replica' => ['username'], + 'pushover' => ['username'], + 'alias' => ['goto'], + "imapsync" => ['user2'], + 'bcc_maps' => ['local_dest', 'bcc_dest'], + 'recipient_maps' => ['old_dest', 'new_dest'] ]; - foreach ($tables as $table => $column) { - $pdo->prepare("UPDATE $table SET $column = :new_username WHERE $column = :old_username") - ->execute([ - ':new_username' => $new_username, - ':old_username' => $old_username - ]); + foreach ($tables as $table => $columns) { + foreach ($columns as $column) { + $stmt = $pdo->prepare("UPDATE $table SET $column = :new_username WHERE $column = :old_username") + ->execute([ + ':new_username' => $new_username, + ':old_username' => $old_username + ]); + } } // Update c_uid, c_name and mail in _sogo_static_view table From f2e35dff68c1763c94a1b05eb389ae3b0ec4406b Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Thu, 5 Sep 2024 12:40:30 +0200 Subject: [PATCH 7/8] [Web] rename user in sender_acl table --- data/web/inc/functions.mailbox.inc.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 9c01e78e1b..276e5629fa 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -3308,7 +3308,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { 'alias' => ['goto'], "imapsync" => ['user2'], 'bcc_maps' => ['local_dest', 'bcc_dest'], - 'recipient_maps' => ['old_dest', 'new_dest'] + 'recipient_maps' => ['old_dest', 'new_dest'], + 'sender_acl' => ['logged_in_as', 'send_as'] ]; foreach ($tables as $table => $columns) { foreach ($columns as $column) { From 1528e8766a0af49e3eb378b479d03c41ff8ae75f Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Fri, 6 Sep 2024 15:59:52 +0200 Subject: [PATCH 8/8] [DockerApi] correctly escape user input --- data/Dockerfiles/dockerapi/modules/DockerApi.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/data/Dockerfiles/dockerapi/modules/DockerApi.py b/data/Dockerfiles/dockerapi/modules/DockerApi.py index 909ac28727..64bcc4d956 100644 --- a/data/Dockerfiles/dockerapi/modules/DockerApi.py +++ b/data/Dockerfiles/dockerapi/modules/DockerApi.py @@ -410,7 +410,7 @@ def container_post__exec__sogo__rename_user(self, request_json, **kwargs): old_username = request_json['old_username'].replace("'", "'\\''") new_username = request_json['new_username'].replace("'", "'\\''") - sogo_return = container.exec_run(['sogo-tool', 'rename-user', old_username, new_username], user='sogo') + sogo_return = container.exec_run(["/bin/bash", "-c", f"sogo-tool rename-user '{old_username}' '{new_username}'"], user='sogo') return self.exec_run_handler('generic', sogo_return) # api call: container_post - post_action: exec - cmd: doveadm - task: get_acl def container_post__exec__doveadm__get_acl(self, request_json, **kwargs): @@ -422,7 +422,7 @@ def container_post__exec__doveadm__get_acl(self, request_json, **kwargs): for container in self.sync_docker_client.containers.list(filters=filters): id = request_json['id'].replace("'", "'\\''") - shared_folders = container.exec_run(["/bin/bash", "-c", f"doveadm mailbox list -u {id}"]) + shared_folders = container.exec_run(["/bin/bash", "-c", f"doveadm mailbox list -u '{id}'"]) shared_folders = shared_folders.output.decode('utf-8') shared_folders = shared_folders.splitlines() @@ -435,12 +435,12 @@ def container_post__exec__doveadm__get_acl(self, request_json, **kwargs): if len(shared_folder) < 3: continue - user = shared_folder[1] - mailbox = '/'.join(shared_folder[2:]) + user = shared_folder[1].replace("'", "'\\''") + mailbox = '/'.join(shared_folder[2:]).replace("'", "'\\''") if mailbox in mailbox_seen: continue - acls = container.exec_run(["/bin/bash", "-c", f"doveadm acl get -u {user} {mailbox}"]) + acls = container.exec_run(["/bin/bash", "-c", f"doveadm acl get -u '{user}' '{mailbox}'"]) acls = acls.output.decode('utf-8').strip().splitlines() if len(acls) >= 2: for acl in acls[1:]: @@ -462,7 +462,7 @@ def container_post__exec__doveadm__delete_acl(self, request_json, **kwargs): id = request_json['id'].replace("'", "'\\''") if user and mailbox and id: - acl_delete_return = container.exec_run(["/bin/bash", "-c", f'doveadm acl delete -u {user} {mailbox} "user={id}"']) + acl_delete_return = container.exec_run(["/bin/bash", "-c", f"doveadm acl delete -u '{user}' '{mailbox}' 'user={id}'"]) return self.exec_run_handler('generic', acl_delete_return) # api call: container_post - post_action: exec - cmd: doveadm - task: set_acl def container_post__exec__doveadm__set_acl(self, request_json, **kwargs): @@ -496,7 +496,7 @@ def container_post__exec__doveadm__set_acl(self, request_json, **kwargs): rights += right + " " if user and mailbox and id and rights: - acl_set_return = container.exec_run(["/bin/bash", "-c", f'doveadm acl set -u {user} {mailbox} "user={id}" {rights}']) + acl_set_return = container.exec_run(["/bin/bash", "-c", f"doveadm acl set -u '{user}' '{mailbox}' 'user={id}' {rights}"]) return self.exec_run_handler('generic', acl_set_return)