various cleanup related to the new permitted_senders column for aliases
This commit is contained in:
parent
123ac4fd33
commit
5924d0fe0d
|
@ -179,15 +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(address+"\t"+receivers+"\t"+senders+"\n" for address, receivers, senders in get_mail_aliases(env))
|
return "".join(address+"\t"+receivers+"\t"+(senders or "")+"\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('address', ''),
|
request.form.get('address', ''),
|
||||||
request.form.get('receivers', ''),
|
request.form.get('forwards_to', ''),
|
||||||
request.form.get('senders', ''),
|
request.form.get('permitted_senders', ''),
|
||||||
env,
|
env,
|
||||||
update_if_exists=(request.form.get('update_if_exists', '') == '1')
|
update_if_exists=(request.form.get('update_if_exists', '') == '1')
|
||||||
)
|
)
|
||||||
|
|
|
@ -183,11 +183,11 @@ def get_admins(env):
|
||||||
def get_mail_aliases(env):
|
def get_mail_aliases(env):
|
||||||
# Returns a sorted list of tuples of (address, forward-tos, permitted-senders).
|
# Returns a sorted list of tuples of (address, forward-tos, permitted-senders).
|
||||||
c = open_database(env)
|
c = open_database(env)
|
||||||
c.execute('SELECT address, receivers, senders FROM aliases')
|
c.execute('SELECT source, destination, permitted_senders FROM aliases')
|
||||||
aliases = { row[0]: row[1:3] for row in c.fetchall() } # make dict
|
aliases = { row[0]: row 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 = [ (address,) + aliases[address] for address in utils.sort_email_addresses(aliases.keys(), env) ]
|
aliases = [ 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):
|
||||||
|
@ -201,8 +201,8 @@ def get_mail_aliases_ex(env):
|
||||||
# {
|
# {
|
||||||
# address: "name@domain.tld", # IDNA-encoded
|
# address: "name@domain.tld", # IDNA-encoded
|
||||||
# address_display: "name@domain.tld", # full Unicode
|
# address_display: "name@domain.tld", # full Unicode
|
||||||
# receivers: ["user1@domain.com", "receiver-only1@domain.com", ...],
|
# forwards_to: ["user1@domain.com", "receiver-only1@domain.com", ...],
|
||||||
# senders: ["user1@domain.com", "sender-only1@domain.com", ...],
|
# permitted_senders: ["user1@domain.com", "sender-only1@domain.com", ...] OR null,
|
||||||
# required: True|False
|
# required: True|False
|
||||||
# },
|
# },
|
||||||
# ...
|
# ...
|
||||||
|
@ -213,7 +213,7 @@ def get_mail_aliases_ex(env):
|
||||||
|
|
||||||
required_aliases = get_required_aliases(env)
|
required_aliases = get_required_aliases(env)
|
||||||
domains = {}
|
domains = {}
|
||||||
for address, receivers, senders in get_mail_aliases(env):
|
for address, forwards_to, permitted_senders in get_mail_aliases(env):
|
||||||
# get alias info
|
# get alias info
|
||||||
domain = get_domain(address)
|
domain = get_domain(address)
|
||||||
required = (address in required_aliases)
|
required = (address in required_aliases)
|
||||||
|
@ -227,8 +227,8 @@ def get_mail_aliases_ex(env):
|
||||||
domains[domain]["aliases"].append({
|
domains[domain]["aliases"].append({
|
||||||
"address": address,
|
"address": address,
|
||||||
"address_display": prettify_idn_email_address(address),
|
"address_display": prettify_idn_email_address(address),
|
||||||
"receivers": [prettify_idn_email_address(r.strip()) for r in receivers.split(",")],
|
"forwards_to": [prettify_idn_email_address(r.strip()) for r in forwards_to.split(",")],
|
||||||
"senders": [prettify_idn_email_address(s.strip()) for s in senders.split(",")],
|
"permitted_senders": [prettify_idn_email_address(s.strip()) for s in permitted_senders.split(",")] if permitted_senders is not None else None,
|
||||||
"required": required,
|
"required": required,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -408,7 +408,7 @@ def add_remove_mail_user_privilege(email, priv, action, env):
|
||||||
|
|
||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
def add_mail_alias(address, receivers, senders, env, update_if_exists=False, do_kick=True):
|
def add_mail_alias(address, forwards_to, permitted_senders, env, update_if_exists=False, do_kick=True):
|
||||||
# convert Unicode domain to IDNA
|
# convert Unicode domain to IDNA
|
||||||
address = sanitize_idn_email_address(address)
|
address = sanitize_idn_email_address(address)
|
||||||
|
|
||||||
|
@ -423,9 +423,9 @@ def add_mail_alias(address, receivers, senders, env, update_if_exists=False, do_
|
||||||
if not validate_email(address, mode='alias'):
|
if not validate_email(address, mode='alias'):
|
||||||
return ("Invalid email address (%s)." % address, 400)
|
return ("Invalid email address (%s)." % address, 400)
|
||||||
|
|
||||||
# validate receivers
|
# validate forwards_to
|
||||||
validated_receivers = []
|
validated_forwards_to = []
|
||||||
receivers = receivers.strip()
|
forwards_to = forwards_to.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(address)
|
is_dcv_source = is_dcv_address(address)
|
||||||
|
@ -435,14 +435,14 @@ def add_mail_alias(address, receivers, senders, env, update_if_exists=False, do_
|
||||||
# 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.
|
||||||
r1 = sanitize_idn_email_address(receivers)
|
r1 = sanitize_idn_email_address(forwards_to)
|
||||||
if validate_email(r1, mode='alias') and not is_dcv_source:
|
if validate_email(r1, mode='alias') and not is_dcv_source:
|
||||||
validated_receivers.append(r1)
|
validated_forwards_to.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 receivers must be complete email addresses.
|
# case, the forwards_to must be complete email addresses.
|
||||||
for line in receivers.split("\n"):
|
for line in forwards_to.split("\n"):
|
||||||
for email in line.split(","):
|
for email in line.split(","):
|
||||||
email = email.strip()
|
email = email.strip()
|
||||||
if email == "": continue
|
if email == "": continue
|
||||||
|
@ -454,36 +454,45 @@ def add_mail_alias(address, receivers, senders, env, update_if_exists=False, do_
|
||||||
# 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)
|
||||||
validated_receivers.append(email)
|
validated_forwards_to.append(email)
|
||||||
receivers = ",".join(validated_receivers)
|
|
||||||
|
|
||||||
|
# validate permitted_senders
|
||||||
valid_logins = get_mail_users(env)
|
valid_logins = get_mail_users(env)
|
||||||
|
validated_permitted_senders = []
|
||||||
|
permitted_senders = permitted_senders.strip()
|
||||||
|
|
||||||
# validate senders
|
# Parse comma and \n-separated sender logins & validate. The permitted_senders must be
|
||||||
validated_senders = []
|
|
||||||
senders = senders.strip()
|
|
||||||
|
|
||||||
# Parse comma and \n-separated sender logins & validate. The senders must be
|
|
||||||
# valid usernames.
|
# valid usernames.
|
||||||
for line in senders.split("\n"):
|
for line in permitted_senders.split("\n"):
|
||||||
for login in line.split(","):
|
for login in line.split(","):
|
||||||
login = login.strip()
|
login = login.strip()
|
||||||
if login == "": continue
|
if login == "": continue
|
||||||
if login not in valid_logins:
|
if login not in valid_logins:
|
||||||
return ("Invalid sender login (%s)." % login, 400)
|
return ("Invalid permitted sender: %s is not a user on this system." % login, 400)
|
||||||
validated_senders.append(login)
|
validated_permitted_senders.append(login)
|
||||||
senders = ",".join(validated_senders)
|
|
||||||
|
# Make sure the alias has either a forwards_to or a permitted_sender.
|
||||||
|
if len(validated_forwards_to) + len(validated_permitted_senders) == 0:
|
||||||
|
return ("The alias must either forward to an address or have a permitted sender.", 400)
|
||||||
|
|
||||||
# save to db
|
# save to db
|
||||||
|
|
||||||
|
forwards_to = ",".join(validated_forwards_to)
|
||||||
|
|
||||||
|
if len(validated_permitted_senders) == 0:
|
||||||
|
permitted_senders = None
|
||||||
|
else:
|
||||||
|
permitted_senders = ",".join(validated_permitted_senders)
|
||||||
|
|
||||||
conn, c = open_database(env, with_connection=True)
|
conn, c = open_database(env, with_connection=True)
|
||||||
try:
|
try:
|
||||||
c.execute("INSERT INTO aliases (address, receivers, senders) VALUES (?, ?, ?)", (address, receivers, senders))
|
c.execute("INSERT INTO aliases (source, destination, permitted_senders) VALUES (?, ?, ?)", (address, forwards_to, permitted_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)." % address, 400)
|
return ("Alias already exists (%s)." % address, 400)
|
||||||
else:
|
else:
|
||||||
c.execute("UPDATE aliases SET receivers = ?, senders = ? WHERE address = ?", (receivers, senders, address))
|
c.execute("UPDATE aliases SET destination = ?, permitted_senders = ? WHERE source = ?", (forwards_to, permitted_senders, address))
|
||||||
return_status = "alias updated"
|
return_status = "alias updated"
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
@ -498,7 +507,7 @@ def remove_mail_alias(address, env, do_kick=True):
|
||||||
|
|
||||||
# remove
|
# remove
|
||||||
conn, c = open_database(env, with_connection=True)
|
conn, c = open_database(env, with_connection=True)
|
||||||
c.execute("DELETE FROM aliases WHERE address=?", (address,))
|
c.execute("DELETE FROM aliases WHERE source=?", (address,))
|
||||||
if c.rowcount != 1:
|
if c.rowcount != 1:
|
||||||
return ("That's not an alias (%s)." % address, 400)
|
return ("That's not an alias (%s)." % address, 400)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
@ -573,13 +582,13 @@ def kick(env, mail_result=None):
|
||||||
|
|
||||||
# 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 address, receivers, *_ in existing_aliases:
|
for address, forwards_to, *_ in existing_aliases:
|
||||||
user, domain = address.split("@")
|
user, domain = address.split("@")
|
||||||
if user in ("postmaster", "admin") \
|
if user in ("postmaster", "admin") \
|
||||||
and address not in required_aliases \
|
and address not in required_aliases \
|
||||||
and receivers == get_system_administrator(env):
|
and forwards_to == get_system_administrator(env):
|
||||||
remove_mail_alias(address, 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" % (address, receivers))
|
results.append("removed alias %s (was to %s; domain no longer used for email)\n" % (address, forwards_to))
|
||||||
|
|
||||||
# Update DNS and nginx in case any domains are added/removed.
|
# Update DNS and nginx in case any domains are added/removed.
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,11 @@
|
||||||
|
|
||||||
<h3>Add a mail alias</h3>
|
<h3>Add a mail alias</h3>
|
||||||
|
|
||||||
<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>
|
<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>
|
||||||
|
|
||||||
<form class="form-horizontal" role="form" onsubmit="do_add_alias(); return false;">
|
<p>To use an alias or any address besides your own login username in outbound mail, the sending user must be included as a permitted sender for the alias.</p>
|
||||||
|
|
||||||
|
<form id="addalias-form" class="form-horizontal" role="form" onsubmit="do_add_alias(); return false;">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-offset-1 col-sm-11">
|
<div class="col-sm-offset-1 col-sm-11">
|
||||||
<div id="alias_type_buttons" class="btn-group btn-group-xs">
|
<div id="alias_type_buttons" class="btn-group btn-group-xs">
|
||||||
|
@ -18,8 +20,8 @@
|
||||||
<button type="button" class="btn btn-default" data-mode="domainalias">Domain Alias</button>
|
<button type="button" class="btn btn-default" data-mode="domainalias">Domain Alias</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="alias_mode_info" class="text-info small" style="display: none; margin: .5em 0 0 0;">
|
<div id="alias_mode_info" class="text-info small" style="display: none; margin: .5em 0 0 0;">
|
||||||
<span class="catchall hidden">A catch-all alias captures all otherwise unmatched email to a domain. Enter just a part of an email address starting with the @-sign.</span>
|
<span class="catchall hidden">A catch-all alias captures all otherwise unmatched email to a domain.</span>
|
||||||
<span class="domainalias hidden">A domain alias forwards all otherwise unmatched mail from one domain to another domain, preserving the part before the @-sign.</span>
|
<span class="domainalias hidden">A domain alias forwards all otherwise unmatched email from one domain to another domain, preserving the part before the @-sign.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -27,19 +29,39 @@
|
||||||
<label for="addaliasAddress" class="col-sm-1 control-label">Alias</label>
|
<label for="addaliasAddress" class="col-sm-1 control-label">Alias</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<input type="email" class="form-control" id="addaliasAddress">
|
<input type="email" class="form-control" id="addaliasAddress">
|
||||||
<div style="margin-top: 3px; padding-left: 3px; font-size: 90%" class="text-muted">You may use international (non-ASCII) characters for the domain part of the email address only.</div>
|
<div style="margin-top: 3px; padding-left: 3px; font-size: 90%" class="text-muted">
|
||||||
|
<span class="catchall domainalias">Enter just the part of an email address starting with the @-sign.</span>
|
||||||
|
You may use international (non-ASCII) characters for the domain part of the email address only.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="addaliasReceivers" class="col-sm-1 control-label">Forwards To</label>
|
<label for="addaliasForwardsTo" class="col-sm-1 control-label">Forwards To</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<textarea class="form-control" rows="3" id="addaliasReceivers"></textarea>
|
<textarea class="form-control" rows="3" id="addaliasForwardsTo"></textarea>
|
||||||
|
<div style="margin-top: 3px; padding-left: 3px; font-size: 90%" class="text-muted">
|
||||||
|
<span class="domainalias">Enter just the part of an email address starting with the @-sign.</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="addaliasSenders" class="col-sm-1 control-label">Permitted Senders</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="addaliasSenders"></textarea>
|
<div class="radio">
|
||||||
|
<label>
|
||||||
|
<input id="addaliasForwardsToNotAdvanced" name="addaliasForwardsToDivToggle" type="radio" checked onclick="$('#addaliasForwardsToDiv').toggle(false)">
|
||||||
|
Any mail user listed in the Fowards To box can send mail claiming to be from <span class="regularalias">the alias address</span><span class="catchall domainalias">any address on the alias domain</span>.
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="radio">
|
||||||
|
<label>
|
||||||
|
<input id="addaliasForwardsToAdvanced" name="addaliasForwardsToDivToggle" type="radio" id="addaliasForwardsToDivShower" onclick="$('#addaliasForwardsToDiv').toggle(true)">
|
||||||
|
I’ll enter the mail users that can send mail claiming to be from <span class="regularalias">the alias address</span><span class="catchall domainalias">any address on the alias domain</span>.
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div id="addaliasForwardsToDiv" style="margin-top: .5em; margin-left: 1.4em; display: none;">
|
||||||
|
<textarea class="form-control" rows="3" id="addaliasSenders" placeholder="one user per line or separated by commas"></textarea>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -78,7 +100,7 @@
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class='address'> </td>
|
<td class='address'> </td>
|
||||||
<td class='receivers'> </td>
|
<td class='forwardsTo'> </td>
|
||||||
<td class='senders'> </td>
|
<td class='senders'> </td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
@ -108,10 +130,10 @@ function show_aliases() {
|
||||||
if (alias.required) n.addClass('alias-required');
|
if (alias.required) n.addClass('alias-required');
|
||||||
n.attr('data-address', alias.address_display); // this is decoded from IDNA, but will get re-coded to IDNA on the backend
|
n.attr('data-address', alias.address_display); // this is decoded from IDNA, but will get re-coded to IDNA on the backend
|
||||||
n.find('td.address').text(alias.address_display)
|
n.find('td.address').text(alias.address_display)
|
||||||
for (var j = 0; j < alias.receivers.length; j++)
|
for (var j = 0; j < alias.forwards_to.length; j++)
|
||||||
n.find('td.receivers').append($("<div></div>").text(alias.receivers[j]))
|
n.find('td.forwardsTo').append($("<div></div>").text(alias.forwards_to[j]))
|
||||||
for (var j = 0; j < alias.senders.length; j++)
|
for (var j = 0; j < (alias.permitted_senders ? alias.permitted_senders.length : 0); j++)
|
||||||
n.find('td.senders').append($("<div></div>").text(alias.senders[j]))
|
n.find('td.senders').append($("<div></div>").text(alias.permitted_senders[j]))
|
||||||
$('#alias_table tbody').append(n);
|
$('#alias_table tbody').append(n);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,28 +143,25 @@ function show_aliases() {
|
||||||
$('#alias_type_buttons button').off('click').click(function() {
|
$('#alias_type_buttons button').off('click').click(function() {
|
||||||
$('#alias_type_buttons button').removeClass('active');
|
$('#alias_type_buttons button').removeClass('active');
|
||||||
$(this).addClass('active');
|
$(this).addClass('active');
|
||||||
|
$('#addalias-form .regularalias, #addalias-form .catchall, #addalias-form .domainalias').addClass('hidden');
|
||||||
if ($(this).attr('data-mode') == "regular") {
|
if ($(this).attr('data-mode') == "regular") {
|
||||||
$('#addaliasAddress').attr('type', 'email');
|
$('#addaliasAddress').attr('type', 'email');
|
||||||
$('#addaliasAddress').attr('placeholder', 'incoming email address (e.g. you@yourdomain.com)');
|
$('#addaliasAddress').attr('placeholder', 'you@yourdomain.com (incoming email address)');
|
||||||
$('#addaliasReceivers').attr('placeholder', 'forward to these email addresses (one per line or separated by commas)');
|
$('#addaliasForwardsTo').attr('placeholder', 'one address 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();
|
||||||
|
$('#addalias-form .regularalias').removeClass('hidden');
|
||||||
} else if ($(this).attr('data-mode') == "catchall") {
|
} else if ($(this).attr('data-mode') == "catchall") {
|
||||||
$('#addaliasAddress').attr('type', 'text');
|
$('#addaliasAddress').attr('type', 'text');
|
||||||
$('#addaliasAddress').attr('placeholder', 'incoming catch-all address (e.g. @yourdomain.com)');
|
$('#addaliasAddress').attr('placeholder', '@yourdomain.com (incoming catch-all domain)');
|
||||||
$('#addaliasReceivers').attr('placeholder', 'forward to these email addresses (one per line or separated by commas)');
|
$('#addaliasForwardsTo').attr('placeholder', 'one address 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');
|
$('#addalias-form .catchall').removeClass('hidden');
|
||||||
$('#alias_mode_info span.catchall').removeClass('hidden');
|
|
||||||
} else if ($(this).attr('data-mode') == "domainalias") {
|
} else if ($(this).attr('data-mode') == "domainalias") {
|
||||||
$('#addaliasAddress').attr('type', 'text');
|
$('#addaliasAddress').attr('type', 'text');
|
||||||
$('#addaliasAddress').attr('placeholder', 'incoming domain (@yourdomain.com)');
|
$('#addaliasAddress').attr('placeholder', '@yourdomain.com (incoming catch-all domain)');
|
||||||
$('#addaliasReceivers').attr('placeholder', 'forward to domain (@yourdomain.com)');
|
$('#addaliasForwardsTo').attr('placeholder', '@otherdomain.com (forward to other domain)');
|
||||||
$('#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');
|
$('#addalias-form .domainalias').removeClass('hidden');
|
||||||
$('#alias_mode_info span.domainalias').removeClass('hidden');
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
$('#alias_type_buttons button[data-mode="regular"]').click(); // init
|
$('#alias_type_buttons button[data-mode="regular"]').click(); // init
|
||||||
|
@ -153,16 +172,20 @@ 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 form_address = $("#addaliasAddress").val();
|
var form_address = $("#addaliasAddress").val();
|
||||||
var form_receivers = $("#addaliasReceivers").val();
|
var form_forwardsto = $("#addaliasForwardsTo").val();
|
||||||
var form_senders = $("#addaliasSenders").val();
|
var form_senders = ($('#addaliasForwardsToAdvanced').prop('checked') ? $("#addaliasSenders").val() : '');
|
||||||
|
if ($('#addaliasForwardsToAdvanced').prop('checked') && !/\S/.exec($("#addaliasSenders").val())) {
|
||||||
|
show_modal_error(title, "You did not enter any permitted senders.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
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',
|
||||||
address: form_address,
|
address: form_address,
|
||||||
receivers: form_receivers,
|
forwards_to: form_forwardsto,
|
||||||
senders: form_senders
|
permitted_senders: form_senders
|
||||||
},
|
},
|
||||||
function(r) {
|
function(r) {
|
||||||
// Responses are multiple lines of pre-formatted text.
|
// Responses are multiple lines of pre-formatted text.
|
||||||
|
@ -179,7 +202,7 @@ function do_add_alias() {
|
||||||
function aliases_reset_form() {
|
function aliases_reset_form() {
|
||||||
$("#addaliasAddress").prop('disabled', false);
|
$("#addaliasAddress").prop('disabled', false);
|
||||||
$("#addaliasAddress").val('')
|
$("#addaliasAddress").val('')
|
||||||
$("#addaliasReceivers").val('')
|
$("#addaliasForwardsTo").val('')
|
||||||
$("#addaliasSenders").val('')
|
$("#addaliasSenders").val('')
|
||||||
$('#alias-cancel').addClass('hidden');
|
$('#alias-cancel').addClass('hidden');
|
||||||
$('#add-alias-button').text('Add Alias');
|
$('#add-alias-button').text('Add Alias');
|
||||||
|
@ -188,15 +211,15 @@ function aliases_reset_form() {
|
||||||
|
|
||||||
function aliases_edit(elem) {
|
function aliases_edit(elem) {
|
||||||
var address = $(elem).parents('tr').attr('data-address');
|
var address = $(elem).parents('tr').attr('data-address');
|
||||||
var receiverdivs = $(elem).parents('tr').find('.receivers div');
|
var receiverdivs = $(elem).parents('tr').find('.forwardsTo div');
|
||||||
var senderdivs = $(elem).parents('tr').find('.senders div');
|
var senderdivs = $(elem).parents('tr').find('.senders div');
|
||||||
var receivers = "";
|
var forwardsTo = "";
|
||||||
for (var i = 0; i < receiverdivs.length; i++)
|
for (var i = 0; i < receiverdivs.length; i++)
|
||||||
receivers += $(receiverdivs[i]).text() + "\n";
|
forwardsTo += $(receiverdivs[i]).text() + "\n";
|
||||||
var senders = "";
|
var senders = "";
|
||||||
for (var i = 0; i < senderdivs.length; i++)
|
for (var i = 0; i < senderdivs.length; i++)
|
||||||
senders += $(senderdivs[i]).text() + "\n";
|
senders += $(senderdivs[i]).text() + "\n";
|
||||||
if (address.charAt(0) == '@' && receivers.charAt(0) == '@')
|
if (address.charAt(0) == '@' && forwardsTo.charAt(0) == '@')
|
||||||
$('#alias_type_buttons button[data-mode="domainalias"]').click();
|
$('#alias_type_buttons button[data-mode="domainalias"]').click();
|
||||||
else if (address.charAt(0) == '@')
|
else if (address.charAt(0) == '@')
|
||||||
$('#alias_type_buttons button[data-mode="catchall"]').click();
|
$('#alias_type_buttons button[data-mode="catchall"]').click();
|
||||||
|
@ -205,7 +228,9 @@ function aliases_edit(elem) {
|
||||||
$('#alias-cancel').removeClass('hidden');
|
$('#alias-cancel').removeClass('hidden');
|
||||||
$("#addaliasAddress").prop('disabled', true);
|
$("#addaliasAddress").prop('disabled', true);
|
||||||
$("#addaliasAddress").val(address);
|
$("#addaliasAddress").val(address);
|
||||||
$("#addaliasReceivers").val(receivers);
|
$("#addaliasForwardsTo").val(forwardsTo);
|
||||||
|
$('#addaliasForwardsToAdvanced').prop('checked', senders != "");
|
||||||
|
$('#addaliasForwardsToNotAdvanced').prop('checked', senders == "");
|
||||||
$("#addaliasSenders").val(senders);
|
$("#addaliasSenders").val(senders);
|
||||||
$('#add-alias-button').text('Update');
|
$('#add-alias-button').text('Update');
|
||||||
$('body').animate({ scrollTop: 0 })
|
$('body').animate({ scrollTop: 0 })
|
||||||
|
|
|
@ -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, address TEXT NOT NULL UNIQUE, receivers TEXT NOT NULL, senders TEXT NOT NULL);" | sqlite3 $db_path;
|
echo "CREATE TABLE aliases (id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL UNIQUE, destination TEXT NOT NULL, permitted_senders TEXT);" | sqlite3 $db_path;
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ### User Authentication
|
# ### User Authentication
|
||||||
|
@ -71,18 +71,23 @@ tools/editconf.py /etc/postfix/main.cf \
|
||||||
|
|
||||||
# ### Sender Validation
|
# ### Sender Validation
|
||||||
|
|
||||||
# Use a Sqlite3 database to set login maps. This is used with
|
# We use Postfix's reject_authenticated_sender_login_mismatch filter to
|
||||||
# reject_authenticated_sender_login_mismatch to see if the user is
|
# prevent intra-domain spoofing by logged in but untrusted users in outbound
|
||||||
# allowed to send mail as the FROM address specified in the request.
|
# email. In all outbound mail (the sender has authenticated), the MAIL FROM
|
||||||
|
# address (aka envelope or return path address) must be "owned" by the user
|
||||||
|
# who authenticated. An SQL query will find who are the owners of any given
|
||||||
|
# address.
|
||||||
tools/editconf.py /etc/postfix/main.cf \
|
tools/editconf.py /etc/postfix/main.cf \
|
||||||
smtpd_sender_login_maps=sqlite:/etc/postfix/sender-login-maps.cf
|
smtpd_sender_login_maps=sqlite:/etc/postfix/sender-login-maps.cf
|
||||||
|
|
||||||
# SQL statement that returns a list of addresses/domains the logged in username
|
# Postfix will query the exact address first, where the priority will be alias
|
||||||
# is allowed to send as. This is similar to virtual-alias-maps.cf (see below).
|
# records first, then user records. If there are no matches for the exact
|
||||||
# Matches from the users table take priority over (direct) aliases.
|
# address, then Postfix will query just the domain part, which we call
|
||||||
|
# catch-alls and domain aliases. A NULL permitted_senders column means to
|
||||||
|
# take the value from the destination column.
|
||||||
cat > /etc/postfix/sender-login-maps.cf << EOF;
|
cat > /etc/postfix/sender-login-maps.cf << EOF;
|
||||||
dbpath=$db_path
|
dbpath=$db_path
|
||||||
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;
|
query = SELECT permitted_senders FROM (SELECT permitted_senders, 0 AS priority FROM aliases WHERE source='%s' AND permitted_senders IS NOT NULL UNION SELECT destination AS permitted_senders, 1 AS priority FROM aliases WHERE source='%s' AND permitted_senders IS NULL UNION SELECT email as permitted_senders, 2 AS priority FROM users WHERE email='%s') ORDER BY priority LIMIT 1;
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# ### Destination Validation
|
# ### Destination Validation
|
||||||
|
@ -95,13 +100,13 @@ tools/editconf.py /etc/postfix/main.cf \
|
||||||
virtual_alias_maps=sqlite:/etc/postfix/virtual-alias-maps.cf \
|
virtual_alias_maps=sqlite:/etc/postfix/virtual-alias-maps.cf \
|
||||||
local_recipient_maps=\$virtual_mailbox_maps
|
local_recipient_maps=\$virtual_mailbox_maps
|
||||||
|
|
||||||
# SQL statement to check if we handle mail for a domain, either for users or aliases.
|
# SQL statement to check if we handle incoming 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 address LIKE '%%@%s'
|
query = SELECT 1 FROM users WHERE email LIKE '%%@%s' UNION SELECT 1 FROM aliases WHERE source LIKE '%%@%s'
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# SQL statement to check if we handle mail for a user.
|
# SQL statement to check if we handle incoming mail for a user.
|
||||||
cat > /etc/postfix/virtual-mailbox-maps.cf << EOF;
|
cat > /etc/postfix/virtual-mailbox-maps.cf << EOF;
|
||||||
dbpath=$db_path
|
dbpath=$db_path
|
||||||
query = SELECT 1 FROM users WHERE email='%s'
|
query = SELECT 1 FROM users WHERE email='%s'
|
||||||
|
@ -127,9 +132,13 @@ EOF
|
||||||
# might be returned by the UNION, so the whole query is wrapped in
|
# might be returned by the UNION, so the whole query is wrapped in
|
||||||
# another select that prioritizes the alias definition to preserve
|
# another select that prioritizes the alias definition to preserve
|
||||||
# postfix's preference for aliases for whole email addresses.
|
# postfix's preference for aliases for whole email addresses.
|
||||||
|
#
|
||||||
|
# Since we might have alias records with an empty destination because
|
||||||
|
# it might have just permitted_senders, skip any records with an
|
||||||
|
# empty destination here so that other lower priority rules might match.
|
||||||
cat > /etc/postfix/virtual-alias-maps.cf << EOF;
|
cat > /etc/postfix/virtual-alias-maps.cf << EOF;
|
||||||
dbpath=$db_path
|
dbpath=$db_path
|
||||||
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;
|
query = SELECT destination from (SELECT destination, 0 as priority FROM aliases WHERE source='%s' AND destination<>'' UNION SELECT email as destination, 1 as priority FROM users WHERE email='%s') ORDER BY priority LIMIT 1;
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Restart Services
|
# Restart Services
|
||||||
|
|
|
@ -102,56 +102,14 @@ 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
|
# Add a column to the aliases table to store permitted_senders,
|
||||||
# directions) to two columns (one for determining inbound forward-tos and
|
# which is a list of user account email addresses that are
|
||||||
# one for determining outbound permitted-senders). This was motivated by the
|
# permitted to send mail using this alias instead of their own
|
||||||
# addition of #427 ("Reject outgoing mail if FROM does not match Login") -
|
# address. This was motivated by the addition of #427 ("Reject
|
||||||
# which introduced the notion of outbound permitted-senders.
|
# 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')
|
||||||
# Move the old aliases table to one side.
|
shell("check_call", ["sqlite3", db, "ALTER TABLE aliases ADD permitted_senders TEXT"])
|
||||||
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
|
||||||
|
|
|
@ -120,10 +120,10 @@ elif sys.argv[1] == "alias" and len(sys.argv) == 2:
|
||||||
print(mgmt("/mail/aliases"))
|
print(mgmt("/mail/aliases"))
|
||||||
|
|
||||||
elif sys.argv[1] == "alias" and sys.argv[2] == "add" and len(sys.argv) == 5:
|
elif sys.argv[1] == "alias" and sys.argv[2] == "add" and len(sys.argv) == 5:
|
||||||
print(mgmt("/mail/aliases/add", { "source": sys.argv[3], "destination": sys.argv[4] }))
|
print(mgmt("/mail/aliases/add", { "address": sys.argv[3], "forwards_to": sys.argv[4] }))
|
||||||
|
|
||||||
elif sys.argv[1] == "alias" and sys.argv[2] == "remove" and len(sys.argv) == 4:
|
elif sys.argv[1] == "alias" and sys.argv[2] == "remove" and len(sys.argv) == 4:
|
||||||
print(mgmt("/mail/aliases/remove", { "source": sys.argv[3] }))
|
print(mgmt("/mail/aliases/remove", { "address": sys.argv[3] }))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print("Invalid command-line arguments.")
|
print("Invalid command-line arguments.")
|
||||||
|
|
Loading…
Reference in New Issue