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":
|
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')
|
||||||
)
|
)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue