diff --git a/management/daemon.py b/management/daemon.py index fe0787f..a6be433 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -1,12 +1,14 @@ #!/usr/bin/python3 -import os, os.path, re +import os, os.path, re, json -from flask import Flask, request, render_template, abort +from flask import Flask, request, render_template, abort, Response app = Flask(__name__) import auth, utils -from mailconfig import get_mail_users, add_mail_user, set_mail_password, remove_mail_user, get_mail_aliases, get_mail_domains, add_mail_alias, remove_mail_alias +from mailconfig import get_mail_users, 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 env = utils.load_environment() @@ -29,7 +31,11 @@ def index(): @app.route('/mail/users') def mail_users(): - return "".join(x+"\n" for x in get_mail_users(env)) + if request.args.get("format", "") == "json": + users = get_mail_users(env, as_json=True) + return Response(json.dumps(users), status=200, mimetype='application/json') + else: + return "".join(x+"\n" for x in get_mail_users(env)) @app.route('/mail/users/add', methods=['POST']) def mail_users_add(): @@ -43,6 +49,22 @@ def mail_users_password(): def mail_users_remove(): return remove_mail_user(request.form.get('email', ''), env) + +@app.route('/mail/users/privileges') +def mail_user_privs(): + privs = get_mail_user_privileges(request.args.get('email', ''), env) + if isinstance(privs, tuple): return privs # error + return "\n".join(privs) + +@app.route('/mail/users/privileges/add', methods=['POST']) +def mail_user_privs_add(): + return add_remove_mail_user_privilege(request.form.get('email', ''), request.form.get('privilege', ''), "add", env) + +@app.route('/mail/users/privileges/remove', methods=['POST']) +def mail_user_privs_remove(): + return add_remove_mail_user_privilege(request.form.get('email', ''), request.form.get('privilege', ''), "remove", env) + + @app.route('/mail/aliases') def mail_aliases(): return "".join(x+"\t"+y+"\n" for x, y in get_mail_aliases(env)) diff --git a/management/mailconfig.py b/management/mailconfig.py index b6fdb2d..b5b20a2 100755 --- a/management/mailconfig.py +++ b/management/mailconfig.py @@ -46,10 +46,16 @@ def open_database(env, with_connection=False): else: return conn, conn.cursor() -def get_mail_users(env): +def get_mail_users(env, as_json=False): c = open_database(env) - c.execute('SELECT email FROM users') - return [row[0] for row in c.fetchall()] + c.execute('SELECT email, privileges FROM users') + if not as_json: + return [row[0] for row in c.fetchall()] + else: + return [ + { "email": row[0], "privileges": parse_privs(row[1]) } + for row in c.fetchall() + ] def get_mail_aliases(env): c = open_database(env) @@ -122,6 +128,40 @@ def remove_mail_user(email, env): # Update things in case any domains are removed. return kick(env, "mail user removed") +def parse_privs(value): + return [p for p in value.split("\n") if p.strip() != ""] + +def get_mail_user_privileges(email, env): + c = open_database(env) + c.execute('SELECT privileges FROM users WHERE email=?', (email,)) + rows = c.fetchall() + if len(rows) != 1: + return ("That's not a user (%s)." % email, 400) + return parse_privs(rows[0][0]) + +def add_remove_mail_user_privilege(email, priv, action, env): + if "\n" in priv or priv.strip() == "": + return ("That's not a valid privilege (%s)." % priv, 400) + + privs = get_mail_user_privileges(email, env) + if isinstance(privs, tuple): return privs # error + + if action == "add": + if priv not in privs: + privs.append(priv) + elif action == "remove": + privs = [p for p in privs if p != priv] + else: + return ("Invalid action.", 400) + + conn, c = open_database(env, with_connection=True) + c.execute("UPDATE users SET privileges=? WHERE email=?", ("\n".join(privs), email)) + if c.rowcount != 1: + return ("Something went wrong.", 400) + conn.commit() + + return "OK" + def add_mail_alias(source, destination, env, do_kick=True): if not validate_email(source, mode='alias'): return ("Invalid email address.", 400) diff --git a/setup/mail-users.sh b/setup/mail-users.sh index d790559..6e0abcd 100755 --- a/setup/mail-users.sh +++ b/setup/mail-users.sh @@ -17,7 +17,7 @@ db_path=$STORAGE_ROOT/mail/users.sqlite # Create an empty database if it doesn't yet exist. if [ ! -f $db_path ]; then echo Creating new user database: $db_path; - echo "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, password TEXT NOT NULL, extra);" | sqlite3 $db_path; + echo "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, password TEXT NOT NULL, extra, privileges TEXT NOT NULL DEFAULT '');" | sqlite3 $db_path; echo "CREATE TABLE aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL);" | sqlite3 $db_path; fi diff --git a/setup/migrate.py b/setup/migrate.py index cce2081..87c915a 100755 --- a/setup/migrate.py +++ b/setup/migrate.py @@ -8,7 +8,7 @@ import sys, os, os.path, glob, re, shutil sys.path.insert(0, 'management') -from utils import load_environment, save_environment, safe_domain_name +from utils import load_environment, save_environment, shell def migration_1(env): # Re-arrange where we store SSL certificates. There was a typo also. @@ -51,6 +51,11 @@ def migration_3(env): # of the file will be handled by the main function. pass +def migration_4(env): + # Add a new column to the mail users table where we can store administrative privileges. + db = os.path.join(env["STORAGE_ROOT"], 'mail/users.sqlite') + shell("check_call", ["sqlite3", db, "ALTER TABLE users ADD privileges TEXT NOT NULL DEFAULT ''"]) + def get_current_migration(): ver = 0 while True: diff --git a/setup/start.sh b/setup/start.sh index ad97449..0b06c9e 100755 --- a/setup/start.sh +++ b/setup/start.sh @@ -302,17 +302,21 @@ if [ -z "`tools/mail.py user`" ]; then EMAIL_ADDR=me@$PRIMARY_HOSTNAME EMAIL_PW=1234 echo - echo "Creating a new mail account for $EMAIL_ADDR with password $EMAIL_PW." + echo "Creating a new administrative mail account for $EMAIL_ADDR with password $EMAIL_PW." echo fi else echo - echo "Okay. I'm about to set up $EMAIL_ADDR for you." + echo "Okay. I'm about to set up $EMAIL_ADDR for you. This account will also" + echo "have access to the box's control panel." fi # Create the user's mail account. This will ask for a password if none was given above. tools/mail.py user add $EMAIL_ADDR $EMAIL_PW + # Make it an admin. + hide_output tools/mail.py user make-admin $EMAIL_ADDR + # Create an alias to which we'll direct all automatically-created administrative aliases. tools/mail.py alias add administrator@$PRIMARY_HOSTNAME $EMAIL_ADDR fi diff --git a/tools/mail.py b/tools/mail.py index 31557df..3794884 100755 --- a/tools/mail.py +++ b/tools/mail.py @@ -1,8 +1,8 @@ #!/usr/bin/python3 -import sys, getpass, urllib.request, urllib.error +import sys, getpass, urllib.request, urllib.error, json -def mgmt(cmd, data=None): +def mgmt(cmd, data=None, is_json=False): mgmt_uri = 'http://localhost:10222' setup_key_auth(mgmt_uri) @@ -18,7 +18,9 @@ def mgmt(cmd, data=None): else: print(e, file=sys.stderr) sys.exit(1) - return response.read().decode('utf8') + resp = response.read().decode('utf8') + if is_json: resp = json.loads(resp) + return resp def read_password(): first = getpass.getpass('password: ') @@ -47,6 +49,8 @@ if len(sys.argv) < 2: print(" tools/mail.py user add user@domain.com [password]") print(" tools/mail.py user password user@domain.com [password]") print(" tools/mail.py user remove user@domain.com") + print(" tools/mail.py user make-admin user@domain.com") + print(" tools/mail.py user remove-admin user@domain.com") print(" tools/mail.py alias (lists aliases)") print(" tools/mail.py alias add incoming.name@domain.com sent.to@other.domain.com") print(" tools/mail.py alias remove incoming.name@domain.com") @@ -55,7 +59,13 @@ if len(sys.argv) < 2: print() elif sys.argv[1] == "user" and len(sys.argv) == 2: - print(mgmt("/mail/users")) + # 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: + 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: @@ -75,6 +85,13 @@ elif sys.argv[1] == "user" and sys.argv[2] in ("add", "password"): elif sys.argv[1] == "user" and sys.argv[2] == "remove" and len(sys.argv) == 4: print(mgmt("/mail/users/remove", { "email": sys.argv[3] })) +elif sys.argv[1] == "user" and sys.argv[2] in ("make-admin", "remove-admin") and len(sys.argv) == 4: + if sys.argv[2] == "make-admin": + action = "add" + else: + action = "remove" + print(mgmt("/mail/users/privileges/" + action, { "email": sys.argv[3], "privilege": "admin" })) + elif sys.argv[1] == "alias" and len(sys.argv) == 2: print(mgmt("/mail/aliases"))