in the admin, group users by domain, fixes 209
This commit is contained in:
parent
6f4d29a410
commit
990649af2d
5 changed files with 141 additions and 80 deletions
|
@ -7,7 +7,7 @@ from functools import wraps
|
||||||
from flask import Flask, request, render_template, abort, Response
|
from flask import Flask, request, render_template, abort, Response
|
||||||
|
|
||||||
import auth, utils
|
import auth, utils
|
||||||
from mailconfig import get_mail_users, add_mail_user, set_mail_password, remove_mail_user, get_archived_mail_users
|
from mailconfig import get_mail_users_ex, get_admins, add_mail_user, set_mail_password, remove_mail_user
|
||||||
from mailconfig import get_mail_user_privileges, add_remove_mail_user_privilege
|
from mailconfig import get_mail_user_privileges, add_remove_mail_user_privilege
|
||||||
from mailconfig import get_mail_aliases, get_mail_domains, add_mail_alias, remove_mail_alias
|
from mailconfig import get_mail_aliases, get_mail_domains, add_mail_alias, remove_mail_alias
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@ def json_response(data):
|
||||||
def index():
|
def index():
|
||||||
# Render the control panel. This route does not require user authentication
|
# Render the control panel. This route does not require user authentication
|
||||||
# so it must be safe!
|
# so it must be safe!
|
||||||
no_admins_exist = (len([user for user in get_mail_users(env, as_json=True) if "admin" in user['privileges']]) == 0)
|
no_admins_exist = (len(get_admins(env)) == 0)
|
||||||
return render_template('index.html',
|
return render_template('index.html',
|
||||||
hostname=env['PRIMARY_HOSTNAME'],
|
hostname=env['PRIMARY_HOSTNAME'],
|
||||||
storage_root=env['STORAGE_ROOT'],
|
storage_root=env['STORAGE_ROOT'],
|
||||||
|
@ -98,7 +98,7 @@ def me():
|
||||||
@authorized_personnel_only
|
@authorized_personnel_only
|
||||||
def mail_users():
|
def mail_users():
|
||||||
if request.args.get("format", "") == "json":
|
if request.args.get("format", "") == "json":
|
||||||
return json_response(get_mail_users(env, as_json=True) + get_archived_mail_users(env))
|
return json_response(get_mail_users_ex(env, with_archived=True))
|
||||||
else:
|
else:
|
||||||
return "".join(x+"\n" for x in get_mail_users(env))
|
return "".join(x+"\n" for x in get_mail_users(env))
|
||||||
|
|
||||||
|
|
|
@ -46,45 +46,98 @@ def open_database(env, with_connection=False):
|
||||||
else:
|
else:
|
||||||
return conn, conn.cursor()
|
return conn, conn.cursor()
|
||||||
|
|
||||||
def get_mail_users(env, as_json=False):
|
def get_mail_users(env):
|
||||||
|
# Returns a flat, sorted list of all user accounts.
|
||||||
|
c = open_database(env)
|
||||||
|
c.execute('SELECT email FROM users')
|
||||||
|
users = [ row[0] for row in c.fetchall() ]
|
||||||
|
return utils.sort_email_addresses(users, env)
|
||||||
|
|
||||||
|
def get_mail_users_ex(env, with_archived=False):
|
||||||
|
# Returns a complex data structure of all user accounts, optionally
|
||||||
|
# including archived (status="inactive") accounts.
|
||||||
|
#
|
||||||
|
# [
|
||||||
|
# {
|
||||||
|
# domain: "domain.tld",
|
||||||
|
# users: [
|
||||||
|
# {
|
||||||
|
# email: "name@domain.tld",
|
||||||
|
# privileges: [ "priv1", "priv2", ... ],
|
||||||
|
# status: "active",
|
||||||
|
# aliases: [
|
||||||
|
# ("alias@domain.tld", ["indirect.alias@domain.tld", ...]),
|
||||||
|
# ...
|
||||||
|
# ]
|
||||||
|
# },
|
||||||
|
# ...
|
||||||
|
# ]
|
||||||
|
# },
|
||||||
|
# ...
|
||||||
|
# ]
|
||||||
|
|
||||||
|
# Pre-load all aliases.
|
||||||
|
aliases = get_mail_alias_map(env)
|
||||||
|
|
||||||
|
# Get users and their privileges.
|
||||||
|
users = []
|
||||||
|
active_accounts = set()
|
||||||
c = open_database(env)
|
c = open_database(env)
|
||||||
c.execute('SELECT email, privileges FROM users')
|
c.execute('SELECT email, privileges FROM users')
|
||||||
|
for email, privileges in c.fetchall():
|
||||||
|
active_accounts.add(email)
|
||||||
|
users.append({
|
||||||
|
"email": email,
|
||||||
|
"privileges": parse_privs(privileges),
|
||||||
|
"status": "active",
|
||||||
|
"aliases": [
|
||||||
|
(alias, sorted(evaluate_mail_alias_map(alias, aliases, env)))
|
||||||
|
for alias in aliases.get(email.lower(), [])
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
# turn into a list of tuples, but sorted by domain & email address
|
# Add in archived accounts.
|
||||||
users = { row[0]: row[1] for row in c.fetchall() } # make dict
|
if with_archived:
|
||||||
users = [ (email, users[email]) for email in utils.sort_email_addresses(users.keys(), env) ]
|
root = os.path.join(env['STORAGE_ROOT'], 'mail/mailboxes')
|
||||||
|
for domain in os.listdir(root):
|
||||||
|
for user in os.listdir(os.path.join(root, domain)):
|
||||||
|
email = user + "@" + domain
|
||||||
|
if email in active_accounts: continue
|
||||||
|
users.append({
|
||||||
|
"email": email,
|
||||||
|
"privileges": "",
|
||||||
|
"status": "inactive",
|
||||||
|
"mailbox": os.path.join(root, domain, user),
|
||||||
|
})
|
||||||
|
|
||||||
if not as_json:
|
# Group by domain.
|
||||||
return [email for email, privileges in users]
|
domains = { }
|
||||||
else:
|
for user in users:
|
||||||
aliases = get_mail_alias_map(env)
|
domain = get_domain(user["email"])
|
||||||
return [
|
if domain not in domains:
|
||||||
{
|
domains[domain] = {
|
||||||
"email": email,
|
"domain": domain,
|
||||||
"privileges": parse_privs(privileges),
|
"users": []
|
||||||
"status": "active",
|
}
|
||||||
"aliases": [
|
domains[domain]["users"].append(user)
|
||||||
(alias, sorted(evaluate_mail_alias_map(alias, aliases, env)))
|
|
||||||
for alias in aliases.get(email.lower(), [])
|
|
||||||
]
|
|
||||||
}
|
|
||||||
for email, privileges in users
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_archived_mail_users(env):
|
# Sort domains.
|
||||||
real_users = set(get_mail_users(env))
|
domains = [domains[domain] for domain in utils.sort_domains(domains.keys(), env)]
|
||||||
root = os.path.join(env['STORAGE_ROOT'], 'mail/mailboxes')
|
|
||||||
ret = []
|
# Sort users within each domain first by status then lexicographically by email address.
|
||||||
for domain_enc in os.listdir(root):
|
for domain in domains:
|
||||||
for user_enc in os.listdir(os.path.join(root, domain_enc)):
|
domain["users"].sort(key = lambda user : (user["status"] != "active", user["email"]))
|
||||||
email = utils.unsafe_domain_name(user_enc) + "@" + utils.unsafe_domain_name(domain_enc)
|
|
||||||
if email in real_users: continue
|
return domains
|
||||||
ret.append({
|
|
||||||
"email": email,
|
def get_admins(env):
|
||||||
"privileges": "",
|
# Returns a set of users with admin privileges.
|
||||||
"status": "inactive"
|
users = set()
|
||||||
})
|
for domain in get_mail_users_ex(env):
|
||||||
return ret
|
for user in domain["users"]:
|
||||||
|
if "admin" in user["privileges"]:
|
||||||
|
users.add(user["email"])
|
||||||
|
return users
|
||||||
|
|
||||||
def get_mail_aliases(env, as_json=False):
|
def get_mail_aliases(env, as_json=False):
|
||||||
c = open_database(env)
|
c = open_database(env)
|
||||||
|
@ -124,9 +177,10 @@ def evaluate_mail_alias_map(email, aliases, env):
|
||||||
ret |= evaluate_mail_alias_map(alias, aliases, env)
|
ret |= evaluate_mail_alias_map(alias, aliases, env)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
def get_domain(emailaddr):
|
||||||
|
return emailaddr.split('@', 1)[1]
|
||||||
|
|
||||||
def get_mail_domains(env, filter_aliases=lambda alias : True):
|
def get_mail_domains(env, filter_aliases=lambda alias : True):
|
||||||
def get_domain(emailaddr):
|
|
||||||
return emailaddr.split('@', 1)[1]
|
|
||||||
return set(
|
return set(
|
||||||
[get_domain(addr) for addr in get_mail_users(env)]
|
[get_domain(addr) for addr in get_mail_users(env)]
|
||||||
+ [get_domain(source) for source, target in get_mail_aliases(env) if filter_aliases((source, target)) ]
|
+ [get_domain(source) for source, target in get_mail_aliases(env) if filter_aliases((source, target)) ]
|
||||||
|
|
|
@ -66,7 +66,7 @@
|
||||||
archive account
|
archive account
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class='if_inactive' style='color: #888; font-size: 90%'>To restore account, create a new account with this email address.</div>
|
<div class='if_inactive restore_info' style='color: #888; font-size: 90%'>To restore account, create a new account with this email address. Or to permanently delete the mailbox, delete the directory <tt></tt> on the machine.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class='aliases' style='display: none'> </div>
|
<div class='aliases' style='display: none'> </div>
|
||||||
|
@ -86,39 +86,48 @@ function show_users() {
|
||||||
function(r) {
|
function(r) {
|
||||||
$('#user_table tbody').html("");
|
$('#user_table tbody').html("");
|
||||||
for (var i = 0; i < r.length; i++) {
|
for (var i = 0; i < r.length; i++) {
|
||||||
var n = $("#user-template").clone();
|
var hdr = $("<tr><td><h4/></td></tr>");
|
||||||
n.attr('id', '');
|
hdr.find('h4').text(r[i].domain);
|
||||||
|
$('#user_table tbody').append(hdr);
|
||||||
|
|
||||||
n.addClass("account_" + r[i].status);
|
for (var k = 0; k < r[i].users.length; k++) {
|
||||||
n.attr('data-email', r[i].email);
|
var user = r[i].users[k];
|
||||||
n.find('td.email .address').text(r[i].email)
|
|
||||||
$('#user_table tbody').append(n);
|
|
||||||
|
|
||||||
if (r[i].status == 'inactive') continue;
|
var n = $("#user-template").clone();
|
||||||
|
n.attr('id', '');
|
||||||
|
|
||||||
var add_privs = ["admin"];
|
n.addClass("account_" + user.status);
|
||||||
|
n.attr('data-email', user.email);
|
||||||
|
n.find('td.email .address').text(user.email)
|
||||||
|
$('#user_table tbody').append(n);
|
||||||
|
n.find('.restore_info tt').text(user.mailbox);
|
||||||
|
|
||||||
for (var j = 0; j < r[i].privileges.length; j++) {
|
if (user.status == 'inactive') continue;
|
||||||
var p = $("<span><b><span class='name'></span></b> (<a href='#' onclick='mod_priv(this, \"remove\"); return false;' title='Remove Privilege'>remove privilege</a>) |</span>");
|
|
||||||
p.find('span.name').text(r[i].privileges[j]);
|
|
||||||
n.find('.privs').append(p);
|
|
||||||
if (add_privs.indexOf(r[i].privileges[j]) >= 0)
|
|
||||||
add_privs.splice(add_privs.indexOf(r[i].privileges[j]), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var j = 0; j < add_privs.length; j++) {
|
var add_privs = ["admin"];
|
||||||
var p = $("<span><a href='#' onclick='mod_priv(this, \"add\"); return false;' title='Add Privilege'>make <span class='name'></span></a> | </span>");
|
|
||||||
p.find('span.name').text(add_privs[j]);
|
|
||||||
n.find('.add-privs').append(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (r[i].aliases && r[i].aliases.length > 0) {
|
for (var j = 0; j < user.privileges.length; j++) {
|
||||||
n.find('.aliases').show();
|
var p = $("<span><b><span class='name'></span></b> (<a href='#' onclick='mod_priv(this, \"remove\"); return false;' title='Remove Privilege'>remove privilege</a>) |</span>");
|
||||||
for (var j = 0; j < r[i].aliases.length; j++) {
|
p.find('span.name').text(user.privileges[j]);
|
||||||
n.find('td.email .aliases').append($("<div/>").text(
|
n.find('.privs').append(p);
|
||||||
r[i].aliases[j][0]
|
if (add_privs.indexOf(user.privileges[j]) >= 0)
|
||||||
+ (r[i].aliases[j][1].length > 0 ? " ⇐ " + r[i].aliases[j][1].join(", ") : "")
|
add_privs.splice(add_privs.indexOf(user.privileges[j]), 1);
|
||||||
))
|
}
|
||||||
|
|
||||||
|
for (var j = 0; j < add_privs.length; j++) {
|
||||||
|
var p = $("<span><a href='#' onclick='mod_priv(this, \"add\"); return false;' title='Add Privilege'>make <span class='name'></span></a> | </span>");
|
||||||
|
p.find('span.name').text(add_privs[j]);
|
||||||
|
n.find('.add-privs').append(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.aliases && user.aliases.length > 0) {
|
||||||
|
n.find('.aliases').show();
|
||||||
|
for (var j = 0; j < user.aliases.length; j++) {
|
||||||
|
n.find('td.email .aliases').append($("<div/>").text(
|
||||||
|
user.aliases[j][0]
|
||||||
|
+ (user.aliases[j][1].length > 0 ? " ⇐ " + user.aliases[j][1].join(", ") : "")
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,10 +23,6 @@ def safe_domain_name(name):
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
return urllib.parse.quote(name, safe='')
|
return urllib.parse.quote(name, safe='')
|
||||||
|
|
||||||
def unsafe_domain_name(name_encoded):
|
|
||||||
import urllib.parse
|
|
||||||
return urllib.parse.unquote(name_encoded)
|
|
||||||
|
|
||||||
def sort_domains(domain_names, env):
|
def sort_domains(domain_names, env):
|
||||||
# Put domain names in a nice sorted order. For web_update, PRIMARY_HOSTNAME
|
# Put domain names in a nice sorted order. For web_update, PRIMARY_HOSTNAME
|
||||||
# must appear first so it becomes the nginx default server.
|
# must appear first so it becomes the nginx default server.
|
||||||
|
|
|
@ -68,12 +68,13 @@ if len(sys.argv) < 2:
|
||||||
elif sys.argv[1] == "user" and len(sys.argv) == 2:
|
elif sys.argv[1] == "user" and len(sys.argv) == 2:
|
||||||
# Dump a list of users, one per line. Mark admins with an asterisk.
|
# Dump a list of users, one per line. Mark admins with an asterisk.
|
||||||
users = mgmt("/mail/users?format=json", is_json=True)
|
users = mgmt("/mail/users?format=json", is_json=True)
|
||||||
for user in users:
|
for domain in users:
|
||||||
if user['status'] == 'inactive': continue
|
for user in domain["users"]:
|
||||||
print(user['email'], end='')
|
if user['status'] == 'inactive': continue
|
||||||
if "admin" in user['privileges']:
|
print(user['email'], end='')
|
||||||
print("*", end='')
|
if "admin" in user['privileges']:
|
||||||
print()
|
print("*", end='')
|
||||||
|
print()
|
||||||
|
|
||||||
elif sys.argv[1] == "user" and sys.argv[2] in ("add", "password"):
|
elif sys.argv[1] == "user" and sys.argv[2] in ("add", "password"):
|
||||||
if len(sys.argv) < 5:
|
if len(sys.argv) < 5:
|
||||||
|
@ -103,9 +104,10 @@ elif sys.argv[1] == "user" and sys.argv[2] in ("make-admin", "remove-admin") and
|
||||||
elif sys.argv[1] == "user" and sys.argv[2] == "admins":
|
elif sys.argv[1] == "user" and sys.argv[2] == "admins":
|
||||||
# Dump a list of admin users.
|
# Dump a list of admin users.
|
||||||
users = mgmt("/mail/users?format=json", is_json=True)
|
users = mgmt("/mail/users?format=json", is_json=True)
|
||||||
for user in users:
|
for domain in users:
|
||||||
if "admin" in user['privileges']:
|
for user in domain["users"]:
|
||||||
print(user['email'])
|
if "admin" in user['privileges']:
|
||||||
|
print(user['email'])
|
||||||
|
|
||||||
elif sys.argv[1] == "alias" and len(sys.argv) == 2:
|
elif sys.argv[1] == "alias" and len(sys.argv) == 2:
|
||||||
print(mgmt("/mail/aliases"))
|
print(mgmt("/mail/aliases"))
|
||||||
|
|
Loading…
Reference in a new issue