diff --git a/management/daemon.py b/management/daemon.py index 47287ce6..8cf52e1a 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -7,7 +7,7 @@ from functools import wraps from flask import Flask, request, render_template, abort, Response 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_aliases, get_mail_domains, add_mail_alias, remove_mail_alias @@ -71,7 +71,7 @@ def json_response(data): def index(): # Render the control panel. This route does not require user authentication # 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', hostname=env['PRIMARY_HOSTNAME'], storage_root=env['STORAGE_ROOT'], @@ -98,7 +98,7 @@ def me(): @authorized_personnel_only def mail_users(): 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: return "".join(x+"\n" for x in get_mail_users(env)) diff --git a/management/mailconfig.py b/management/mailconfig.py index 1c05f2bc..ea4a5cc2 100755 --- a/management/mailconfig.py +++ b/management/mailconfig.py @@ -46,45 +46,98 @@ def open_database(env, with_connection=False): else: 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.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 - users = { row[0]: row[1] for row in c.fetchall() } # make dict - users = [ (email, users[email]) for email in utils.sort_email_addresses(users.keys(), env) ] + # Add in archived accounts. + if with_archived: + 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: - return [email for email, privileges in users] - else: - aliases = get_mail_alias_map(env) - return [ - { - "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(), []) - ] - } - for email, privileges in users - ] + # Group by domain. + domains = { } + for user in users: + domain = get_domain(user["email"]) + if domain not in domains: + domains[domain] = { + "domain": domain, + "users": [] + } + domains[domain]["users"].append(user) -def get_archived_mail_users(env): - real_users = set(get_mail_users(env)) - root = os.path.join(env['STORAGE_ROOT'], 'mail/mailboxes') - ret = [] - for domain_enc in os.listdir(root): - for user_enc in os.listdir(os.path.join(root, domain_enc)): - email = utils.unsafe_domain_name(user_enc) + "@" + utils.unsafe_domain_name(domain_enc) - if email in real_users: continue - ret.append({ - "email": email, - "privileges": "", - "status": "inactive" - }) - return ret + # Sort domains. + domains = [domains[domain] for domain in utils.sort_domains(domains.keys(), env)] + + # Sort users within each domain first by status then lexicographically by email address. + for domain in domains: + domain["users"].sort(key = lambda user : (user["status"] != "active", user["email"])) + + return domains + +def get_admins(env): + # Returns a set of users with admin privileges. + users = set() + for domain in get_mail_users_ex(env): + for user in domain["users"]: + if "admin" in user["privileges"]: + users.add(user["email"]) + return users def get_mail_aliases(env, as_json=False): c = open_database(env) @@ -124,9 +177,10 @@ def evaluate_mail_alias_map(email, aliases, env): ret |= evaluate_mail_alias_map(alias, aliases, env) return ret +def get_domain(emailaddr): + return emailaddr.split('@', 1)[1] + def get_mail_domains(env, filter_aliases=lambda alias : True): - def get_domain(emailaddr): - return emailaddr.split('@', 1)[1] return set( [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)) ] diff --git a/management/templates/users.html b/management/templates/users.html index cc58c6c1..8b07af64 100644 --- a/management/templates/users.html +++ b/management/templates/users.html @@ -66,7 +66,7 @@ archive account -
To restore account, create a new account with this email address.
+
To restore account, create a new account with this email address. Or to permanently delete the mailbox, delete the directory on the machine.
@@ -86,39 +86,48 @@ function show_users() { function(r) { $('#user_table tbody').html(""); for (var i = 0; i < r.length; i++) { - var n = $("#user-template").clone(); - n.attr('id', ''); + var hdr = $("

"); + hdr.find('h4').text(r[i].domain); + $('#user_table tbody').append(hdr); - n.addClass("account_" + r[i].status); - n.attr('data-email', r[i].email); - n.find('td.email .address').text(r[i].email) - $('#user_table tbody').append(n); + for (var k = 0; k < r[i].users.length; k++) { + var user = r[i].users[k]; - 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++) { - var p = $(" (remove privilege) |"); - 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); - } + if (user.status == 'inactive') continue; - for (var j = 0; j < add_privs.length; j++) { - var p = $("make | "); - p.find('span.name').text(add_privs[j]); - n.find('.add-privs').append(p); - } + var add_privs = ["admin"]; - if (r[i].aliases && r[i].aliases.length > 0) { - n.find('.aliases').show(); - for (var j = 0; j < r[i].aliases.length; j++) { - n.find('td.email .aliases').append($("
").text( - r[i].aliases[j][0] - + (r[i].aliases[j][1].length > 0 ? " ⇐ " + r[i].aliases[j][1].join(", ") : "") - )) + for (var j = 0; j < user.privileges.length; j++) { + var p = $(" (remove privilege) |"); + p.find('span.name').text(user.privileges[j]); + n.find('.privs').append(p); + if (add_privs.indexOf(user.privileges[j]) >= 0) + add_privs.splice(add_privs.indexOf(user.privileges[j]), 1); + } + + for (var j = 0; j < add_privs.length; j++) { + var p = $("make | "); + 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($("
").text( + user.aliases[j][0] + + (user.aliases[j][1].length > 0 ? " ⇐ " + user.aliases[j][1].join(", ") : "") + )) + } } } } diff --git a/management/utils.py b/management/utils.py index ffe49734..f7bc1290 100644 --- a/management/utils.py +++ b/management/utils.py @@ -23,10 +23,6 @@ def safe_domain_name(name): import urllib.parse 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): # Put domain names in a nice sorted order. For web_update, PRIMARY_HOSTNAME # must appear first so it becomes the nginx default server. diff --git a/tools/mail.py b/tools/mail.py index 0fd89d2f..c22c0adc 100755 --- a/tools/mail.py +++ b/tools/mail.py @@ -68,12 +68,13 @@ if 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. users = mgmt("/mail/users?format=json", is_json=True) - for user in users: - if user['status'] == 'inactive': continue - print(user['email'], end='') - if "admin" in user['privileges']: - print("*", end='') - print() + for domain in users: + for user in domain["users"]: + if user['status'] == 'inactive': continue + print(user['email'], end='') + if "admin" in user['privileges']: + print("*", end='') + print() elif sys.argv[1] == "user" and sys.argv[2] in ("add", "password"): 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": # Dump a list of admin users. users = mgmt("/mail/users?format=json", is_json=True) - for user in users: - if "admin" in user['privileges']: - print(user['email']) + for domain in users: + for user in domain["users"]: + if "admin" in user['privileges']: + print(user['email']) elif sys.argv[1] == "alias" and len(sys.argv) == 2: print(mgmt("/mail/aliases"))