Repository: modoboa/modoboa (language: python)

Issue:
Logging: ERROR modoboa.admin No AAAA record for mail.example.com
# Impacted versions

* OS Type: Debian/Ubuntu
* OS Version: 22.10
* Database Type: PostgreSQL
* Database version: 14.9
* Modoboa: 2.0.3
* installer used: Yes
* Webserver: Nginx

# Steps to reproduce
Clean install, no IPv6 addresses available
# Current behavior

<!--

  Explain the behavior you're seeing that you think is a bug, and explain how you
  think things should behave instead.

-->

The admin interface in modoboa - logs   shows an error:
```
09.10.2023 17:30 | ERROR | modoboa.admin | No AAAA record for mail.example.com
```

# Expected behavior
Do not show the error, when the machine does not support IPv6. 

Can we have an option to exclude IPv6 checks, if it is not available?

# Video/Screenshot link (optional)




Discussion / hints:

Fix patch:
diff --git a/modoboa/admin/api/v2/serializers.py b/modoboa/admin/api/v2/serializers.py
index 49eaba499..e3e44227a 100644
--- a/modoboa/admin/api/v2/serializers.py
+++ b/modoboa/admin/api/v2/serializers.py
@@ -181,6 +181,7 @@ class AdminGlobalParametersSerializer(serializers.Serializer):
     enable_mx_checks = serializers.BooleanField(default=True)
     valid_mxs = serializers.CharField(allow_blank=True)
     domains_must_have_authorized_mx = serializers.BooleanField(default=False)
+    enable_ipv6_mx_checks = serializers.BooleanField(default=True)
     enable_spf_checks = serializers.BooleanField(default=True)
     enable_dkim_checks = serializers.BooleanField(default=True)
     enable_dmarc_checks = serializers.BooleanField(default=True)
@@ -274,6 +275,8 @@ def validate(self, data):
             raise serializers.ValidationError({
                 "valid_mxs": _("Define at least one authorized network / address")
             })
+        if not data.get("enable_mx_checks"):
+            data["enable_ipv6_mx_checks"] = False
         return data
 
 
diff --git a/modoboa/admin/app_settings.py b/modoboa/admin/app_settings.py
index be18a0ab8..eb97ec8e4 100644
--- a/modoboa/admin/app_settings.py
+++ b/modoboa/admin/app_settings.py
@@ -50,6 +50,14 @@ class AdminParametersForm(param_forms.AdminParametersForm):
         )
     )
 
+    enable_ipv6_mx_checks = YesNoField(
+        label=gettext_lazy("Enable IPV6 checks"),
+        initial=True,
+        help_text=gettext_lazy(
+            "Check if MX setup for ipv6 is valid"
+        )
+    )
+
     enable_spf_checks = YesNoField(
         label=gettext_lazy("Enable SPF checks"),
         initial=True,
@@ -194,7 +202,8 @@ class AdminParametersForm(param_forms.AdminParametersForm):
     # Visibility rules
     visibility_rules = {
         "valid_mxs": "enable_mx_checks=True",
-        "domains_must_have_authorized_mx": "enable_mx_checks=True"
+        "domains_must_have_authorized_mx": "enable_mx_checks=True",
+        "enable_ipv6_mx_checks": "enable_mx_checks=True"
     }
 
     def __init__(self, *args, **kwargs):
@@ -292,6 +301,8 @@ def clean(self):
                 "valid_mxs",
                 _("Define at least one authorized network / address")
             )
+        if not cleaned_data.get("enable_mx_checks"):
+            cleaned_data["enable_ipv6_mx_checks"] = False
         return cleaned_data
 
 
@@ -322,6 +333,13 @@ def clean(self):
                     "does not use one of the defined addresses."
                 )
             }),
