Ver Fonte

in the admin, group users by domain, fixes 209

Joshua Tauberer há 10 anos atrás
pai
commit
990649af2d
5 ficheiros alterados com 144 adições e 83 exclusões
  1. 3 3
      management/daemon.py
  2. 93 39
      management/mailconfig.py
  3. 37 28
      management/templates/users.html
  4. 0 4
      management/utils.py
  5. 11 9
      tools/mail.py

+ 3 - 3
management/daemon.py

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

+ 93 - 39
management/mailconfig.py

@@ -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')
-
-	# 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) ]
-
-	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
-			 ]
-
-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
+	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(), [])
+				]
+		})
+
+	# 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),
+				})
+
+	# 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)
+
+	# 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):
 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)) ]

+ 37 - 28
management/templates/users.html

@@ -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();
-        n.attr('id', '');
+        var hdr = $("<tr><td><h4/></td></tr>");
+        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 = $("<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);
-        }
+          if (user.status == 'inactive') continue;
 
 
-        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);
-        }
+          var add_privs = ["admin"];
+
+          for (var j = 0; j < user.privileges.length; j++) {
+            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(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 = $("<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) {
-          n.find('.aliases').show();
-          for (var j = 0; j < r[i].aliases.length; j++) {
-            n.find('td.email .aliases').append($("<div/>").text(
-              r[i].aliases[j][0]
-              + (r[i].aliases[j][1].length > 0 ? " ⇐ " + r[i].aliases[j][1].join(", ") : "")
-              ))
+          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(", ") : "")
+                ))
+            }
           }
           }
         }
         }
       }
       }

+ 0 - 4
management/utils.py

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

+ 11 - 9
tools/mail.py

@@ -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:
-		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"):
 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:
-		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:
 elif sys.argv[1] == "alias" and len(sys.argv) == 2:
 	print(mgmt("/mail/aliases"))
 	print(mgmt("/mail/aliases"))