From 11e84d0d40e9db8a4ec2208823bcdbfee84c2028 Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Sat, 18 Sep 2021 19:23:31 -0400 Subject: [PATCH] Move automatically generated aliases to a separate database table They really should never have been conflated with the user-provided aliases. Update the postfix alias map to query the automatically generated aliases with lowest priority. --- management/daemon.py | 2 +- management/mailconfig.py | 58 +++++++++++++------------------ management/templates/aliases.html | 4 +-- setup/mail-users.sh | 3 +- setup/migrate.py | 5 +++ 5 files changed, 35 insertions(+), 37 deletions(-) diff --git a/management/daemon.py b/management/daemon.py index 280efec0..e8e679e4 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -235,7 +235,7 @@ def mail_aliases(): if request.args.get("format", "") == "json": return json_response(get_mail_aliases_ex(env)) else: - return "".join(address+"\t"+receivers+"\t"+(senders or "")+"\n" for address, receivers, senders in get_mail_aliases(env)) + return "".join(address+"\t"+receivers+"\t"+(senders or "")+"\n" for address, receivers, senders, auto in get_mail_aliases(env)) @app.route('/mail/aliases/add', methods=['POST']) @authorized_personnel_only diff --git a/management/mailconfig.py b/management/mailconfig.py index 47faad5f..04a4c8b4 100755 --- a/management/mailconfig.py +++ b/management/mailconfig.py @@ -186,9 +186,9 @@ def get_admins(env): return users def get_mail_aliases(env): - # Returns a sorted list of tuples of (address, forward-tos, permitted-senders). + # Returns a sorted list of tuples of (address, forward-tos, permitted-senders, auto). c = open_database(env) - c.execute('SELECT source, destination, permitted_senders FROM aliases') + c.execute('SELECT source, destination, permitted_senders, 0 as auto FROM aliases UNION SELECT source, destination, permitted_senders, 1 as auto FROM auto_aliases') aliases = { row[0]: row for row in c.fetchall() } # make dict # put in a canonical order: sort by domain, then by email address lexicographically @@ -208,7 +208,7 @@ def get_mail_aliases_ex(env): # address_display: "name@domain.tld", # full Unicode # forwards_to: ["user1@domain.com", "receiver-only1@domain.com", ...], # permitted_senders: ["user1@domain.com", "sender-only1@domain.com", ...] OR null, - # required: True|False + # auto: True|False # }, # ... # ] @@ -216,12 +216,10 @@ def get_mail_aliases_ex(env): # ... # ] - required_aliases = get_required_aliases(env) domains = {} - for address, forwards_to, permitted_senders in get_mail_aliases(env): + for address, forwards_to, permitted_senders, auto in get_mail_aliases(env): # get alias info domain = get_domain(address) - required = (address in required_aliases) # add to list if not domain in domains: @@ -234,7 +232,7 @@ def get_mail_aliases_ex(env): "address_display": prettify_idn_email_address(address), "forwards_to": [prettify_idn_email_address(r.strip()) for r in forwards_to.split(",")], "permitted_senders": [prettify_idn_email_address(s.strip()) for s in permitted_senders.split(",")] if permitted_senders is not None else None, - "required": required, + "auto": bool(auto), }) # Sort domains. @@ -242,7 +240,7 @@ def get_mail_aliases_ex(env): # Sort aliases within each domain first by required-ness then lexicographically by address. for domain in domains: - domain["aliases"].sort(key = lambda alias : (alias["required"], alias["address"])) + domain["aliases"].sort(key = lambda alias : (alias["auto"], alias["address"])) return domains def get_domain(emailaddr, as_unicode=True): @@ -512,6 +510,13 @@ def remove_mail_alias(address, env, do_kick=True): # Update things in case any domains are removed. return kick(env, "alias removed") +def add_auto_aliases(aliases, env): + conn, c = open_database(env, with_connection=True) + c.execute("DELETE FROM auto_aliases"); + for source, destination in aliases.items(): + c.execute("INSERT INTO auto_aliases (source, destination) VALUES (?, ?)", (source, destination)) + conn.commit() + def get_system_administrator(env): return "administrator@" + env['PRIMARY_HOSTNAME'] @@ -555,39 +560,26 @@ def kick(env, mail_result=None): if mail_result is not None: results.append(mail_result + "\n") - # Ensure every required alias exists. + auto_aliases = { } - existing_users = get_mail_users(env) - existing_alias_records = get_mail_aliases(env) - existing_aliases = set(a for a, *_ in existing_alias_records) # just first entry in tuple + # Mape required aliases to the administrator alias (which should be created manually). + administrator = get_system_administrator(env) required_aliases = get_required_aliases(env) + for alias in required_aliases: + if alias == administrator: continue # don't make an alias from the administrator to itself --- this alias must be created manually + auto_aliases[alias] = administrator - def ensure_admin_alias_exists(address): - # If a user account exists with that address, we're good. - if address in existing_users: - return - # If the alias already exists, we're good. - if address in existing_aliases: - return + add_auto_aliases(auto_aliases, env) - # Doesn't exist. - administrator = get_system_administrator(env) - if address == administrator: return # don't make an alias from the administrator to itself --- this alias must be created manually - add_mail_alias(address, administrator, "", env, do_kick=False) - if administrator not in existing_aliases: return # don't report the alias in output if the administrator alias isn't in yet -- this is a hack to supress confusing output on initial setup - results.append("added alias %s (=> %s)\n" % (address, administrator)) - - for address in required_aliases: - ensure_admin_alias_exists(address) - - # Remove auto-generated postmaster/admin on domains we no - # longer have any other email addresses for. - for address, forwards_to, *_ in existing_alias_records: + # Remove auto-generated postmaster/admin/abuse alises from the main aliases table. + # They are now stored in the auto_aliases table. + for address, forwards_to, permitted_senders, auto in get_mail_aliases(env): user, domain = address.split("@") if user in ("postmaster", "admin", "abuse") \ and address not in required_aliases \ - and forwards_to == get_system_administrator(env): + and forwards_to == get_system_administrator(env) \ + and not auto: remove_mail_alias(address, env, do_kick=False) results.append("removed alias %s (was to %s; domain no longer used for email)\n" % (address, forwards_to)) diff --git a/management/templates/aliases.html b/management/templates/aliases.html index 3d37c4fd..c2a141f7 100644 --- a/management/templates/aliases.html +++ b/management/templates/aliases.html @@ -1,6 +1,6 @@

