Store and set alias receivers and senders separately for maximum control

This commit is contained in:
David Piggott 2015-07-04 16:31:11 +01:00
parent 3fdfad27cd
commit e6ff280984
6 changed files with 186 additions and 146 deletions

View file

@ -179,16 +179,15 @@ def mail_aliases():
if request.args.get("format", "") == "json": if request.args.get("format", "") == "json":
return json_response(get_mail_aliases_ex(env)) return json_response(get_mail_aliases_ex(env))
else: else:
return "".join(source+"\t"+destination+"\t"+applies_inbound+"\t"+applies_outbound+"\n" for source, destination, applies_inbound, applies_outbound in get_mail_aliases(env)) return "".join(address+"\t"+receivers+"\t"+senders+"\n" for address, receivers, senders in get_mail_aliases(env))
@app.route('/mail/aliases/add', methods=['POST']) @app.route('/mail/aliases/add', methods=['POST'])
@authorized_personnel_only @authorized_personnel_only
def mail_aliases_add(): def mail_aliases_add():
return add_mail_alias( return add_mail_alias(
request.form.get('source', ''), request.form.get('address', ''),
request.form.get('destination', ''), request.form.get('receivers', ''),
request.form.get('applies_inbound', '') == '1', request.form.get('senders', ''),
request.form.get('applies_outbound', '') == '1',
env, env,
update_if_exists=(request.form.get('update_if_exists', '') == '1') update_if_exists=(request.form.get('update_if_exists', '') == '1')
) )

View file

