From 1d9b678d89b78449619143e84e75f8521d36543c Mon Sep 17 00:00:00 2001 From: Bernard `Guyzmo` Pratz Date: Sun, 24 Jan 2016 14:43:16 +0000 Subject: [PATCH 1/3] Added support of mail only configuration - added checkboxes to disable web and/or dns on alias or user creation - added ignore lists for domain not being handled on either web or dns - added API for managing ignore lists Signed-off-by: Bernard `Guyzmo` Pratz --- management/daemon.py | 13 +++++-- management/dns_update.py | 5 +-- management/mailconfig.py | 62 +++++++++++++++++++++++++++++-- management/templates/aliases.html | 27 +++++++++++++- management/templates/users.html | 23 +++++++++++- management/web_update.py | 4 +- 6 files changed, 120 insertions(+), 14 deletions(-) diff --git a/management/daemon.py b/management/daemon.py index 1099a596..dc16d7a5 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -155,7 +155,12 @@ def mail_users(): @authorized_personnel_only def mail_users_add(): try: - return add_mail_user(request.form.get('email', ''), request.form.get('password', ''), request.form.get('privileges', ''), env) + return add_mail_user(request.form.get('email', ''), + request.form.get('password', ''), + request.form.get('privileges', ''), + env, + dns_enabled=request.form.get('dns_enabled', False), + web_enabled=request.form.get('web_enabled', False)) except ValueError as e: return (str(e), 400) @@ -207,7 +212,9 @@ def mail_aliases_add(): request.form.get('forwards_to', ''), request.form.get('permitted_senders', ''), env, - update_if_exists=(request.form.get('update_if_exists', '') == '1') + update_if_exists=(request.form.get('update_if_exists', '') == '1'), + dns_enabled=request.form.get('dns_enabled', False), + web_enabled=request.form.get('web_enabled', False) ) @app.route('/mail/aliases/remove', methods=['POST']) @@ -335,7 +342,7 @@ def ssl_get_status(): # What domains can we provision certificates for? What unexpected problems do we have? provision, cant_provision = get_certificates_to_provision(env, show_extended_problems=False) - + # What's the current status of TLS certificates on all of the domain? domains_status = get_web_domains_info(env) domains_status = [{ "domain": d["domain"], "status": d["ssl_certificate"][0], "text": d["ssl_certificate"][1] } for d in domains_status ] diff --git a/management/dns_update.py b/management/dns_update.py index d3ef2cbc..c978a834 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -15,8 +15,7 @@ from utils import shell, load_env_vars_from_file, safe_domain_name, sort_domains def get_dns_domains(env): # Add all domain names in use by email users and mail aliases and ensure # PRIMARY_HOSTNAME is in the list. - domains = set() - domains |= get_mail_domains(env) + domains = get_mail_domains(env, filter_list='dns') domains.add(env['PRIMARY_HOSTNAME']) return domains @@ -144,7 +143,7 @@ def build_zone(domain, all_domains, additional_records, www_redirect_domains, en # Define ns2.PRIMARY_HOSTNAME or whatever the user overrides. # User may provide one or more additional nameservers secondary_ns_list = get_secondary_dns(additional_records, mode="NS") \ - or ["ns2." + env["PRIMARY_HOSTNAME"]] + or ["ns2." + env["PRIMARY_HOSTNAME"]] for secondary_ns in secondary_ns_list: records.append((None, "NS", secondary_ns+'.', False)) diff --git a/management/mailconfig.py b/management/mailconfig.py index d9ffdf65..befcc93b 100755 --- a/management/mailconfig.py +++ b/management/mailconfig.py @@ -3,8 +3,35 @@ import subprocess, shutil, os, sqlite3, re import utils from email_validator import validate_email as validate_email_, EmailNotValidError +import rtyaml import idna +def load_domain_blacklist(env, service): + try: + return set(rtyaml.load(open(os.path.join(env['STORAGE_ROOT'], '{}/ignored.yaml'.format(service))))) + except: + return set() + +def add_domain_blacklist(env, domain, service): + try: + ignored = set(rtyaml.load(open(os.path.join(env['STORAGE_ROOT'], '{}/ignored.yaml'.format(service))))) + ignored.add(domain) + except Exception as err: + # if non-existent or baldy formatted, consider it new + ignored = {domain} + with open(os.path.join(env['STORAGE_ROOT'], '{}/ignored.yaml'.format(service)), "w") as f: + f.write(rtyaml.dump(list(ignored))) + +def remove_domain_blacklist(env, domain, service): + try: + ignored = set(rtyaml.load(open(os.path.join(env['STORAGE_ROOT'], '{}/ignored.yaml'.format(service))))) + if domain in ignored: + ignored.remove(domain) + with open(os.path.join(env['STORAGE_ROOT'], '{}/ignored.yaml'.format(service)), "w") as f: + f.write(rtyaml.dump(list(ignored))) + except Exception as err: + pass + def validate_email(email, mode=None): # Checks that an email address is syntactically valid. Returns True/False. # Until Postfix supports SMTPUTF8, an email address may contain ASCII @@ -253,15 +280,20 @@ def get_domain(emailaddr, as_unicode=True): pass return ret -def get_mail_domains(env, filter_aliases=lambda alias : True): +def get_mail_domains(env, filter_aliases=lambda alias : True, filter_list=None): + ignored = set() + if filter_list == 'dns': + ignored |= load_domain_blacklist(env, 'dns') + if filter_list in ('web', 'www'): + ignored |= load_domain_blacklist(env, 'www') # Returns the domain names (IDNA-encoded) of all of the email addresses # configured on the system. return set( [get_domain(login, as_unicode=False) for login in get_mail_users(env)] + [get_domain(address, as_unicode=False) for address, *_ in get_mail_aliases(env) if filter_aliases(address) ] - ) + ) - ignored -def add_mail_user(email, pw, privs, env): +def add_mail_user(email, pw, privs, env, dns_enabled=True, web_enabled=True): # validate email if email.strip() == "": return ("No email address provided.", 400) @@ -303,6 +335,14 @@ def add_mail_user(email, pw, privs, env): # write databasebefore next step conn.commit() + # Add non dns enabled domain to the ignored domain list + if dns_enabled == 'false': + add_domain_blacklist(env, get_domain(email), 'dns') + + # Add non web enabled domain to the ignored domain list + if web_enabled == 'false': + add_domain_blacklist(env, get_domain(email), 'www') + # Update things in case any new domains are added. return kick(env, "mail user added") @@ -347,6 +387,9 @@ def remove_mail_user(email, env): return ("That's not a user (%s)." % email, 400) conn.commit() + remove_domain_blacklist(env, get_domain(address), 'dns') + remove_domain_blacklist(env, get_domain(address), 'www') + # Update things in case any domains are removed. return kick(env, "mail user removed") @@ -395,7 +438,7 @@ def add_remove_mail_user_privilege(email, priv, action, env): return "OK" -def add_mail_alias(address, forwards_to, permitted_senders, env, update_if_exists=False, do_kick=True): +def add_mail_alias(address, forwards_to, permitted_senders, env, update_if_exists=False, dns_enabled=True, web_enabled=True, do_kick=True): # convert Unicode domain to IDNA address = sanitize_idn_email_address(address) @@ -484,6 +527,14 @@ def add_mail_alias(address, forwards_to, permitted_senders, env, update_if_exist conn.commit() + # Add non dns enabled domain to the ignored domain list + if dns_enabled == 'false': + add_domain_blacklist(env, get_domain(address), 'dns') + + # Add non web enabled domain to the ignored domain list + if web_enabled == 'false': + add_domain_blacklist(env, get_domain(address), 'www') + if do_kick: # Update things in case any new domains are added. return kick(env, return_status) @@ -499,6 +550,9 @@ def remove_mail_alias(address, env, do_kick=True): return ("That's not an alias (%s)." % address, 400) conn.commit() + remove_domain_blacklist(env, get_domain(address), 'dns') + remove_domain_blacklist(env, get_domain(address), 'www') + if do_kick: # Update things in case any domains are removed. return kick(env, "alias removed") diff --git a/management/templates/aliases.html b/management/templates/aliases.html index dc916f95..50f538b2 100644 --- a/management/templates/aliases.html +++ b/management/templates/aliases.html @@ -64,6 +64,23 @@ +
+ +
+
+ +
+
+ +
+
+
@@ -174,6 +191,8 @@ function do_add_alias() { var form_address = $("#addaliasAddress").val(); var form_forwardsto = $("#addaliasForwardsTo").val(); var form_senders = ($('#addaliasForwardsToAdvanced').prop('checked') ? $("#addaliasSenders").val() : ''); + var form_dns = $('#addaliasDnsHandling').prop('checked'); + var form_web = $('#addaliasWebHandling').prop('checked'); if ($('#addaliasForwardsToAdvanced').prop('checked') && !/\S/.exec($("#addaliasSenders").val())) { show_modal_error(title, "You did not enter any permitted senders."); return false; @@ -185,7 +204,9 @@ function do_add_alias() { update_if_exists: is_alias_add_update ? '1' : '0', address: form_address, forwards_to: form_forwardsto, - permitted_senders: form_senders + permitted_senders: form_senders, + dns_enabled: form_dns, + web_enabled: form_web }, function(r) { // Responses are multiple lines of pre-formatted text. @@ -204,6 +225,8 @@ function aliases_reset_form() { $("#addaliasAddress").val('') $("#addaliasForwardsTo").val('') $("#addaliasSenders").val('') + $('#addaliasDnsHandling').prop('disabled', false); + $('#addaliasWebHandling').prop('disabled', false); $('#alias-cancel').addClass('hidden'); $('#add-alias-button').text('Add Alias'); is_alias_add_update = false; @@ -232,6 +255,8 @@ function aliases_edit(elem) { $('#addaliasForwardsToAdvanced').prop('checked', senders != ""); $('#addaliasForwardsToNotAdvanced').prop('checked', senders == ""); $("#addaliasSenders").val(senders); + $('#addaliasDnsHandling').prop('disabled', true); + $('#addaliasWebHandling').prop('disabled', true); $('#add-alias-button').text('Update'); $('body').animate({ scrollTop: 0 }) is_alias_add_update = true; diff --git a/management/templates/users.html b/management/templates/users.html index 010a1edf..6e07673a 100644 --- a/management/templates/users.html +++ b/management/templates/users.html @@ -28,6 +28,23 @@
+
+ +
+
+ +
+
+ +
+
+
    @@ -143,13 +160,17 @@ function do_add_user() { var email = $("#adduserEmail").val(); var pw = $("#adduserPassword").val(); var privs = $("#adduserPrivs").val(); + var form_dns = $('#adduserDnsHandling').prop('checked'); + var form_web = $('#adduserWebHandling').prop('checked'); api( "/mail/users/add", "POST", { email: email, password: pw, - privileges: privs + privileges: privs, + dns_enabled: form_dns, + web_enabled: form_web }, function(r) { // Responses are multiple lines of pre-formatted text. diff --git a/management/web_update.py b/management/web_update.py index ffd1cff9..e36986d2 100644 --- a/management/web_update.py +++ b/management/web_update.py @@ -16,14 +16,14 @@ def get_web_domains(env, include_www_redirects=True, exclude_dns_elsewhere=True) # Serve web for all mail domains so that we might at least # provide auto-discover of email settings, and also a static website # if the user wants to make one. - domains |= get_mail_domains(env) + domains |= get_mail_domains(env, filter_list='www') if include_www_redirects: # Add 'www.' subdomains that we want to provide default redirects # to the main domain for. We'll add 'www.' to any DNS zones, i.e. # the topmost of each domain we serve. domains |= set('www.' + zone for zone, zonefile in get_dns_zones(env)) - + if exclude_dns_elsewhere: # ...Unless the domain has an A/AAAA record that maps it to a different # IP address than this box. Remove those domains from our list. From 4e2b109a7240ca1962743046cee9773c1d64dc4f Mon Sep 17 00:00:00 2001 From: Bernard `Guyzmo` Pratz Date: Wed, 27 Jan 2016 18:30:19 +0100 Subject: [PATCH 2/3] Improved UI for services handling * Used a three button row and with a nice message Signed-off-by: Bernard `Guyzmo` Pratz --- management/templates/aliases.html | 65 ++++++++++++++++++++++--------- management/templates/users.html | 53 ++++++++++++++++++------- 2 files changed, 84 insertions(+), 34 deletions(-) diff --git a/management/templates/aliases.html b/management/templates/aliases.html index 50f538b2..f8fb3c17 100644 --- a/management/templates/aliases.html +++ b/management/templates/aliases.html @@ -26,7 +26,7 @@
- +
@@ -36,7 +36,7 @@
- +
@@ -45,7 +45,7 @@
- +
- +
-
- +
+ + +
-
- +
@@ -182,7 +179,33 @@ function show_aliases() { } }) $('#alias_type_buttons button[data-mode="regular"]').click(); // init - }) + + // Service buttons + $('#alias_service_buttons button').off('click').click(function() { + if ($(this).hasClass('active')) { + $(this).removeClass('active'); + $(this).addClass('btn-default'); + $(this).removeClass('btn-success'); + $('#alias_service_info .www').addClass('hidden'); + $('#alias_service_info .dns').addClass('hidden'); + if ($(this).attr('data-mode') == "dns") { + $('#alias_service_info').slideDown(); + $('#addalias-form .dns').removeClass('hidden'); + } else if ($(this).attr('data-mode') == "www") { + $('#alias_service_info').slideDown(); + $('#addalias-form .www').removeClass('hidden'); + } + } else { + $(this).addClass('active'); + $(this).removeClass('btn-default'); + $(this).addClass('btn-success'); + $('#alias_service_info').slideUp(); + $('#addalias-form .www').addClass('hidden'); + $('#addalias-form .dns').addClass('hidden'); + } + return false; + }); + }); } var is_alias_add_update = false; @@ -191,8 +214,8 @@ function do_add_alias() { var form_address = $("#addaliasAddress").val(); var form_forwardsto = $("#addaliasForwardsTo").val(); var form_senders = ($('#addaliasForwardsToAdvanced').prop('checked') ? $("#addaliasSenders").val() : ''); - var form_dns = $('#addaliasDnsHandling').prop('checked'); - var form_web = $('#addaliasWebHandling').prop('checked'); + var form_dns = $('#addaliasDnsHandling').hasClass('active'); + var form_web = $('#addaliasWebHandling').hasClass('active'); if ($('#addaliasForwardsToAdvanced').prop('checked') && !/\S/.exec($("#addaliasSenders").val())) { show_modal_error(title, "You did not enter any permitted senders."); return false; @@ -225,13 +248,17 @@ function aliases_reset_form() { $("#addaliasAddress").val('') $("#addaliasForwardsTo").val('') $("#addaliasSenders").val('') - $('#addaliasDnsHandling').prop('disabled', false); - $('#addaliasWebHandling').prop('disabled', false); $('#alias-cancel').addClass('hidden'); $('#add-alias-button').text('Add Alias'); is_alias_add_update = false; + + $('#addaliasDnsHandling').prop('disabled', false).addClass('btn-success'); + $('#addaliasWebHandling').prop('disabled', false).addClass('btn-success'); + } + + function aliases_edit(elem) { var address = $(elem).parents('tr').attr('data-address'); var receiverdivs = $(elem).parents('tr').find('.forwardsTo div'); diff --git a/management/templates/users.html b/management/templates/users.html index 6e07673a..8c57c20f 100644 --- a/management/templates/users.html +++ b/management/templates/users.html @@ -29,24 +29,20 @@
- -
-
- -
-
- +
+
+ + +
+
  • Passwords must be at least four characters and may not contain spaces. For best results, generate a random password.
  • Use aliases to create email addresses that forward to existing accounts.
  • @@ -154,14 +150,41 @@ function show_users() { } } }) + + // Service buttons + $('#user_service_buttons button').off('click').click(function() { + if ($(this).hasClass('active')) { + $(this).removeClass('active'); + $(this).addClass('btn-default'); + $(this).removeClass('btn-success'); + $('#user_service_info .www').addClass('hidden'); + $('#user_service_info .dns').addClass('hidden'); + if ($(this).attr('data-mode') == "dns") { + $('#user_service_info').slideDown(); + $('#user_service_info .dns').removeClass('hidden'); + } else if ($(this).attr('data-mode') == "www") { + $('#user_service_info').slideDown(); + $('#user_service_info .www').removeClass('hidden'); + } + } else { + $(this).addClass('active'); + $(this).removeClass('btn-default'); + $(this).addClass('btn-success'); + $('#user_service_info').slideUp(); + $('#user_service_info .www').addClass('hidden'); + $('#user_service_info .dns').addClass('hidden'); + } + return false; + }); + } function do_add_user() { var email = $("#adduserEmail").val(); var pw = $("#adduserPassword").val(); var privs = $("#adduserPrivs").val(); - var form_dns = $('#adduserDnsHandling').prop('checked'); - var form_web = $('#adduserWebHandling').prop('checked'); + var form_dns = $('#adduserDnsHandling').hasClass('active'); + var form_web = $('#adduserWebHandling').hasClass('active'); api( "/mail/users/add", "POST", From 5a3af9525a099137c5281a2c3758e8140651dbff Mon Sep 17 00:00:00 2001 From: Bernard `Guyzmo` Pratz Date: Fri, 29 Jan 2016 20:31:21 +0100 Subject: [PATCH 3/3] Added support for custom DNS-only zones - Updated dropdown list for domains, - Added free text input for writing down domain addresses, - Updated backend with support of custom zones Signed-off-by: Bernard `Guyzmo` Pratz --- management/dns_update.py | 24 ++++----- management/templates/custom-dns.html | 73 ++++++++++++++++++++++++++-- 2 files changed, 82 insertions(+), 15 deletions(-) diff --git a/management/dns_update.py b/management/dns_update.py index c978a834..ac4a3f7d 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -22,7 +22,9 @@ def get_dns_domains(env): def get_dns_zones(env): # What domains should we create DNS zones for? Never create a zone for # a domain & a subdomain of that domain. - domains = get_dns_domains(env) + domains_mail = get_dns_domains(env) + domains_custom = set([n for n, *_ in get_custom_dns_config(env)]) + domains = domains_mail | domains_custom # Exclude domains that are subdomains of other domains we know. Proceed # by looking at shorter domains first. @@ -727,16 +729,16 @@ def write_custom_dns_config(config, env): f.write(config_yaml) def set_custom_dns_record(qname, rtype, value, action, env): - # validate qname - for zone, fn in get_dns_zones(env): - # It must match a zone apex or be a subdomain of a zone - # that we are otherwise hosting. - if qname == zone or qname.endswith("."+zone): - break - else: - # No match. - if qname != "_secondary_nameserver": - raise ValueError("%s is not a domain name or a subdomain of a domain name managed by this box." % qname) + # # validate qname + # for zone, fn in get_dns_zones(env): + # # It must match a zone apex or be a subdomain of a zone + # # that we are otherwise hosting. + # if qname == zone or qname.endswith("."+zone): + # break + # else: + # # No match. + # if qname != "_secondary_nameserver": + # raise ValueError("%s is not a domain name or a subdomain of a domain name managed by this box." % qname) # validate rtype rtype = rtype.upper() diff --git a/management/templates/custom-dns.html b/management/templates/custom-dns.html index f1244810..30ad5af9 100644 --- a/management/templates/custom-dns.html +++ b/management/templates/custom-dns.html @@ -23,6 +23,13 @@ . +
    + + + + +
    +
    Leave the left field blank to set a record on the chosen domain name, or enter a subdomain.
@@ -163,19 +170,69 @@ function show_custom_dns() { $('#secondarydns-clear-instructions').toggle(data.hostnames.length > 0); }); + $('#customdnsZone').text(''); + var dns_list_domains = $('').attr('label', 'Domains'); + var dns_list_customs = $('').attr('label', 'Custom'); + var dns_list_advanced = $('').attr('label', 'Advanced'); + + $('#customdnsZone').append(dns_list_domains); + $('#customdnsZone').append(dns_list_customs); + $('#customdnsZone').append(dns_list_advanced); + + dns_list_advanced.append($('