Aliases

@@ -163,7 +163,7 @@ function show_aliases() { var n = $("#alias-template").clone(); n.attr('id', ''); - if (alias.required) n.addClass('alias-required'); + if (alias.auto) n.addClass('alias-auto'); n.attr('data-address', alias.address_display); // this is decoded from IDNA, but will get re-coded to IDNA on the backend n.find('td.address').text(alias.address_display) for (var j = 0; j < alias.forwards_to.length; j++) diff --git a/setup/mail-users.sh b/setup/mail-users.sh index 48b3ef32..efa5cacd 100755 --- a/setup/mail-users.sh +++ b/setup/mail-users.sh @@ -23,6 +23,7 @@ if [ ! -f $db_path ]; then 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, permitted_senders TEXT);" | sqlite3 $db_path; echo "CREATE TABLE mfa (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, type TEXT NOT NULL, secret TEXT NOT NULL, mru_token TEXT, label TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE);" | sqlite3 $db_path; + echo "CREATE TABLE auto_aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);" | sqlite3 $db_path; fi # ### User Authentication @@ -145,7 +146,7 @@ EOF # empty destination here so that other lower priority rules might match. cat > /etc/postfix/virtual-alias-maps.cf << EOF; dbpath=$db_path -query = SELECT destination from (SELECT destination, 0 as priority FROM aliases WHERE source='%s' AND destination<>'' UNION SELECT email as destination, 1 as priority FROM users WHERE email='%s') ORDER BY priority LIMIT 1; +query = SELECT destination from (SELECT destination, 0 as priority FROM aliases WHERE source='%s' AND destination<>'' UNION SELECT email as destination, 1 as priority FROM users WHERE email='%s' UNION SELECT destination, 2 as priority FROM auto_aliases WHERE source='%s' AND destination<>'') ORDER BY priority LIMIT 1; EOF # Restart Services diff --git a/setup/migrate.py b/setup/migrate.py index c52ac967..9065cf40 100755 --- a/setup/migrate.py +++ b/setup/migrate.py @@ -186,6 +186,11 @@ def migration_13(env): db = os.path.join(env["STORAGE_ROOT"], 'mail/users.sqlite') shell("check_call", ["sqlite3", db, "CREATE TABLE mfa (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, type TEXT NOT NULL, secret TEXT NOT NULL, mru_token TEXT, label TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE);"]) +def migration_14(env): + # Add the "auto_aliases" table. + db = os.path.join(env["STORAGE_ROOT"], 'mail/users.sqlite') + shell("check_call", ["sqlite3", db, "CREATE TABLE auto_aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);"]) + ########################################################### def get_current_migration():