@ -181,13 +181,13 @@ def get_admins(env):
return users return users
def get_mail_aliases(env): def get_mail_aliases(env):
# Returns a sorted list of tuples of (alias, forward-to string, applies-to-inbound-mail, applies-to-outbound-mail). # Returns a sorted list of tuples of (address, forward-tos, permitted-senders).
c = open_database(env) c = open_database(env)
c.execute('SELECT source, destination, applies_inbound, applies_outbound FROM aliases') c.execute('SELECT address, receivers, senders FROM aliases')
aliases = { row[0]: row[1:4] for row in c.fetchall() } # make dict aliases = { row[0]: row[1:3] for row in c.fetchall() } # make dict
# put in a canonical order: sort by domain, then by email address lexicographically # put in a canonical order: sort by domain, then by email address lexicographically
aliases = [ (source,) + aliases[source] for source in utils.sort_email_addresses(aliases.keys(), env) ] aliases = [ (address,) + aliases[address] for address in utils.sort_email_addresses(aliases.keys(), env) ]
return aliases return aliases
def get_mail_aliases_ex(env): def get_mail_aliases_ex(env):
@ -199,11 +199,10 @@ def get_mail_aliases_ex(env):
# domain: "domain.tld", # domain: "domain.tld",
# alias: [ # alias: [
# { # {
# source: "name@domain.tld", # IDNA-encoded # address: "name@domain.tld", # IDNA-encoded
# source_display: "name@domain.tld", # full Unicode # address_display: "name@domain.tld", # full Unicode
# destination: ["target1@domain.com", "target2@domain.com", ...], # receivers: ["user1@domain.com", "receiver-only1@domain.com", ...],
# applies_inbound: True|False # senders: ["user1@domain.com", "sender-only1@domain.com", ...],
# applies_outbound: True|False
# required: True|False # required: True|False
# }, # },
# ... # ...
@ -214,10 +213,10 @@ def get_mail_aliases_ex(env):
required_aliases = get_required_aliases(env) required_aliases = get_required_aliases(env)
domains = {} domains = {}
for source, destination, applies_inbound, applies_outbound in get_mail_aliases(env): for address, receivers, senders in get_mail_aliases(env):
# get alias info # get alias info
domain = get_domain(source) domain = get_domain(address)
required = (source in required_aliases) required = (address in required_aliases)
# add to list # add to list
if not domain in domains: if not domain in domains:
@ -226,20 +225,19 @@ def get_mail_aliases_ex(env):
"aliases": [], "aliases": [],
} }
domains[domain]["aliases"].append({ domains[domain]["aliases"].append({
"source": source, "address": address,
"source_display": prettify_idn_email_address(source), "address_display": prettify_idn_email_address(address),
"destination": [prettify_idn_email_address(d.strip()) for d in destination.split(",")], "receivers": [prettify_idn_email_address(r.strip()) for r in receivers.split(",")],
"applies_inbound": True if applies_inbound == 1 else False, "senders": [prettify_idn_email_address(s.strip()) for s in senders.split(",")],
"applies_outbound": True if applies_outbound == 1 else False,
"required": required, "required": required,
}) })
# Sort domains. # Sort domains.
domains = [domains[domain] for domain in utils.sort_domains(domains.keys(), env)] domains = [domains[domain] for domain in utils.sort_domains(domains.keys(), env)]
# Sort aliases within each domain first by required-ness then lexicographically by source address. # Sort aliases within each domain first by required-ness then lexicographically by address.
for domain in domains: for domain in domains:
domain["aliases"].sort(key = lambda alias : (alias["required"], alias["source"])) domain["aliases"].sort(key = lambda alias : (alias["required"], alias["address"]))
return domains return domains
def get_domain(emailaddr, as_unicode=True): def get_domain(emailaddr, as_unicode=True):
@ -253,8 +251,8 @@ def get_mail_domains(env, filter_aliases=lambda alias : True):
# Returns the domain names (IDNA-encoded) of all of the email addresses # Returns the domain names (IDNA-encoded) of all of the email addresses
# configured on the system. # configured on the system.
return set( return set(
[get_domain(addr, as_unicode=False) for addr in get_mail_users(env)] [get_domain(login, as_unicode=False) for login in get_mail_users(env)]
+ [get_domain(source, as_unicode=False) for source, *_ in get_mail_aliases(env) if filter_aliases(source) ] + [get_domain(address, as_unicode=False) for address, *_ in get_mail_aliases(env) if filter_aliases(address) ]
) )
def add_mail_user(email, pw, privs, env): def add_mail_user(email, pw, privs, env):
@ -410,67 +408,82 @@ def add_remove_mail_user_privilege(email, priv, action, env):
return "OK" return "OK"
def add_mail_alias(source, destination, applies_inbound, applies_outbound, env, update_if_exists=False, do_kick=True): def add_mail_alias(address, receivers, senders, env, update_if_exists=False, do_kick=True):
# convert Unicode domain to IDNA # convert Unicode domain to IDNA
source = sanitize_idn_email_address(source) address = sanitize_idn_email_address(address)
# Our database is case sensitive (oops), which affects mail delivery # Our database is case sensitive (oops), which affects mail delivery
# (Postfix always queries in lowercase?), so force lowercase. # (Postfix always queries in lowercase?), so force lowercase.
source = source.lower() address = address.lower()
# validate source # validate address
source = source.strip() address = address.strip()
if source == "": if address == "":
return ("No incoming email address provided.", 400) return ("No email address provided.", 400)
if not validate_email(source, mode='alias'): if not validate_email(address, mode='alias'):
return ("Invalid incoming email address (%s)." % source, 400) return ("Invalid email address (%s)." % address, 400)
# validate receivers
validated_receivers = []
receivers = receivers.strip()
# extra checks for email addresses used in domain control validation # extra checks for email addresses used in domain control validation
is_dcv_source = is_dcv_address(source) is_dcv_source = is_dcv_address(address)
# validate destination
dests = []
destination = destination.strip()
# Postfix allows a single @domain.tld as the destination, which means # Postfix allows a single @domain.tld as the destination, which means
# the local part on the address is preserved in the rewrite. We must # the local part on the address is preserved in the rewrite. We must
# try to convert Unicode to IDNA first before validating that it's a # try to convert Unicode to IDNA first before validating that it's a
# legitimate alias address. Don't allow this sort of rewriting for # legitimate alias address. Don't allow this sort of rewriting for
# DCV source addresses. # DCV source addresses.
d1 = sanitize_idn_email_address(destination) r1 = sanitize_idn_email_address(receivers)
if validate_email(d1, mode='alias') and not is_dcv_source: if validate_email(r1, mode='alias') and not is_dcv_source:
dests.append(d1) validated_receivers.append(r1)
else: else:
# Parse comma and \n-separated destination emails & validate. In this # Parse comma and \n-separated destination emails & validate. In this
# case, the recipients must be complete email addresses. # case, the receivers must be complete email addresses.
for line in destination.split("\n"): for line in receivers.split("\n"):
for email in line.split(","): for email in line.split(","):
email = email.strip() email = email.strip()
if email == "": continue if email == "": continue
email = sanitize_idn_email_address(email) # Unicode => IDNA email = sanitize_idn_email_address(email) # Unicode => IDNA
if not validate_email(email): if not validate_email(email):
return ("Invalid destination email address (%s)." % email, 400) return ("Invalid receiver email address (%s)." % email, 400)
if is_dcv_source and not is_dcv_address(email) and "admin" not in get_mail_user_privileges(email, env, empty_on_error=True): if is_dcv_source and not is_dcv_address(email) and "admin" not in get_mail_user_privileges(email, env, empty_on_error=True):
# Make domain control validation hijacking a little harder to mess up by # Make domain control validation hijacking a little harder to mess up by
# requiring aliases for email addresses typically used in DCV to forward # requiring aliases for email addresses typically used in DCV to forward
# only to accounts that are administrators on this system. # only to accounts that are administrators on this system.
return ("This alias can only have administrators of this system as destinations because the address is frequently used for domain control validation.", 400) return ("This alias can only have administrators of this system as destinations because the address is frequently used for domain control validation.", 400)
dests.append(email) validated_receivers.append(email)
if len(destination) == 0: receivers = ",".join(validated_receivers)
return ("No destination email address(es) provided.", 400)
destination = ",".join(dests) valid_logins = get_mail_users(env)
# validate senders
validated_senders = []
senders = senders.strip()
# Parse comma and \n-separated sender logins & validate. The senders must be
# valid usernames.
for line in senders.split("\n"):
for login in line.split(","):
login = login.strip()
if login == "": continue
if login not in valid_logins:
return ("Invalid sender login (%s)." % login, 400)
validated_senders.append(login)
senders = ",".join(validated_senders)
# save to db # save to db
conn, c = open_database(env, with_connection=True) conn, c = open_database(env, with_connection=True)
try: try:
c.execute("INSERT INTO aliases (source, destination, applies_inbound, applies_outbound) VALUES (?, ?, ?, ?)", (source, destination, 1 if applies_inbound else 0, 1 if applies_outbound else 0)) c.execute("INSERT INTO aliases (address, receivers, senders) VALUES (?, ?, ?)", (address, receivers, senders))
return_status = "alias added" return_status = "alias added"
except sqlite3.IntegrityError: except sqlite3.IntegrityError:
if not update_if_exists: if not update_if_exists:
return ("Alias already exists (%s)." % source, 400) return ("Alias already exists (%s)." % address, 400)
else: else:
c.execute("UPDATE aliases SET destination = ?, applies_inbound = ?, applies_outbound = ? WHERE source = ?", (destination, 1 if applies_inbound else 0, 1 if applies_outbound else 0, source)) c.execute("UPDATE aliases SET receivers = ?, senders = ? WHERE address = ?", (receivers, senders, address))
return_status = "alias updated" return_status = "alias updated"
conn.commit() conn.commit()
@ -479,15 +492,15 @@ def add_mail_alias(source, destination, applies_inbound, applies_outbound, env,
# Update things in case any new domains are added. # Update things in case any new domains are added.
return kick(env, return_status) return kick(env, return_status)
def remove_mail_alias(source, env, do_kick=True): def remove_mail_alias(address, env, do_kick=True):
# convert Unicode domain to IDNA # convert Unicode domain to IDNA
source = sanitize_idn_email_address(source) address = sanitize_idn_email_address(address)
# remove # remove
conn, c = open_database(env, with_connection=True) conn, c = open_database(env, with_connection=True)
c.execute("DELETE FROM aliases WHERE source=?", (source,)) c.execute("DELETE FROM aliases WHERE address=?", (address,))
if c.rowcount != 1: if c.rowcount != 1:
return ("That's not an alias (%s)." % source, 400) return ("That's not an alias (%s)." % address, 400)
conn.commit() conn.commit()
if do_kick: if do_kick:
@ -539,34 +552,34 @@ def kick(env, mail_result=None):
existing_aliases = get_mail_aliases(env) existing_aliases = get_mail_aliases(env)
required_aliases = get_required_aliases(env) required_aliases = get_required_aliases(env)
def ensure_admin_alias_exists(source): def ensure_admin_alias_exists(address):
# If a user account exists with that address, we're good. # If a user account exists with that address, we're good.
if source in existing_users: if address in existing_users:
return return
# Does this alias exists? # Does this alias exists?
for s, *_ in existing_aliases: for a, *_ in existing_aliases:
if s == source: if a == address:
return return
# Doesn't exist. # Doesn't exist.
administrator = get_system_administrator(env) administrator = get_system_administrator(env)
if source == administrator: return # don't make an alias from the administrator to itself --- this alias must be created manually if address == administrator: return # don't make an alias from the administrator to itself --- this alias must be created manually
add_mail_alias(source, administrator, True, True, env, do_kick=False) add_mail_alias(address, administrator, "", env, do_kick=False)
results.append("added alias %s (=> %s)\n" % (source, administrator)) results.append("added alias %s (<==> %s)\n" % (address, administrator))
for alias in required_aliases: for address in required_aliases:
ensure_admin_alias_exists(alias) ensure_admin_alias_exists(address)
# Remove auto-generated postmaster/admin on domains we no # Remove auto-generated postmaster/admin on domains we no
# longer have any other email addresses for. # longer have any other email addresses for.
for source, target, *_ in existing_aliases: for address, receivers, *_ in existing_aliases:
user, domain = source.split("@") user, domain = address.split("@")
if user in ("postmaster", "admin") \ if user in ("postmaster", "admin") \
and source not in required_aliases \ and address not in required_aliases \
and target == get_system_administrator(env): and receivers == get_system_administrator(env):
remove_mail_alias(source, env, do_kick=False) remove_mail_alias(address, env, do_kick=False)
results.append("removed alias %s (was to %s; domain no longer used for email)\n" % (source, target)) results.append("removed alias %s (was to %s; domain no longer used for email)\n" % (address, receivers))
# Update DNS and nginx in case any domains are added/removed. # Update DNS and nginx in case any domains are added/removed.

View file

@ -351,14 +351,14 @@ def check_primary_hostname_dns(domain, env, output, dns_domains, dns_zonefiles):
check_alias_exists("Hostmaster contact address", "hostmaster@" + domain, env, output) check_alias_exists("Hostmaster contact address", "hostmaster@" + domain, env, output)
def check_alias_exists(alias_name, alias, env, output): def check_alias_exists(alias_name, alias, env, output):
mail_aliases = dict([(source, (destination, applies_inbound)) for source, destination, applies_inbound, *_ in get_mail_aliases(env)]) mail_aliases = dict([(address, receivers) for address, receivers, *_ in get_mail_aliases(env)])
if alias in mail_aliases: if alias in mail_aliases:
if mail_aliases[alias][1]: if mail_aliases[alias]:
output.print_ok("%s exists as an inbound mail alias. [%s%s]" % (alias_name, alias, mail_aliases[alias][0])) output.print_ok("%s exists as a mail alias. [%s%s]" % (alias_name, alias, mail_aliases[alias]))
else: else:
output.print_error("%s exists as a mail alias [%s%s] but is not enabled for inbound email." % (alias_name, alias, mail_aliases[alias][0])) output.print_error("""You must set the destination of the mail alias for %s to direct email to you or another administrator.""" % alias)
else: else:
output.print_error("""You must add an inbound mail alias for %s which directs email to you or another administrator.""" % alias) output.print_error("""You must add a mail alias for %s which directs email to you or another administrator.""" % alias)
def check_dns_zone(domain, env, output, dns_zonefiles): def check_dns_zone(domain, env, output, dns_zonefiles):
# If a DS record is set at the registrar, check DNSSEC first because it will affect the NS query. # If a DS record is set at the registrar, check DNSSEC first because it will affect the NS query.
@ -495,7 +495,7 @@ def check_mail_domain(domain, env, output):
# Check that the postmaster@ email address exists. Not required if the domain has a # Check that the postmaster@ email address exists. Not required if the domain has a
# catch-all address or domain alias. # catch-all address or domain alias.
if "@" + domain not in [source for source, *_ in get_mail_aliases(env)]: if "@" + domain not in [address for address, *_ in get_mail_aliases(env)]:
check_alias_exists("Postmaster contact address", "postmaster@" + domain, env, output) check_alias_exists("Postmaster contact address", "postmaster@" + domain, env, output)
# Stop if the domain is listed in the Spamhaus Domain Block List. # Stop if the domain is listed in the Spamhaus Domain Block List.

View file

@ -7,7 +7,7 @@
<h3>Add a mail alias</h3> <h3>Add a mail alias</h3>
<p>Aliases are email forwarders. An alias can forward email to a <a href="javascript:show_panel('users')">mail user</a> or to any email address.</p> <p>An alias can forward email to a <a href="javascript:show_panel('users')">mail user</a> or to any email address. You can separately grant permission to one or more users to send as an alias.</p>
<form class="form-horizontal" role="form" onsubmit="do_add_alias(); return false;"> <form class="form-horizontal" role="form" onsubmit="do_add_alias(); return false;">
<div class="form-group"> <div class="form-group">
@ -31,20 +31,15 @@
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="addaliasDirection" class="col-sm-1 control-label">Direction</label> <label for="addaliasReceivers" class="col-sm-1 control-label">Forwards To</label>
<div class="col-sm-10"> <div class="col-sm-10">
<select class="form-control" id="addaliasDirection"> <textarea class="form-control" rows="3" id="addaliasReceivers"></textarea>
<option value="disabled">Disabled</option>
<option value="outbound">Outbound only</option>
<option value="inbound">Inbound only</option>
<option value="bidirectional">Both</option>
</select>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="addaliasTargets" class="col-sm-1 control-label">Forward To</label> <label for="addaliasSenders" class="col-sm-1 control-label">Permitted Senders</label>
<div class="col-sm-10"> <div class="col-sm-10">
<textarea class="form-control" rows="3" id="addaliasTargets"></textarea> <textarea class="form-control" rows="3" id="addaliasSenders"></textarea>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -61,8 +56,8 @@
<tr> <tr>
<th></th> <th></th>
<th>Alias<br></th> <th>Alias<br></th>
<th>Direction</th>
<th>Forwards To</th> <th>Forwards To</th>
<th>Permitted Senders</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -83,8 +78,8 @@
</a> </a>
</td> </td>
<td class='email'> </td> <td class='email'> </td>
<td class='direction'> </td> <td class='receivers'> </td>
<td class='target'> </td> <td class='senders'> </td>
</tr> </tr>
</table> </table>
</div> </div>
@ -111,23 +106,12 @@ function show_aliases() {
n.attr('id', ''); n.attr('id', '');
if (alias.required) n.addClass('alias-required'); if (alias.required) n.addClass('alias-required');
n.attr('data-email', alias.source_display); // this is decoded from IDNA, but will get re-coded to IDNA on the backend n.attr('data-email', alias.address_display); // this is decoded from IDNA, but will get re-coded to IDNA on the backend
n.find('td.email').text(alias.source_display) n.find('td.email').text(alias.address_display)
if (!alias.applies_inbound && !alias.applies_outbound) { for (var j = 0; j < alias.receivers.length; j++)
n.find('td.direction').text('') n.find('td.receivers').append($("<div></div>").text(alias.receivers[j]))
n.attr('data-direction', 'disabled'); for (var j = 0; j < alias.senders.length; j++)
} else if (!alias.applies_inbound && alias.applies_outbound) { n.find('td.senders').append($("<div></div>").text(alias.senders[j]))
n.find('td.direction').text('↤')
n.attr('data-direction', 'outbound');
} else if (alias.applies_inbound && !alias.applies_outbound) {
n.find('td.direction').text('↦')
n.attr('data-direction', 'inbound');
} else if (alias.applies_inbound && alias.applies_outbound) {
n.find('td.direction').text('↮')
n.attr('data-direction', 'bidirectional');
}
for (var j = 0; j < alias.destination.length; j++)
n.find('td.target').append($("<div></div>").text(alias.destination[j]))
$('#alias_table tbody').append(n); $('#alias_table tbody').append(n);
} }
} }
@ -140,22 +124,22 @@ function show_aliases() {
if ($(this).attr('data-mode') == "regular") { if ($(this).attr('data-mode') == "regular") {
$('#addaliasEmail').attr('type', 'email'); $('#addaliasEmail').attr('type', 'email');
$('#addaliasEmail').attr('placeholder', 'incoming email address (e.g. you@yourdomain.com)'); $('#addaliasEmail').attr('placeholder', 'incoming email address (e.g. you@yourdomain.com)');
$('#addaliasDirection').val('bidirectional'); $('#addaliasReceivers').attr('placeholder', 'forward to these email addresses (one per line or separated by commas)');
$('#addaliasTargets').attr('placeholder', 'forward to these email addresses (one per line or separated by commas)'); $('#addaliasSenders').attr('placeholder', 'allow these users to send as this alias (one per line or separated by commas)');
$('#alias_mode_info').slideUp(); $('#alias_mode_info').slideUp();
} else if ($(this).attr('data-mode') == "catchall") { } else if ($(this).attr('data-mode') == "catchall") {
$('#addaliasEmail').attr('type', 'text'); $('#addaliasEmail').attr('type', 'text');
$('#addaliasEmail').attr('placeholder', 'incoming catch-all address (e.g. @yourdomain.com)'); $('#addaliasEmail').attr('placeholder', 'incoming catch-all address (e.g. @yourdomain.com)');
$('#addaliasDirection').val('outbound'); $('#addaliasReceivers').attr('placeholder', 'forward to these email addresses (one per line or separated by commas)');
$('#addaliasTargets').attr('placeholder', 'forward to these email addresses (one per line or separated by commas)'); $('#addaliasSenders').attr('placeholder', 'allow these users to send as any address on this domain (one per line or separated by commas)');
$('#alias_mode_info').slideDown(); $('#alias_mode_info').slideDown();
$('#alias_mode_info span').addClass('hidden'); $('#alias_mode_info span').addClass('hidden');
$('#alias_mode_info span.catchall').removeClass('hidden'); $('#alias_mode_info span.catchall').removeClass('hidden');
} else if ($(this).attr('data-mode') == "domainalias") { } else if ($(this).attr('data-mode') == "domainalias") {
$('#addaliasEmail').attr('type', 'text'); $('#addaliasEmail').attr('type', 'text');
$('#addaliasEmail').attr('placeholder', 'incoming domain (@yourdomain.com)'); $('#addaliasEmail').attr('placeholder', 'incoming domain (@yourdomain.com)');
$('#addaliasDirection').val('inbound'); $('#addaliasReceivers').attr('placeholder', 'forward to domain (@yourdomain.com)');
$('#addaliasTargets').attr('placeholder', 'forward to domain (@yourdomain.com)'); $('#addaliasSenders').attr('placeholder', 'allow these users to send as any address on this domain (one per line or separated by commas)');
$('#alias_mode_info').slideDown(); $('#alias_mode_info').slideDown();
$('#alias_mode_info span').addClass('hidden'); $('#alias_mode_info span').addClass('hidden');
$('#alias_mode_info span.domainalias').removeClass('hidden'); $('#alias_mode_info span.domainalias').removeClass('hidden');
@ -168,18 +152,17 @@ function show_aliases() {
var is_alias_add_update = false; var is_alias_add_update = false;
function do_add_alias() { function do_add_alias() {
var title = (!is_alias_add_update) ? "Add Alias" : "Update Alias"; var title = (!is_alias_add_update) ? "Add Alias" : "Update Alias";
var email = $("#addaliasEmail").val(); var form_address = $("#addaliasEmail").val();
var direction = $("#addaliasDirection").val(); var form_receivers = $("#addaliasReceivers").val();
var targets = $("#addaliasTargets").val(); var form_senders = $("#addaliasSenders").val();
api( api(
"/mail/aliases/add", "/mail/aliases/add",
"POST", "POST",
{ {
update_if_exists: is_alias_add_update ? '1' : '0', update_if_exists: is_alias_add_update ? '1' : '0',
source: email, address: form_address,
destination: targets, receivers: form_receivers,
applies_inbound: (direction == 'bidirectional' || direction == 'inbound') ? '1' : '0', senders: form_senders
applies_outbound: (direction == 'bidirectional' || direction == 'outbound') ? '1' : '0'
}, },
function(r) { function(r) {
// Responses are multiple lines of pre-formatted text. // Responses are multiple lines of pre-formatted text.
@ -196,13 +179,8 @@ function do_add_alias() {
function aliases_reset_form() { function aliases_reset_form() {
$("#addaliasEmail").prop('disabled', false); $("#addaliasEmail").prop('disabled', false);
$("#addaliasEmail").val('') $("#addaliasEmail").val('')
if ($('#alias_type_buttons button').attr('data-mode') == "regular") $("#addaliasReceivers").val('')
$('#addaliasDirection').val('bidirectional'); $("#addaliasSenders").val('')
else if ($('#alias_type_buttons button').attr('data-mode') == "catchall")
$('#alias_type_buttons').val('outbound');
else if ($('#addaliasDirection button').attr('data-mode') == "domainalias")
$('#addaliasDirection').val('inbound');
$("#addaliasTargets").val('')
$('#alias-cancel').addClass('hidden'); $('#alias-cancel').addClass('hidden');
$('#add-alias-button').text('Add Alias'); $('#add-alias-button').text('Add Alias');
is_alias_add_update = false; is_alias_add_update = false;
@ -210,12 +188,15 @@ function aliases_reset_form() {
function aliases_edit(elem) { function aliases_edit(elem) {
var email = $(elem).parents('tr').attr('data-email'); var email = $(elem).parents('tr').attr('data-email');
var targetdivs = $(elem).parents('tr').find('.target div'); var receiverdivs = $(elem).parents('tr').find('.receivers div');
var targets = ""; var senderdivs = $(elem).parents('tr').find('.senders div');
for (var i = 0; i < targetdivs.length; i++) var receivers = "";
targets += $(targetdivs[i]).text() + "\n"; for (var i = 0; i < receiverdivs.length; i++)
var direction = $(elem).parents('tr').attr('data-direction') receivers += $(receiverdivs[i]).text() + "\n";
if (email.charAt(0) == '@' && targets.charAt(0) == '@') var senders = "";
for (var i = 0; i < senderdivs.length; i++)
senders += $(senderdivs[i]).text() + "\n";
if (email.charAt(0) == '@' && receivers.charAt(0) == '@')
$('#alias_type_buttons button[data-mode="domainalias"]').click(); $('#alias_type_buttons button[data-mode="domainalias"]').click();
else if (email.charAt(0) == '@') else if (email.charAt(0) == '@')
$('#alias_type_buttons button[data-mode="catchall"]').click(); $('#alias_type_buttons button[data-mode="catchall"]').click();
@ -224,15 +205,15 @@ function aliases_edit(elem) {
$('#alias-cancel').removeClass('hidden'); $('#alias-cancel').removeClass('hidden');
$("#addaliasEmail").prop('disabled', true); $("#addaliasEmail").prop('disabled', true);
$("#addaliasEmail").val(email); $("#addaliasEmail").val(email);
$('#addaliasDirection').val(direction); $("#addaliasReceivers").val(receivers);
$("#addaliasTargets").val(targets); $("#addaliasSenders").val(senders);
$('#add-alias-button').text('Update'); $('#add-alias-button').text('Update');
$('body').animate({ scrollTop: 0 }) $('body').animate({ scrollTop: 0 })
is_alias_add_update = true; is_alias_add_update = true;
} }
function aliases_remove(elem) { function aliases_remove(elem) {
var email = $(elem).parents('tr').attr('data-email'); var row_address = $(elem).parents('tr').attr('data-email');
show_modal_confirm( show_modal_confirm(
"Remove Alias", "Remove Alias",
"Remove " + email + "?", "Remove " + email + "?",
@ -242,7 +223,7 @@ function aliases_remove(elem) {
"/mail/aliases/remove", "/mail/aliases/remove",
"POST", "POST",
{ {
source: email address: row_address
}, },
function(r) { function(r) {
// Responses are multiple lines of pre-formatted text. // Responses are multiple lines of pre-formatted text.

View file

@ -21,7 +21,7 @@ db_path=$STORAGE_ROOT/mail/users.sqlite
if [ ! -f $db_path ]; then if [ ! -f $db_path ]; then
echo Creating new user database: $db_path; 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, privileges TEXT NOT NULL DEFAULT '');" | 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, applies_inbound INTEGER NOT NULL DEFAULT 1, applies_outbound INTEGER NOT NULL DEFAULT 1);" | sqlite3 $db_path; echo "CREATE TABLE aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, address TEXT NOT NULL UNIQUE, receivers TEXT NOT NULL, senders TEXT NOT NULL);" | sqlite3 $db_path;
fi fi
# ### User Authentication # ### User Authentication
@ -82,7 +82,7 @@ tools/editconf.py /etc/postfix/main.cf \
# Matches from the users table take priority over (direct) aliases. # Matches from the users table take priority over (direct) aliases.
cat > /etc/postfix/sender-login-maps.cf << EOF; cat > /etc/postfix/sender-login-maps.cf << EOF;
dbpath=$db_path dbpath=$db_path
query = SELECT destination from (SELECT destination, 0 as priority FROM aliases WHERE source='%s' AND applies_outbound=1 UNION SELECT email as destination, 1 as priority FROM users WHERE email='%s') ORDER BY priority LIMIT 1; query = SELECT senders from (SELECT senders, 0 as priority FROM aliases WHERE address='%s' UNION SELECT email as senders, 1 as priority FROM users WHERE email='%s') ORDER BY priority LIMIT 1;
EOF EOF
# ### Destination Validation # ### Destination Validation
@ -98,7 +98,7 @@ tools/editconf.py /etc/postfix/main.cf \
# SQL statement to check if we handle mail for a domain, either for users or aliases. # SQL statement to check if we handle mail for a domain, either for users or aliases.
cat > /etc/postfix/virtual-mailbox-domains.cf << EOF; cat > /etc/postfix/virtual-mailbox-domains.cf << EOF;
dbpath=$db_path dbpath=$db_path
query = SELECT 1 FROM users WHERE email LIKE '%%@%s' UNION SELECT 1 FROM aliases WHERE source LIKE '%%@%s' AND applies_inbound=1 query = SELECT 1 FROM users WHERE email LIKE '%%@%s' UNION SELECT 1 FROM aliases WHERE address LIKE '%%@%s'
EOF EOF
# SQL statement to check if we handle mail for a user. # SQL statement to check if we handle mail for a user.
@ -129,7 +129,7 @@ EOF
# postfix's preference for aliases for whole email addresses. # postfix's preference for aliases for whole email addresses.
cat > /etc/postfix/virtual-alias-maps.cf << EOF; cat > /etc/postfix/virtual-alias-maps.cf << EOF;
dbpath=$db_path dbpath=$db_path
query = SELECT destination from (SELECT destination, 0 as priority FROM aliases WHERE source='%s' AND applies_inbound=1 UNION SELECT email as destination, 1 as priority FROM users WHERE email='%s') ORDER BY priority LIMIT 1; query = SELECT receivers from (SELECT receivers, 0 as priority FROM aliases WHERE address='%s' UNION SELECT email as receivers, 1 as priority FROM users WHERE email='%s') ORDER BY priority LIMIT 1;
EOF EOF
# Restart Services # Restart Services

View file

@ -102,9 +102,56 @@ def migration_8(env):
os.unlink(os.path.join(env['STORAGE_ROOT'], 'mail/dkim/mail.private')) os.unlink(os.path.join(env['STORAGE_ROOT'], 'mail/dkim/mail.private'))
def migration_9(env): def migration_9(env):
# Switch from storing alias ownership in one column (used for both
# directions) to two columns (one for determining inbound forward-tos and
# one for determining outbound permitted-senders). This was motivated by the
# addition of #427 ("Reject outgoing mail if FROM does not match Login") -
# which introduced the notion of outbound permitted-senders.
db = os.path.join(env["STORAGE_ROOT"], 'mail/users.sqlite') db = os.path.join(env["STORAGE_ROOT"], 'mail/users.sqlite')
shell("check_call", ["sqlite3", db, "ALTER TABLE aliases ADD COLUMN applies_inbound INTEGER NOT NULL DEFAULT 1"]) # Move the old aliases table to one side.
shell("check_call", ["sqlite3", db, "ALTER TABLE aliases ADD COLUMN applies_outbound INTEGER NOT NULL DEFAULT 1"]) shell("check_call", ["sqlite3", db, "ALTER TABLE aliases RENAME TO aliases_8"])
# Create the new aliases table, initially empty.
shell("check_call", ["sqlite3", db, "CREATE TABLE aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, address TEXT NOT NULL UNIQUE, receivers TEXT NOT NULL, senders TEXT NOT NULL)"])
import sqlite3
conn = sqlite3.connect(os.path.join(env["STORAGE_ROOT"], "mail/users.sqlite"))
c = conn.cursor()
c.execute('SELECT email FROM users')
valid_logins = [ row[0] for row in c.fetchall() ]
c = conn.cursor()
c.execute('SELECT source, destination FROM aliases_8')
aliases = { row[0]: row[1] for row in c.fetchall() }
# Populate the new aliases table. Forward-to addresses (receivers) is taken
# directly from the old destination column. Permitted-sender logins
# (senders) is made up of only those addresses in the old destination column
# that are valid logins, as other values are not relevant. Their presence
# would not do any harm, except that it would make the aliases UI confusing
# on upgraded boxes.
for source in aliases:
address = source
receivers = aliases[source]
validated_senders = []
for login in aliases[source].split(","):
login = login.strip()
if login == "": continue
if login in valid_logins:
validated_senders.append(login)
senders = ",".join(validated_senders)
c = conn.cursor()
c.execute("INSERT INTO aliases (address, receivers, senders) VALUES (?, ?, ?)", (address, receivers, senders))
# Save.
conn.commit()
# Delete the old aliases table.
shell("check_call", ["sqlite3", db, "DROP TABLE aliases_8"])
def get_current_migration(): def get_current_migration():
ver = 0 ver = 0