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 5601990916..64bcc4d956 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 = 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 = 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] + 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,107 @@ 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(["/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): + 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): + 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 = [] + 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].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 = 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: 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: + 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: 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: + 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): @@ -462,7 +587,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..276e5629fa 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,195 @@ 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; + } + + // get imap acls + try { + $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) { + $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 + try { + $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, + ':new_local_part' => $new_local_part, + ':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'], + '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'], + 'sender_acl' => ['logged_in_as', 'send_as'] + ]; + 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 + $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 + ]); + + // 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() + ); + return false; + } + + // 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); + + // set imap acls + foreach ($imap_acls as $imap_acl) { + $exec_fields = array( + 'cmd' => 'doveadm', + '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( + "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 +3424,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 +3448,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 +3471,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 +3486,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ":id" => $id , ":template" => $_data["template"] , ":attributes" => json_encode($attr) - )); + )); } @@ -3326,7 +3515,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 +3542,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 +3632,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 +3885,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 +3894,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 +4449,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 +4609,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 +4629,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 +4799,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 +5253,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 +5566,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); continue; } - + update_sogo_static_view($username); $_SESSION['return'][] = array( 'type' => 'success', @@ -5404,7 +5593,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 +5602,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ":id" => $id, ":type" => "mailbox", ":template" => "Default" - )); + )); } $_SESSION['return'][] = array( @@ -5487,7 +5676,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 +5689,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 +5706,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); return false; } - + foreach($tags as $tag){ // delete tag $wasModified = true; @@ -5572,7 +5761,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..807393d779 100644 --- a/data/web/lang/lang.de-de.json +++ b/data/web/lang/lang.de-de.json @@ -641,6 +641,11 @@ "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 umbenennen", + "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)", @@ -764,7 +769,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 +1089,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..6e898099bb 100644 --- a/data/web/lang/lang.en-gb.json +++ b/data/web/lang/lang.en-gb.json @@ -641,6 +641,11 @@ "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", + "mailbox_rename_title": "New local mailbox name", "max_aliases": "Max. aliases", "max_mailboxes": "Max. possible mailboxes", "max_quota": "Max. quota per mailbox (MiB)", @@ -1091,6 +1096,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..04be194c19 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,58 @@
+
+
+
+ +
+
+
+
+

{{ lang.edit.mailbox_rename_warning }}

+
+
+
+ +
+
+
+
+
+ + + +
+
+ {{ lang.edit.mailbox_rename_title }} +
+
+
+ + @{{ 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