Merge pull request #6045 from mailcow/feat/rename-mbox

[Web][DockerApi] Add Feature to Rename Email Addresses
This commit is contained in:
FreddleSpl0it 2024-10-25 10:49:58 +02:00 committed by GitHub
commit 2d76ffc88c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 497 additions and 107 deletions

View File

@ -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

View File

@ -159,7 +159,7 @@ class DockerApi:
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 @@ class DockerApi:
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 @@ class DockerApi:
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 @@ class DockerApi:
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 @@ class DockerApi:
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"):

View File

@ -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;
}

View File

@ -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);
}
@ -4617,19 +4806,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;
}
@ -5071,7 +5260,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$ids = $_data['ids'];
}
foreach ($ids as $id) {
// delete template
$stmt = $pdo->prepare("DELETE FROM `templates`
@ -5384,7 +5573,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
);
continue;
}
update_sogo_static_view($username);
$_SESSION['return'][] = array(
'type' => 'success',
@ -5411,7 +5600,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$ids = $_data['ids'];
}
foreach ($ids as $id) {
// delete template
$stmt = $pdo->prepare("DELETE FROM `templates`
@ -5420,7 +5609,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
":id" => $id,
":type" => "mailbox",
":template" => "Default"
));
));
}
$_SESSION['return'][] = array(
@ -5494,7 +5683,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'];
@ -5507,7 +5696,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',
@ -5524,7 +5713,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
);
return false;
}
foreach($tags as $tag){
// delete tag
$wasModified = true;
@ -5579,7 +5768,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,

View File

@ -18,7 +18,7 @@ if (isset($_POST["pw_reset_request"]) && !empty($_POST['username'])) {
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 @@ if (isset($_POST["verify_tfa_login"])) {
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 (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
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']);

View File

@ -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];

View File

@ -509,7 +509,7 @@ if (isset($_GET['query'])) {
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 @@ if (isset($_GET['query'])) {
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;

View File

@ -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",

View File

@ -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",

View File

@ -9,6 +9,7 @@
<li role="presentation" class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#mpushover">{{ lang.edit.pushover }}</button></li>
<li role="presentation" class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#macl">{{ lang.edit.acl }}</button></li>
<li role="presentation" class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#mrl">{{ lang.edit.ratelimit }}</button></li>
<li role="presentation" class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#mrename">⚠️ {{ lang.edit.mailbox_rename }}</button></li>
</ul>
<hr class="d-none d-md-block">
<div class="tab-content">
@ -287,10 +288,10 @@
<div class="card mb-4">
<div class="card-header d-flex d-md-none fs-5">
<button class="btn flex-grow-1 text-start" data-bs-target="#collapse-tab-mattr" data-bs-toggle="collapse" aria-controls="collapse-tab-mattr">
{{ lang.edit.mailbox }} <span class="badge bg-info table-lines"></span>
{{ lang.edit.custom_attributes }} <span class="badge bg-info table-lines"></span>
</button>
</div>
<div id="collapse-tab-mattr" class="card-body collapse show" data-bs-parent="#mailbox-content">
<div id="collapse-tab-mattr" class="card-body collapse" data-bs-parent="#mailbox-content">
<form class="form-inline" data-id="mbox_attr" role="form" method="post">
<table class="table table-condensed" style="white-space: nowrap;" id="mbox_attr_table">
<tr>
@ -465,10 +466,10 @@
{% include 'mailbox/rl-frame.twig' %}
</select>
</div>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="offset-sm-2 col-sm-10">
<button class="btn btn-xs-lg d-block d-sm-inline btn-secondary" data-action="edit_selected" data-id="mboxratelimit" data-item="{{ mailbox }}" data-api-url='edit/rl-mbox' data-api-attr='{}' href="#">{{ lang.edit.save }}</button>
<p class="text-muted mt-2">{{ lang.edit.mbox_rl_info }}</p>
</div>
@ -477,6 +478,58 @@
</div>
</div>
</div>
<div id="mrename" class="tab-pane fade" role="tabpanel" aria-labelledby="mailbox-rename">
<div class="card mb-4">
<div class="card-header d-flex d-md-none fs-5">
<button class="btn flex-grow-1 text-start" data-bs-target="#collapse-tab-mrename" data-bs-toggle="collapse" aria-controls="collapse-tab-mrename">
⚠️ {{ lang.edit.mailbox_rename }}<span class="badge bg-info table-lines"></span>
</button>
</div>
<div id="collapse-tab-mrename" class="card-body collapse" data-bs-parent="#mailbox-content">
<div class="well">
<div id="rename_warning">
<p>{{ lang.edit.mailbox_rename_warning }}</p>
<div id="confirm_show_rspamd_global_filters">
<div class="row">
<div class="offset-sm-2 col-sm-10">
<label>
<input type="checkbox" class="form-check-input" id="show_mailbox_rename_form"> {{ lang.edit.mailbox_rename_agree }}
</label>
</div>
</div>
</div>
</div>
<div id="rename_form" class="d-none">
<form data-id="mboxrename" method="post">
<input name="domain" type="hidden" value="{{ result.domain }}">
<input name="old_local_part" type="hidden" value="{{ result.local_part }}">
<div class="row mb-2">
<div class="col-sm-12 col-md-2">
<span>{{ lang.edit.mailbox_rename_title }}</span>
</div>
<div class="col-sm-12 col-md-10 col-xl-8">
<div class="input-group mb-2">
<input type="text" class="form-control" name="new_local_part" autocomplete="off" value="{{ result.local_part }}">
<span class="input-group-text">@{{ result.domain }}</span>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-sm-12 offset-md-2 col-md-10 col-xl-8">
<label><input type="checkbox" class="form-check-input" value="1" name="create_alias" checked> {{ lang.edit.mailbox_rename_alias }}</label>
</div>
</div>
<div class="row mb-2">
<div class="col-sm-12 offset-md-2 col-md-10 col-xl-8">
<button class="btn btn-xs-lg d-block d-sm-inline btn-secondary" data-action="edit_selected" data-id="mboxrename" data-item="{{ mailbox }}" data-api-url='edit/rename-mbox' data-api-attr='{}' data-api-reload-location="/mailbox" href="#">{{ lang.edit.save }}</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% else %}

View File

@ -535,7 +535,7 @@ services:
- watchdog
dockerapi-mailcow:
image: mailcow/dockerapi:2.08
image: mailcow/dockerapi:2.09
security_opt:
- label=disable
restart: always