Store and set alias receivers and senders separately for maximum control
This commit is contained in:
parent
3fdfad27cd
commit
e6ff280984
6 changed files with 186 additions and 146 deletions
|
@ -179,16 +179,15 @@ def mail_aliases():
|
|||
if request.args.get("format", "") == "json":
|
||||
return json_response(get_mail_aliases_ex(env))
|
||||
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'])
|
||||
@authorized_personnel_only
|
||||
def mail_aliases_add():
|
||||
return add_mail_alias(
|
||||
request.form.get('source', ''),
|
||||
request.form.get('destination', ''),
|
||||
request.form.get('applies_inbound', '') == '1',
|
||||
request.form.get('applies_outbound', '') == '1',
|
||||
request.form.get('address', ''),
|
||||
request.form.get('receivers', ''),
|
||||
request.form.get('senders', ''),
|
||||
env,
|
||||
update_if_exists=(request.form.get('update_if_exists', '') == '1')
|
||||
)
|
||||
|
|
|
@ -58,7 +58,7 @@ def sanitize_idn_email_address(email):
|
|||
except (ValueError, idna.IDNAError):
|
||||
# ValueError: String does not have a single @-sign, so it is not
|
||||
# a valid email address. IDNAError: Domain part is not IDNA-valid.
|
||||
# Validation is not this function's job, so return value unchanged.
|
||||
# Validation is not this function's job, so return value unchanged.
|
||||
# If there are non-ASCII characters it will be filtered out by
|
||||
# validate_email.
|
||||
return email
|
||||
|
@ -181,13 +181,13 @@ def get_admins(env):
|
|||
return users
|
||||
|
||||
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.execute('SELECT source, destination, applies_inbound, applies_outbound FROM aliases')
|
||||
aliases = { row[0]: row[1:4] for row in c.fetchall() } # make dict
|
||||
c.execute('SELECT address, receivers, senders FROM aliases')
|
||||
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
|
||||
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
|
||||
|
||||
def get_mail_aliases_ex(env):
|
||||
|
@ -199,11 +199,10 @@ def get_mail_aliases_ex(env):
|
|||
# domain: "domain.tld",
|
||||
# alias: [
|
||||
# {
|
||||
# source: "name@domain.tld", # IDNA-encoded
|
||||
# source_display: "name@domain.tld", # full Unicode
|
||||
# destination: ["target1@domain.com", "target2@domain.com", ...],
|
||||
# applies_inbound: True|False
|
||||
# applies_outbound: True|False
|
||||
# address: "name@domain.tld", # IDNA-encoded
|
||||
# address_display: "name@domain.tld", # full Unicode
|
||||
# receivers: ["user1@domain.com", "receiver-only1@domain.com", ...],
|
||||
# senders: ["user1@domain.com", "sender-only1@domain.com", ...],
|
||||
# required: True|False
|
||||
# },
|
||||
# ...
|
||||
|
@ -214,10 +213,10 @@ def get_mail_aliases_ex(env):
|
|||
|
||||
required_aliases = get_required_aliases(env)
|
||||
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
|
||||
domain = get_domain(source)
|
||||
required = (source in required_aliases)
|
||||
domain = get_domain(address)
|
||||
required = (address in required_aliases)
|
||||
|
||||
# add to list
|
||||
if not domain in domains:
|
||||
|
@ -226,20 +225,19 @@ def get_mail_aliases_ex(env):
|
|||
"aliases": [],
|
||||
}
|
||||
domains[domain]["aliases"].append({
|
||||
"source": source,
|
||||
"source_display": prettify_idn_email_address(source),
|
||||
"destination": [prettify_idn_email_address(d.strip()) for d in destination.split(",")],
|
||||
"applies_inbound": True if applies_inbound == 1 else False,
|
||||
"applies_outbound": True if applies_outbound == 1 else False,
|
||||
"address": address,
|
||||
"address_display": prettify_idn_email_address(address),
|
||||
"receivers": [prettify_idn_email_address(r.strip()) for r in receivers.split(",")],
|
||||
"senders": [prettify_idn_email_address(s.strip()) for s in senders.split(",")],
|
||||
"required": required,
|
||||
})
|
||||
|
||||
# Sort domains.
|
||||
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:
|
||||
domain["aliases"].sort(key = lambda alias : (alias["required"], alias["source"]))
|
||||
domain["aliases"].sort(key = lambda alias : (alias["required"], alias["address"]))
|
||||
return domains
|
||||
|
||||
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
|
||||
# configured on the system.
|
||||
return set(
|
||||
[get_domain(addr, as_unicode=False) for addr in get_mail_users(env)]
|
||||
+ [get_domain(source, as_unicode=False) for source, *_ in get_mail_aliases(env) if filter_aliases(source) ]
|
||||
[get_domain(login, as_unicode=False) for login in get_mail_users(env)]
|
||||
+ [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):
|
||||
|
@ -410,67 +408,82 @@ def add_remove_mail_user_privilege(email, priv, action, env):
|
|||
|
||||
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
|
||||
source = sanitize_idn_email_address(source)
|
||||
address = sanitize_idn_email_address(address)
|
||||
|
||||
# Our database is case sensitive (oops), which affects mail delivery
|
||||
# (Postfix always queries in lowercase?), so force lowercase.
|
||||
source = source.lower()
|
||||
address = address.lower()
|
||||
|
||||
# validate source
|
||||
source = source.strip()
|
||||
if source == "":
|
||||
return ("No incoming email address provided.", 400)
|
||||
if not validate_email(source, mode='alias'):
|
||||
return ("Invalid incoming email address (%s)." % source, 400)
|
||||
# validate address
|
||||
address = address.strip()
|
||||
if address == "":
|
||||
return ("No email address provided.", 400)
|
||||
if not validate_email(address, mode='alias'):
|
||||
return ("Invalid email address (%s)." % address, 400)
|
||||
|
||||
# validate receivers
|
||||
validated_receivers = []
|
||||
receivers = receivers.strip()
|
||||
|
||||
# extra checks for email addresses used in domain control validation
|
||||
is_dcv_source = is_dcv_address(source)
|
||||
|
||||
# validate destination
|
||||
dests = []
|
||||
destination = destination.strip()
|
||||
is_dcv_source = is_dcv_address(address)
|
||||
|
||||
# Postfix allows a single @domain.tld as the destination, which means
|
||||
# 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
|
||||
# legitimate alias address. Don't allow this sort of rewriting for
|
||||
# DCV source addresses.
|
||||
d1 = sanitize_idn_email_address(destination)
|
||||
if validate_email(d1, mode='alias') and not is_dcv_source:
|
||||
dests.append(d1)
|
||||
r1 = sanitize_idn_email_address(receivers)
|
||||
if validate_email(r1, mode='alias') and not is_dcv_source:
|
||||
validated_receivers.append(r1)
|
||||
|
||||
else:
|
||||
# Parse comma and \n-separated destination emails & validate. In this
|
||||
# case, the recipients must be complete email addresses.
|
||||
for line in destination.split("\n"):
|
||||
# case, the receivers must be complete email addresses.
|
||||
for line in receivers.split("\n"):
|
||||
for email in line.split(","):
|
||||
email = email.strip()
|
||||
if email == "": continue
|
||||
email = sanitize_idn_email_address(email) # Unicode => IDNA
|
||||
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):
|
||||
# Make domain control validation hijacking a little harder to mess up by
|
||||
# requiring aliases for email addresses typically used in DCV to forward
|
||||
# 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)
|
||||
dests.append(email)
|
||||
if len(destination) == 0:
|
||||
return ("No destination email address(es) provided.", 400)
|
||||
destination = ",".join(dests)
|
||||
validated_receivers.append(email)
|
||||
receivers = ",".join(validated_receivers)
|
||||
|
||||
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
|
||||
conn, c = open_database(env, with_connection=True)
|
||||
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"
|
||||
except sqlite3.IntegrityError:
|
||||
if not update_if_exists:
|
||||
return ("Alias already exists (%s)." % source, 400)
|
||||
return ("Alias already exists (%s)." % address, 400)
|
||||
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"
|
||||
|
||||
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.
|
||||
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
|
||||
source = sanitize_idn_email_address(source)
|
||||
address = sanitize_idn_email_address(address)
|
||||
|
||||
# remove
|
||||
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:
|
||||
return ("That's not an alias (%s)." % source, 400)
|
||||
return ("That's not an alias (%s)." % address, 400)
|
||||
conn.commit()
|
||||
|
||||
if do_kick:
|
||||
|
@ -539,34 +552,34 @@ def kick(env, mail_result=None):
|
|||
existing_aliases = get_mail_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 source in existing_users:
|
||||
if address in existing_users:
|
||||
return
|
||||
|
||||
# Does this alias exists?
|
||||
for s, *_ in existing_aliases:
|
||||
if s == source:
|
||||
for a, *_ in existing_aliases:
|
||||
if a == address:
|
||||
return
|
||||
|
||||
# Doesn't exist.
|
||||
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
|
||||
add_mail_alias(source, administrator, True, True, env, do_kick=False)
|
||||
results.append("added alias %s (=> %s)\n" % (source, administrator))
|
||||
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)
|
||||
results.append("added alias %s (<==> %s)\n" % (address, administrator))
|
||||
|
||||
for alias in required_aliases:
|
||||
ensure_admin_alias_exists(alias)
|
||||
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 source, target, *_ in existing_aliases:
|
||||
user, domain = source.split("@")
|
||||
for address, receivers, *_ in existing_aliases:
|
||||
user, domain = address.split("@")
|
||||
if user in ("postmaster", "admin") \
|
||||
and source not in required_aliases \
|
||||
and target == get_system_administrator(env):
|
||||
remove_mail_alias(source, env, do_kick=False)
|
||||
results.append("removed alias %s (was to %s; domain no longer used for email)\n" % (source, target))
|
||||
and address not in required_aliases \
|
||||
and receivers == get_system_administrator(env):
|
||||
remove_mail_alias(address, env, do_kick=False)
|
||||
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.
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
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 mail_aliases[alias][1]:
|
||||
output.print_ok("%s exists as an inbound mail alias. [%s ↦ %s]" % (alias_name, alias, mail_aliases[alias][0]))
|
||||
if mail_aliases[alias]:
|
||||
output.print_ok("%s exists as a mail alias. [%s ↦ %s]" % (alias_name, alias, mail_aliases[alias]))
|
||||
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:
|
||||
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):
|
||||
# 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
|
||||
# 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)
|
||||
|
||||
# Stop if the domain is listed in the Spamhaus Domain Block List.
|
||||
|
@ -647,7 +647,7 @@ def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring
|
|||
return "*." + idna.encode(dns_name[2:]).decode('ascii')
|
||||
else:
|
||||
return idna.encode(dns_name).decode('ascii')
|
||||
|
||||
|
||||
try:
|
||||
sans = cert.extensions.get_extension_for_oid(OID_SUBJECT_ALTERNATIVE_NAME).value.get_values_for_type(DNSName)
|
||||
for san in sans:
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
<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;">
|
||||
<div class="form-group">
|
||||
|
@ -31,20 +31,15 @@
|
|||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<select class="form-control" id="addaliasDirection">
|
||||
<option value="disabled">Disabled</option>
|
||||
<option value="outbound">Outbound only</option>
|
||||
<option value="inbound">Inbound only</option>
|
||||
<option value="bidirectional">Both</option>
|
||||
</select>
|
||||
<textarea class="form-control" rows="3" id="addaliasReceivers"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<textarea class="form-control" rows="3" id="addaliasTargets"></textarea>
|
||||
<textarea class="form-control" rows="3" id="addaliasSenders"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
@ -61,8 +56,8 @@
|
|||
<tr>
|
||||
<th></th>
|
||||
<th>Alias<br></th>
|
||||
<th>Direction</th>
|
||||
<th>Forwards To</th>
|
||||
<th>Permitted Senders</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -83,8 +78,8 @@
|
|||
</a>
|
||||
</td>
|
||||
<td class='email'> </td>
|
||||
<td class='direction'> </td>
|
||||
<td class='target'> </td>
|
||||
<td class='receivers'> </td>
|
||||
<td class='senders'> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -111,23 +106,12 @@ function show_aliases() {
|
|||
n.attr('id', '');
|
||||
|
||||
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.find('td.email').text(alias.source_display)
|
||||
if (!alias.applies_inbound && !alias.applies_outbound) {
|
||||
n.find('td.direction').text('')
|
||||
n.attr('data-direction', 'disabled');
|
||||
} else if (!alias.applies_inbound && alias.applies_outbound) {
|
||||
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]))
|
||||
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.address_display)
|
||||
for (var j = 0; j < alias.receivers.length; j++)
|
||||
n.find('td.receivers').append($("<div></div>").text(alias.receivers[j]))
|
||||
for (var j = 0; j < alias.senders.length; j++)
|
||||
n.find('td.senders').append($("<div></div>").text(alias.senders[j]))
|
||||
$('#alias_table tbody').append(n);
|
||||
}
|
||||
}
|
||||
|
@ -140,22 +124,22 @@ function show_aliases() {
|
|||
if ($(this).attr('data-mode') == "regular") {
|
||||
$('#addaliasEmail').attr('type', 'email');
|
||||
$('#addaliasEmail').attr('placeholder', 'incoming email address (e.g. you@yourdomain.com)');
|
||||
$('#addaliasDirection').val('bidirectional');
|
||||
$('#addaliasTargets').attr('placeholder', 'forward to these email addresses (one per line or separated by commas)');
|
||||
$('#addaliasReceivers').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();
|
||||
} else if ($(this).attr('data-mode') == "catchall") {
|
||||
$('#addaliasEmail').attr('type', 'text');
|
||||
$('#addaliasEmail').attr('placeholder', 'incoming catch-all address (e.g. @yourdomain.com)');
|
||||
$('#addaliasDirection').val('outbound');
|
||||
$('#addaliasTargets').attr('placeholder', 'forward to these email addresses (one per line or separated by commas)');
|
||||
$('#addaliasReceivers').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 span').addClass('hidden');
|
||||
$('#alias_mode_info span.catchall').removeClass('hidden');
|
||||
} else if ($(this).attr('data-mode') == "domainalias") {
|
||||
$('#addaliasEmail').attr('type', 'text');
|
||||
$('#addaliasEmail').attr('placeholder', 'incoming domain (@yourdomain.com)');
|
||||
$('#addaliasDirection').val('inbound');
|
||||
$('#addaliasTargets').attr('placeholder', 'forward to domain (@yourdomain.com)');
|
||||
$('#addaliasReceivers').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 span').addClass('hidden');
|
||||
$('#alias_mode_info span.domainalias').removeClass('hidden');
|
||||
|
@ -168,18 +152,17 @@ function show_aliases() {
|
|||
var is_alias_add_update = false;
|
||||
function do_add_alias() {
|
||||
var title = (!is_alias_add_update) ? "Add Alias" : "Update Alias";
|
||||
var email = $("#addaliasEmail").val();
|
||||
var direction = $("#addaliasDirection").val();
|
||||
var targets = $("#addaliasTargets").val();
|
||||
var form_address = $("#addaliasEmail").val();
|
||||
var form_receivers = $("#addaliasReceivers").val();
|
||||
var form_senders = $("#addaliasSenders").val();
|
||||
api(
|
||||
"/mail/aliases/add",
|
||||
"POST",
|
||||
{
|
||||
update_if_exists: is_alias_add_update ? '1' : '0',
|
||||
source: email,
|
||||
destination: targets,
|
||||
applies_inbound: (direction == 'bidirectional' || direction == 'inbound') ? '1' : '0',
|
||||
applies_outbound: (direction == 'bidirectional' || direction == 'outbound') ? '1' : '0'
|
||||
address: form_address,
|
||||
receivers: form_receivers,
|
||||
senders: form_senders
|
||||
},
|
||||
function(r) {
|
||||
// Responses are multiple lines of pre-formatted text.
|
||||
|
@ -196,13 +179,8 @@ function do_add_alias() {
|
|||
function aliases_reset_form() {
|
||||
$("#addaliasEmail").prop('disabled', false);
|
||||
$("#addaliasEmail").val('')
|
||||
if ($('#alias_type_buttons button').attr('data-mode') == "regular")
|
||||
$('#addaliasDirection').val('bidirectional');
|
||||
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('')
|
||||
$("#addaliasReceivers").val('')
|
||||
$("#addaliasSenders").val('')
|
||||
$('#alias-cancel').addClass('hidden');
|
||||
$('#add-alias-button').text('Add Alias');
|
||||
is_alias_add_update = false;
|
||||
|
@ -210,12 +188,15 @@ function aliases_reset_form() {
|
|||
|
||||
function aliases_edit(elem) {
|
||||
var email = $(elem).parents('tr').attr('data-email');
|
||||
var targetdivs = $(elem).parents('tr').find('.target div');
|
||||
var targets = "";
|
||||
for (var i = 0; i < targetdivs.length; i++)
|
||||
targets += $(targetdivs[i]).text() + "\n";
|
||||
var direction = $(elem).parents('tr').attr('data-direction')
|
||||
if (email.charAt(0) == '@' && targets.charAt(0) == '@')
|
||||
var receiverdivs = $(elem).parents('tr').find('.receivers div');
|
||||
var senderdivs = $(elem).parents('tr').find('.senders div');
|
||||
var receivers = "";
|
||||
for (var i = 0; i < receiverdivs.length; i++)
|
||||
receivers += $(receiverdivs[i]).text() + "\n";
|
||||
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();
|
||||
else if (email.charAt(0) == '@')
|
||||
$('#alias_type_buttons button[data-mode="catchall"]').click();
|
||||
|
@ -224,15 +205,15 @@ function aliases_edit(elem) {
|
|||
$('#alias-cancel').removeClass('hidden');
|
||||
$("#addaliasEmail").prop('disabled', true);
|
||||
$("#addaliasEmail").val(email);
|
||||
$('#addaliasDirection').val(direction);
|
||||
$("#addaliasTargets").val(targets);
|
||||
$("#addaliasReceivers").val(receivers);
|
||||
$("#addaliasSenders").val(senders);
|
||||
$('#add-alias-button').text('Update');
|
||||
$('body').animate({ scrollTop: 0 })
|
||||
is_alias_add_update = true;
|
||||
}
|
||||
|
||||
function aliases_remove(elem) {
|
||||
var email = $(elem).parents('tr').attr('data-email');
|
||||
var row_address = $(elem).parents('tr').attr('data-email');
|
||||
show_modal_confirm(
|
||||
"Remove Alias",
|
||||
"Remove " + email + "?",
|
||||
|
@ -242,7 +223,7 @@ function aliases_remove(elem) {
|
|||
"/mail/aliases/remove",
|
||||
"POST",
|
||||
{
|
||||
source: email
|
||||
address: row_address
|
||||
},
|
||||
function(r) {
|
||||
// Responses are multiple lines of pre-formatted text.
|
||||
|
|
|
@ -21,7 +21,7 @@ db_path=$STORAGE_ROOT/mail/users.sqlite
|
|||
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, 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
|
||||
|
||||
# ### User Authentication
|
||||
|
@ -82,7 +82,7 @@ tools/editconf.py /etc/postfix/main.cf \
|
|||
# Matches from the users table take priority over (direct) aliases.
|
||||
cat > /etc/postfix/sender-login-maps.cf << EOF;
|
||||
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
|
||||
|
||||
# ### 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.
|
||||
cat > /etc/postfix/virtual-mailbox-domains.cf << EOF;
|
||||
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
|
||||
|
||||
# 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.
|
||||
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 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
|
||||
|
||||
# Restart Services
|
||||
|
|
|
@ -102,9 +102,56 @@ def migration_8(env):
|
|||
os.unlink(os.path.join(env['STORAGE_ROOT'], 'mail/dkim/mail.private'))
|
||||
|
||||
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')
|
||||
shell("check_call", ["sqlite3", db, "ALTER TABLE aliases ADD COLUMN applies_inbound INTEGER NOT NULL DEFAULT 1"])
|
||||
shell("check_call", ["sqlite3", db, "ALTER TABLE aliases ADD COLUMN applies_outbound INTEGER NOT NULL DEFAULT 1"])
|
||||
# Move the old aliases table to one side.
|
||||
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():
|
||||
ver = 0
|
||||
|
|
Loading…
Reference in a new issue