diff --git a/Dockerfile.dev b/Dockerfile.dev index 2621f68d3..33f195eb8 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -2,13 +2,14 @@ FROM python:3.6-alpine MAINTAINER Antoine Nguyen -RUN apk add --update python3-dev libffi-dev gcc musl-dev libxml2-dev libxslt-dev \ +RUN apk add --update openssl python3-dev libffi-dev gcc musl-dev libxml2-dev libxslt-dev \ libressl-dev jpeg-dev \ && rm -rf /var/cache/apk/* WORKDIR /tmp COPY requirements.txt /tmp -RUN pip install -r requirements.txt +COPY test-requirements.txt /tmp +RUN pip install -r requirements.txt -r test-requirements.txt RUN mkdir /code WORKDIR /code diff --git a/modoboa/admin/app_settings.py b/modoboa/admin/app_settings.py index cd1afa3e7..ebe6032f3 100644 --- a/modoboa/admin/app_settings.py +++ b/modoboa/admin/app_settings.py @@ -10,6 +10,7 @@ from django import forms from django.conf import settings from django.utils.encoding import force_text from django.utils.translation import ugettext as _, ugettext_lazy +from django.forms.fields import GenericIPAddressField from modoboa.lib.form_utils import SeparatorField, YesNoField from modoboa.lib.sysutils import exec_cmd @@ -78,6 +79,14 @@ class AdminParametersForm(param_forms.AdminParametersForm): ) ) + use_specific_dns_server = GenericIPAddressField( + label=ugettext_lazy("Use specific DNS server"), + required=False, + help_text=ugettext_lazy( + "Use a specific DNS server instead of local server configuration" + ) + ) + mbsep = SeparatorField(label=ugettext_lazy("Mailboxes")) handle_mailboxes = YesNoField( diff --git a/modoboa/admin/lib.py b/modoboa/admin/lib.py index 2b1819bed..a8aabd51d 100644 --- a/modoboa/admin/lib.py +++ b/modoboa/admin/lib.py @@ -175,6 +175,10 @@ def get_domain_mx_list(domain): """Return a list of MX IP address for domain.""" result = [] logger = logging.getLogger("modoboa.admin") + dns_server = param_tools.get_global_parameter("use_specific_dns_server") + if dns_server: + dns.resolver.default_resolver.nameservers = [dns_server] + try: dns_answers = dns.resolver.query(domain, "MX") except dns.resolver.NXDOMAIN as e: @@ -192,28 +196,26 @@ def get_domain_mx_list(domain): try: mx_domain = dns_answer.exchange.to_unicode( omit_final_dot=True, idna_codec=IDNA_2008_UTS_46) - ip_answers = socket.getaddrinfo( - mx_domain, 25, 0, socket.SOCK_STREAM) - except socket.gaierror as e: - logger.warning( - _("Unable to lookup ip addresses for %(domain)s; " - "%(error)s") - % {"domain": mx_domain, "error": str(e)}, - exc_info=e) + ip_answers = dns.resolver.query(mx_domain) + except dns.resolver.NXDOMAIN as e: + logger.error(_("No DNS records found without MX for %s") + % domain, exc_info=e) else: for ip_answer in ip_answers: - try: - mx_ip = ipaddress.ip_address( - smart_text(ip_answer[4][0])) - except ValueError as e: - logger.warning( - _("Invalid IP address format for %(domain)s; " - "%(addr)s") - % {"domain": mx_domain, - "addr": smart_text(ip_answer[4][0])}, - exc_info=e) - else: - result.append((mx_domain, mx_ip)) + for answer_set in ip_answer.response.answer: + for address in answer_set: + try: + address_smart = smart_text(address) + mx_ip = ipaddress.ip_address(address_smart) + except ValueError as e: + logger.warning( + _("Invalid IP address format for " + "%(domain)s; %(addr)s") + % {"domain": mx_domain, + "addr": smart_text(address)}, + exc_info=e) + else: + result.append((mx_domain, mx_ip)) return result diff --git a/modoboa/admin/tests/test_mx.py b/modoboa/admin/tests/test_mx.py index fea42ee53..b7bdc962f 100644 --- a/modoboa/admin/tests/test_mx.py +++ b/modoboa/admin/tests/test_mx.py @@ -110,9 +110,19 @@ class MXTestCase(ModoTestCase): @mock.patch("socket.getaddrinfo") @mock.patch.object(dns.resolver.Resolver, "query") - def test_get_domain_mx_list_logging(self, mock_query, mock_getaddrinfo): + def test_get_mx_list_dsn_server(self, mock_query, mock_getaddrinfo): + """Test to get mx list from specific DNS server.""" mock_query.side_effect = utils.mock_dns_query_result mock_getaddrinfo.side_effect = utils.mock_ip_query_result + self.set_global_parameter("use_specific_dns_server", "123.45.67.89") + get_domain_mx_list("does-not-exist.example.com") + + @mock.patch("ipaddress.ip_address") + @mock.patch.object(dns.resolver.Resolver, "query") + def test_get_domain_mx_list_logging(self, mock_query, mock_ip_address): + """Test to get error loggins from specific DNS server.""" + mock_query.side_effect = utils.mock_dns_query_result + mock_ip_address.side_effect = utils.mock_ip_address_result with LogCapture("modoboa.admin") as log: get_domain_mx_list("does-not-exist.example.com") get_domain_mx_list("no-mx.example.com") @@ -130,12 +140,12 @@ class MXTestCase(ModoTestCase): ("modoboa.admin", "WARNING", _("DNS resolution timeout, unable to query %s at the moment") % "timeout.example.com"), + ("modoboa.admin", "ERROR", + _("No DNS records found without MX for %(domain)s") + % {"domain": "no-lookup.example.com"}), ("modoboa.admin", "WARNING", - _("Unable to lookup ip addresses for %(domain)s; %(error)s") - % {"domain": "does-not-exist.example.com", "error": ""}), - ("modoboa.admin", "WARNING", - _("Invalid IP address format for %(domain)s; %(addr)s") - % {"domain": "bad-response.example.com", "addr": "BAD"}), + _("Invalid IP address format for %(domain)s; %(addr)s") + % {"domain": "bad-response.example.com", "addr": "000.0.0.0"}), ) diff --git a/modoboa/admin/tests/utils.py b/modoboa/admin/tests/utils.py index 797fc6dcb..3e9161d2b 100644 --- a/modoboa/admin/tests/utils.py +++ b/modoboa/admin/tests/utils.py @@ -3,6 +3,7 @@ """Test utilities.""" from __future__ import unicode_literals +from dns.rrset import RRset import socket @@ -10,12 +11,38 @@ from dns.name import Name from dns.rdtypes.ANY.MX import MX from dns.resolver import NXDOMAIN, NoAnswer, NoNameservers, Timeout + + +class RRsetResponse(object): + + def __init__(self): + self.answer = [["192.0.2.1"]] + +class RRset(object): + + def __init__(self): + self.response = RRsetResponse() + +class RRsetResponseInvalid(object): + + def __init__(self): + self.answer = [["000.0.0.0"]] + +class RRsetInvalid(object): + + def __init__(self): + self.response = RRsetResponseInvalid() + + + _MX_RECORD_1 = MX("IN", "MX", 10, Name("mx.example.com".split("."))) _MX_RECORD_2 = MX("IN", "MX", 10, Name("mx2.example.com".split("."))) _BAD_MX_RECORD = MX("IN", "MX", 10, Name("bad-response.example.com".split("."))) _DNE_MX_RECORD = MX("IN", "MX", 10, Name( "does-not-exist.example.com".split("."))) _MX_RECORDS = [_MX_RECORD_1] +_IP_SET_RECORDS = [RRset()] + _IPV4_RECORD_1 = (socket.AF_INET, socket.SOCK_STREAM, 6, "", ("192.0.2.1", 25)) _IPV4_RECORD_2 = (socket.AF_INET, socket.SOCK_STREAM, 6, "", ("192.0.2.2", 25)) @@ -33,6 +60,12 @@ _POSSIBLE_DNS_RESULTS = { "no-lookup.example.com": [_DNE_MX_RECORD], } +_POSSIBLE_DNS_RESULTS_NO_MX = { + "does-not-exist.example.com": NXDOMAIN(), + "mx2.example.com": NXDOMAIN(), + "no-lookup.example.com": NXDOMAIN(), + "bad-response.example.com": [RRsetInvalid()], +} _POSSIBLE_IP_RESULTS = { "test3.com": [_IPV4_RECORD_2], "mx2.example.com": [_IPV4_RECORD_2], @@ -40,18 +73,36 @@ _POSSIBLE_IP_RESULTS = { "does-not-exist.example.com": socket.gaierror(), "192.0.2.254": socket.gaierror(), "2001:db8:254": socket.gaierror(), + "000.0.0.0": ValueError(), } def mock_dns_query_result(qname, *args, **kwargs): - if qname in _POSSIBLE_DNS_RESULTS: - if isinstance(_POSSIBLE_DNS_RESULTS[qname], Exception): - raise _POSSIBLE_DNS_RESULTS[qname] + if "MX" in args: + if qname in _POSSIBLE_DNS_RESULTS: + return get_mock_dns_query_response(_POSSIBLE_DNS_RESULTS, qname) + return _MX_RECORDS + elif qname in _POSSIBLE_DNS_RESULTS_NO_MX: + return get_mock_dns_query_response(_POSSIBLE_DNS_RESULTS_NO_MX, qname) + else: + return _IP_SET_RECORDS + + +def get_mock_dns_query_response(responses, attributeName): + if isinstance(responses[attributeName], Exception): + raise responses[attributeName] + else: + return responses[attributeName] + +def mock_ip_address_result(host, *args, **kwargs): + if host in _POSSIBLE_IP_RESULTS: + if isinstance(_POSSIBLE_IP_RESULTS[host], Exception): + raise _POSSIBLE_IP_RESULTS[host] else: - return _POSSIBLE_DNS_RESULTS[qname] - - return _MX_RECORDS - + return _POSSIBLE_IP_RESULTS[host] + + return _IP_RECORDS + def mock_ip_query_result(host, port, *args, **kwargs): if host in _POSSIBLE_IP_RESULTS: @@ -59,5 +110,5 @@ def mock_ip_query_result(host, port, *args, **kwargs): raise _POSSIBLE_IP_RESULTS[host] else: return _POSSIBLE_IP_RESULTS[host] - + return _IP_RECORDS diff --git a/modoboa/core/tests/test_views.py b/modoboa/core/tests/test_views.py index 20d730a0a..ce8fc71d5 100644 --- a/modoboa/core/tests/test_views.py +++ b/modoboa/core/tests/test_views.py @@ -68,7 +68,8 @@ SETTINGS_SAMPLE = { "core-random_password_length": "8", "admin-create_alias_on_mbox_rename": False, "admin-dkim_keys_storage_dir": "", - "admin-dkim_default_key_length": 1024 + "admin-dkim_default_key_length": 1024, + "admin-use_specific_dns_server": "193.43.55.67" }