+            ("enable_ipv6_mx_checks", {
+                "label": gettext_lazy("Enable IPV6 checks"),
+                "display": "enable_mx_checks=true",
+                "help_text": gettext_lazy(
+                    "Check if MX setup for ipv6 is valid"
+                )
+            }),
             ("enable_spf_checks", {
                 "label": gettext_lazy("Enable SPF checks"),
                 "help_text": gettext_lazy(
diff --git a/modoboa/admin/lib.py b/modoboa/admin/lib.py
index cf1254feb..a1100cfc9 100644
--- a/modoboa/admin/lib.py
+++ b/modoboa/admin/lib.py
@@ -226,7 +226,10 @@ def get_domain_mx_list(domain):
     for dns_answer in dns_answers:
         mx_domain = dns_answer.exchange.to_unicode(
             omit_final_dot=True, idna_codec=IDNA_2008_UTS_46)
-        for rtype in ["A", "AAAA"]:
+        rtypes = ["A"]
+        if param_tools.get_global_parameter("enable_ipv6_mx_checks", app="admin"):
+            rtypes.append("AAAA")
+        for rtype in rtypes:
             ip_answers = get_dns_records(mx_domain, rtype, resolver)
             if not ip_answers:
                 continue
diff --git a/modoboa/dnstools/lib.py b/modoboa/dnstools/lib.py
index 5f92ab731..e9edbb7f1 100644
--- a/modoboa/dnstools/lib.py
+++ b/modoboa/dnstools/lib.py
@@ -5,6 +5,7 @@
 from django.utils.translation import gettext as _
 
 from modoboa.admin import lib as admin_lib
+from modoboa.parameters import tools as param_tools
 
 from . import constants
 
@@ -43,7 +44,10 @@ def get_dmarc_record(domain):
 
 def _get_simple_record(name):
     """We just want to know if name is declared."""
-    for rdtype in ["A", "CNAME", "AAAA"]:
+    rtypes = ["A", "CNAME"]
+    if param_tools.get_global_parameter("enable_ipv6_mx_checks", app="admin"):
+        rtypes.append("AAAA")
+    for rdtype in rtypes:
         records = admin_lib.get_dns_records(name, rdtype)
         if records is not None:
             break


Test patch:
diff --git a/modoboa/admin/tests/test_mx.py b/modoboa/admin/tests/test_mx.py
index c15f5ef95..a8718483d 100644
--- a/modoboa/admin/tests/test_mx.py
+++ b/modoboa/admin/tests/test_mx.py
@@ -10,7 +10,8 @@
 from django.urls import reverse
 from django.utils.translation import gettext as _
 
-from modoboa.core import factories as core_factories
+from modoboa.core import (
+    models as core_models, factories as core_factories)
 from modoboa.lib.tests import ModoTestCase
 from . import utils
 from .. import factories, models
@@ -171,6 +172,29 @@ def test_get_domain_mx_list_logging(self, mock_query, mock_ip_address):
              % {"domain": "bad-response.example.com", "addr": "000.0.0.0"}),
         )
 
+    @mock.patch("ipaddress.ip_address")
+    @mock.patch.object(dns.resolver.Resolver, "resolve")
+    def test_ipv6_logging(self, mock_query, mock_ip_address):
+        """Test to check that AAAA missing record is logged consistently."""
+        mock_query.side_effect = utils.mock_dns_query_result
+        mock_ip_address.side_effect = utils.mock_ip_address_result
+        localconfig = core_models.LocalConfig.objects.first()
+        localconfig.parameters.set_value("enable_ipv6_mx_checks", True, "admin")
+        localconfig.save()
+        with LogCapture("modoboa.admin") as log:
+            get_domain_mx_list("test3.com")
+        log.check(
+            ("modoboa.admin", "ERROR",
+                _("No AAAA record for %s")
+                % "mx3.example.com")
+        )
+        localconfig = core_models.LocalConfig.objects.first()
+        localconfig.parameters.set_value("enable_ipv6_mx_checks", False, "admin")
+        localconfig.save()
+        with LogCapture("modoboa.admin") as log2:
+            get_domain_mx_list("test3.com")
+        log2.check()
+
 
 @override_settings(DNSBL_PROVIDERS=["zen.spamhaus.org"])
 class DNSBLTestCase(ModoTestCase):
diff --git a/modoboa/admin/tests/utils.py b/modoboa/admin/tests/utils.py
index 0bce09b2c..cac41ecf2 100644
--- a/modoboa/admin/tests/utils.py
+++ b/modoboa/admin/tests/utils.py
@@ -43,7 +43,7 @@ def __init__(self):
 _IP_RECORDS = [_IPV4_RECORD_1, _IPV6_RECORD]
 
 _POSSIBLE_DNS_RESULTS = {
-    "test3.com": [_MX_RECORD_2],
+    "test3.com": [_MX_RECORD_3],
     "no-mx.example.com": NoAnswer(),
     "does-not-exist.example.com": NXDOMAIN(),
     "timeout.example.com": Timeout(),
@@ -79,12 +79,20 @@ def __init__(self):
     "000.0.0.0": ValueError(),
 }
 
+_POSSIBLE_DNS_IPV6_RESULTS = {
+    settings.REDIS_HOST: socket.getaddrinfo(settings.REDIS_HOST, settings.REDIS_PORT),
+    "mx3.example.com": NoAnswer()
+    }
+
 
 def mock_dns_query_result(qname, *args, **kwargs):
     if "MX" in args:
         if qname in _POSSIBLE_DNS_RESULTS:
             return get_mock_dns_query_response(_POSSIBLE_DNS_RESULTS, qname)
         return _MX_RECORDS
+    elif "AAAA" in args:
+        if qname in _POSSIBLE_DNS_IPV6_RESULTS:
+            return get_mock_dns_query_response(_POSSIBLE_DNS_IPV6_RESULTS, qname)
     if qname in _POSSIBLE_DNS_RESULTS_NO_MX:
         return get_mock_dns_query_response(_POSSIBLE_DNS_RESULTS_NO_MX, qname)
     return _IP_SET_RECORDS
diff --git a/modoboa/core/tests/test_views.py b/modoboa/core/tests/test_views.py
index da9d7199a..f60106438 100644
--- a/modoboa/core/tests/test_views.py
+++ b/modoboa/core/tests/test_views.py
@@ -51,6 +51,7 @@
     "core-ldap_admin_groups": "",
     "core-ldap_groups_search_base": "",
     "admin-enable_mx_checks": "True",
+    "admin-enable_ipv6_mx_checks": "True",
     "core-ldap_search_base": "",
     "admin-valid_mxs": "",
     "limits-deflt_user_domains_limit": "0",
@@ -264,6 +265,15 @@ def test_sms_settings_clean(self):
         self.assertIn(
             "sms_ovh_application_secret", errors["form_errors"])
 
+    def test_mx_check_settings_clean(self):
+        settings = SETTINGS_SAMPLE.copy()
+        settings["admin-enable_mx_checks"] = False
+        url = reverse("core:parameters")
+        response = self.client.post(url, settings, format="json")
+        self.assertEqual(response.status_code, 200)
+        self.assertFalse(
+            param_tools.get_global_parameter("enable_ipv6_mx_checks", "admin")
+        )
 
 class UserSettings(param_forms.UserParametersForm):
     """Stupid user settings form."""