support "domain aliases" (@domain => @domain aliases)

This seemed to already be technically supported but the validation is now stricter and the admin is more helpful:

* Postfix seems to allow @domain.tld as an alias destination address but only if it is the only destination address (see the virtual man page).
 * Allow @domain.tld if it is the whole destination address string.
 * Otherwise, do not allow email addresses without local parts in the destination.
* In the admin, add a third tab for making it clear how to add a domain alias.

closes #265
This commit is contained in:
Joshua Tauberer 2014-11-14 13:33:12 +00:00
parent 9b9f5abf8f
commit 7e7abf3b53
2 changed files with 53 additions and 21 deletions

View File

@ -15,7 +15,7 @@ def validate_email(email, mode=None):
if mode == 'user':
# For Dovecot's benefit, only allow basic characters.
ATEXT = r'[\w\-]'
elif mode == 'alias':
elif mode in (None, 'alias'):
# For aliases, we can allow any valid email address.
# Based on RFC 2822 and https://github.com/SyrusAkbary/validate_email/blob/master/validate_email.py,
# these characters are permitted in email addresses.
@ -27,7 +27,8 @@ def validate_email(email, mode=None):
DOT_ATOM_TEXT_LOCAL = ATEXT + r'+(?:\.' + ATEXT + r'+)*'
if mode == 'alias':
# For aliases, Postfix accepts '@domain.tld' format for
# catch-all addresses. Make the local part optional.
# catch-all addresses on the source side and domain aliases
# on the destination side. Make the local part optional.
DOT_ATOM_TEXT_LOCAL = '(?:' + DOT_ATOM_TEXT_LOCAL + ')?'
# as above, but we can require that the host part have at least
@ -356,19 +357,28 @@ def add_mail_alias(source, destination, env, update_if_exists=False, do_kick=Tru
if not validate_email(source, mode='alias'):
return ("Invalid incoming email address (%s)." % source, 400)
# parse comma and \n-separated destination emails & validate
# validate destination
dests = []
for line in destination.split("\n"):
for email in line.split(","):
email = email.strip()
if email == "": continue
if not validate_email(email, mode='alias'):
return ("Invalid destination email address (%s)." % email, 400)
dests.append(email)
destination = destination.strip()
if validate_email(destination, mode='alias'):
# Oostfix allows a single @domain.tld as the destination, which means
# the local part on the address is preserved in the rewrite.
dests.append(destination)
else:
# Parse comma and \n-separated destination emails & validate. In this
# case, the recipients must be complete email addresses.
for line in destination.split("\n"):
for email in line.split(","):
email = email.strip()
if email == "": continue
if not validate_email(email):
return ("Invalid destination email address (%s)." % email, 400)
dests.append(email)
if len(destination) == 0:
return ("No destination email address(es) provided.", 400)
destination = ",".join(dests)
# save to db
conn, c = open_database(env, with_connection=True)
try:
c.execute("INSERT INTO aliases (source, destination) VALUES (?, ?)", (source, destination))

View File

@ -13,22 +13,26 @@
<div class="form-group">
<div class="col-sm-offset-1 col-sm-11">
<div id="alias_type_buttons" class="btn-group btn-group-xs">
<button type="button" class="btn btn-default active">Regular</button>
<button type="button" class="btn btn-default">Catch-All</button>
<button type="button" class="btn btn-default active" data-mode="regular">Regular</button>
<button type="button" class="btn btn-default" data-mode="catchall">Catch-All</button>
<button type="button" class="btn btn-default" data-mode="domainalias">Domain Alias</button>
</div>
<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="domainalias hidden">A domain alias forwards all otherwise unmatched mail from one domain to another domain, preserving the part before the @-sign.</span>
</div>
<div id="alias_catchall_info" class="text-info small" style="display: none; margin: .5em 0 0 0;">A catch-all alias captures all otherwise unmatched email to a domain. Enter just a part of an email address starting with the @-sign.</div>
</div>
</div>
<div class="form-group">
<label for="addaliasEmail" class="col-sm-1 control-label">Alias</label>
<div class="col-sm-10">
<input type="email" class="form-control" id="addaliasEmail" placeholder="incoming email address (you@yourdomain.com)">
<input type="email" class="form-control" id="addaliasEmail">
</div>
</div>
<div class="form-group">
<label for="addaliasTargets" class="col-sm-1 control-label">Forward To</label>
<div class="col-sm-10">
<textarea class="form-control" rows="3" id="addaliasTargets" placeholder="forward to these email addresses (one per line or separated by commas)"></textarea>
<textarea class="form-control" rows="3" id="addaliasTargets"></textarea>
</div>
</div>
<div class="form-group">
@ -106,16 +110,28 @@ function show_aliases() {
$('#alias_type_buttons button').off('click').click(function() {
$('#alias_type_buttons button').removeClass('active');
$(this).addClass('active');
if ($(this).text() == "Regular") {
if ($(this).attr('data-mode') == "regular") {
$('#addaliasEmail').attr('type', 'email');
$('#addaliasEmail').attr('placeholder', 'incoming email address (you@yourdomain.com)');
$('#alias_catchall_info').slideUp();
} else {
$('#addaliasEmail').attr('placeholder', 'incoming email address (e.g. you@yourdomain.com)');
$('#addaliasTargets').attr('placeholder', 'forward to these email addresses (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 (@yourdomain.com)');
$('#alias_catchall_info').slideDown();
$('#addaliasEmail').attr('placeholder', 'incoming catch-all address (e.g. @yourdomain.com)');
$('#addaliasTargets').attr('placeholder', 'forward to these email addresses (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)');
$('#addaliasTargets').attr('placeholder', 'forward to domain (@yourdomain.com)');
$('#alias_mode_info').slideDown();
$('#alias_mode_info span').addClass('hidden');
$('#alias_mode_info span.domainalias').removeClass('hidden');
}
})
$('#alias_type_buttons button[data-mode="regular"]').click(); // init
})
}
@ -166,6 +182,12 @@ function aliases_edit(elem) {
$("#addaliasEmail").val(email);
$("#addaliasTargets").val(targets);
$('#add-alias-button').text('Update');
if (email.charAt(0) == '@' && targets.charAt(0) == '@')
$('#alias_type_buttons button[data-mode="domainalias"]').click();
else if (email.charAt(0) == '@')
$('#alias_type_buttons button[data-mode="catchall"]').click();
else
$('#alias_type_buttons button[data-mode="regular"]').click();
$('body').animate({ scrollTop: 0 })
}