Commit e6ff2809 authored by David Piggott's avatar David Piggott

Store and set alias receivers and senders separately for maximum control

parent 3fdfad27
......@@ -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')
)
......
This diff is collapsed.
......@@ -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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment