<NME> _mx.py
<BEF> """Management command to check defined domains."""

import ipaddress

import gevent
from gevent import socket

from django.conf import settings
from django.core.mail import EmailMessage
from django.core.management.base import BaseCommand
from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.encoding import smart_text
from django.utils.functional import cached_property
from django.utils.translation import ugettext as _

from modoboa.admin import constants, models
from modoboa.dnstools import models as dns_models
from modoboa.parameters import tools as param_tools


class CheckMXRecords(BaseCommand):
    """Command class."""

    help = "Check defined domains."  # NOQA:A003

    @cached_property
    def providers(self):
        """Return a list of DNSBL providers."""
        if not hasattr(settings, "DNSBL_PROVIDERS"):
            return constants.DNSBL_PROVIDERS
        return settings.DNSBL_PROVIDERS

    @cached_property
    def sender(self):
        """Return sender address for notifications."""
        return param_tools.get_global_parameter("sender_address", app="core")

    @cached_property
    def valid_mxs(self):
    def add_arguments(self, parser):
        """Add extra arguments to command."""
        parser.add_argument(
            "--valid-mx", type=str, action='append', default=[],
            help="Valid MX ip(s) or subnet(s) used to check that domain's MX "
                 "are your own.")
        parser.add_argument(
            "--no-dnsbl", action='store_true', default=False,
            help="Skip DNSBL queries.")
        parser.add_argument(
            "--email", type=str, action='append', default=[],
            help="One or more email to notify")
        parser.add_argument(
            "--skip-admin-emails", action='store_true',
            default=False,
            help="Skip domain's admins email notification.")
        parser.add_argument(
        parser.add_argument(
            "--domain", type=str, action="append", default=[],
            help="Domain name or id to update.")
        parser.add_argument(
            help="TTL for dns query.")

    def get_mx_records_for_domain(self, domain, ttl=7200):
        """Return one or more `models.MXRecord` for `domain`. DNS queries are
        not performed while `ttl` (in seconds) is still valid"""
        now = datetime.now()
        records = models.MXRecord.objects.filter(domain=domain,
                                                 updated__gt=now)
        if records.count():
            for record in records:
                yield record
            raise StopIteration()
        else:
            models.MXRecord.objects.filter(domain=domain).delete()

        try:
            answers = dns.resolver.query(domain.name, "MX")
        except dns.resolver.NoAnswer:
                result = socket.gethostbyname(pattern)
                #result from dnsbl is in ipv4 format
                splited_result = result.split(".")
                if int(splited_result[-1]) > 15:
                    #Typical dnsbl result : 127.0.0.[1-15] (depends on services)
                    result = False
            except socket.gaierror:
                result = False
            for mx in mxs:
                results[mx] = result
        return provider, results

    def store_dnsbl_result(self, domain, provider, results, **options):
        """Store DNSBL provider results for domain.

        Return a list of alerts.
                if address is not None:
                    record = models.MXRecord.objects.create(
                        domain=domain,
                        name=str(answer.exchange).strip('.'),
                        address=address,
                        updated=now + delta)
                    yield record
                domain=domain, provider=provider, mx=mx).first()
            trigger = False
            if dnsbl_result is None:
                to_create.append(
                    models.DNSBLResult(
                        domain=domain, provider=provider, mx=mx,
                        status=result)
                )
                if result:
                    trigger = True
            else:
                dnsbl_result.status = result
                dnsbl_result.save()
                if not dnsbl_result.status and result:
                    trigger = True
            alarm_name = "domain_mx_in_dnsbl_{}".format(provider)
            if trigger:
                title = _("MX {} listed by DNSBL provider {}").format(
                    mx.name, provider)
                domain.alarms.create(
                    internal_name=alarm_name,
                    status=constants.ALARM_OPENED,
                    title=title
                )
                alerts.append((provider, mx.name))
            else:
                domain.alarms.filter(
                    internal_name=alarm_name,
                    status=constants.ALARM_OPENED
                ).update(
                    status=constants.ALARM_CLOSED,
                    closed=timezone.now()
                )
        models.DNSBLResult.objects.bulk_create(to_create)
        models.DNSBLResult.objects.bulk_create(to_create)
        if not alerts:
            return
        emails = options['email']
        if not options['skip_admin_emails']:
            emails.extend([a.email for a in domain.admins if a.email])
        if emails:
            content = render_to_string(
                "admin/notifications/domain_in_dnsbl.html", {
                    "domain": domain, "alerts": alerts
                })
            subject = _("[modoboa] DNSBL issue(s) for domain {}").format(
                domain.name)
            for email in emails:
                status, msg = email_utils.sendmail_simple(
                    self.sender, email,
                    subject=subject, content=content)
                if not status:
                    print(msg)

    def check_valid_mx(self, domain, mx_list, valid_mx=None, **options):
        """Check that domain's MX record exist and is contains in `valid_mx` if
        the option is provided"""
        alerts = []
        check = False
        mxs = [ipaddress.ip_address(u"%s" % mx.address) for mx in mx_list]
        if valid_mx:
            for subnet in valid_mx:
                check = True in [mx in subnet for mx in mxs]
                if check is True:
                    break
        if not mxs:
            alerts.append(_("Domain {} as no MX record").format(domain))
        elif valid_mx and check is False:
            mx_names = ["{0.name} ({0.address})".format(mx) for mx in mx_list]
            args = (domain, ", ".join(mx_names))
            alerts.append(
                _("Domain {0} got an invalid MX record. Got {1}").format(*args)
            )
        if not alerts:
            return
        emails = options['email']
        if not options['skip_admin_emails']:
            emails.extend([a.email for a in domain.admins if a.email])
        if emails:
            content = render_to_string(
                "admin/notifications/domain_invalid_mx.html", {
                    "domain": domain, "valid_mx": valid_mx, "alerts": alerts
                })
            subject = _("[modoboa] MX issue(s) for domain {}").format(
                domain.name)
            for email in emails:
                status, msg = email_utils.sendmail_simple(
                    self.sender, email,
                    subject=subject, content=content)
                if not status:
                    print(msg)

    def check_domain(self, domain, timeout=3, ttl=7200, **options):
        """Check specified domain."""

        mx_list = list(self.get_mx_records_for_domain(domain, ttl=ttl))

        if parameters.get_admin("ENABLE_MX_CHECKS") != "no":
            self.check_valid_mx(domain, mx_list, **options)

        if parameters.get_admin("ENABLE_DNSBL_CHECKS") == "no":
            return
        elif options['no_dnsbl'] is True:
            return
        elif mx_list:
            jobs = [
                gevent.spawn(self.query_dnsbl, mx_list, provider)
                for provider in self.providers]
            gevent.joinall(jobs, timeout)
            for job in jobs:
                if not job.successful():
                    continue
                provider, results = job.value
                self.store_dnsbl_result(domain, provider, results, **options)

    def handle(self, *args, **options):
        """Command entry point."""
        # Check that user provide valid network addresses
        valid_mx = options['valid_mx']
        options['valid_mx'] = [ipaddress.ip_network(u"%s" % v)
                               for v in valid_mx]

        # Remove deprecated records first
        models.DNSBLResult.objects.exclude(
            dns_models.DNSRecord.objects.get_or_create_for_domain(
                domain, "dmarc", ttl)
        if param_tools.get_global_parameter("enable_autoconfig_checks"):
            dns_models.DNSRecord.objects.get_or_create_for_domain(
                domain, "autoconfig", ttl)
            dns_models.DNSRecord.objects.get_or_create_for_domain(
                domain, "autodiscover", ttl)

        condition = (
            not param_tools.get_global_parameter("enable_dnsbl_checks") or
            options["no_dnsbl"] is True)
        if condition or not mx_list:
            return

        mx_by_ip = {}
        for mx in mx_list:
            if mx.address not in mx_by_ip:
                mx_by_ip[mx.address] = [mx]
            elif mx not in mx_by_ip[mx.address]:
                mx_by_ip[mx.address].append(mx)

        jobs = [
            gevent.spawn(self.query_dnsbl, mx_by_ip, provider)
            for provider in self.providers]
        gevent.joinall(jobs, timeout)
        alerts = []
        for job in jobs:
            if not job.successful():
                continue
            provider, results = job.value
            alerts += self.store_dnsbl_result(
                domain, provider, results, **options)
        if not alerts:
            return
        subject = _("[modoboa] DNSBL issue(s) for domain {}").format(
            domain.name)
        tpl = "admin/notifications/domain_in_dnsbl.html"
        self.send_alert_notifications(domain, alerts, subject, tpl, **options)

    def handle(self, *args, **options):
        """Command entry point."""
        # Remove deprecated records first
        models.DNSBLResult.objects.exclude(
            provider__in=self.providers).delete()

        if options["domain"]:
            domains = []
            for domain in options["domain"]:
                try:
                    if domain.isdigit():
                        domains.append(models.Domain.objects.get(pk=domain))
                    else:
                        domains.append(models.Domain.objects.get(name=domain))
                except models.Domain.DoesNotExist:
                    pass
        else:
            domains = models.Domain.objects.filter(
                enabled=True, enable_dns_checks=True)

        options.pop("domain")

        for domain in domains:
            if domain.uses_a_reserved_tld:
                continue
            self.check_domain(domain, **options)

<MSG> Few changes.

<DFF> @@ -41,17 +41,17 @@ class CheckMXRecords(BaseCommand):
     def add_arguments(self, parser):
         """Add extra arguments to command."""
         parser.add_argument(
-            "--valid-mx", type=str, action='append', default=[],
+            "--valid-mx", type=str, action="append", default=[],
             help="Valid MX ip(s) or subnet(s) used to check that domain's MX "
                  "are your own.")
         parser.add_argument(
-            "--no-dnsbl", action='store_true', default=False,
+            "--no-dnsbl", action="store_true", default=False,
             help="Skip DNSBL queries.")
         parser.add_argument(
-            "--email", type=str, action='append', default=[],
+            "--email", type=str, action="append", default=[],
             help="One or more email to notify")
         parser.add_argument(
-            "--skip-admin-emails", action='store_true',
+            "--skip-admin-emails", action="store_true",
             default=False,
             help="Skip domain's admins email notification.")
         parser.add_argument(
@@ -62,18 +62,19 @@ class CheckMXRecords(BaseCommand):
             help="TTL for dns query.")
 
     def get_mx_records_for_domain(self, domain, ttl=7200):
-        """Return one or more `models.MXRecord` for `domain`. DNS queries are
-        not performed while `ttl` (in seconds) is still valid"""
+        """Return one or more `models.MXRecord` for `domain`.
+
+        DNS queries are not performed while `ttl` (in seconds) is still valid.
+        """
         now = datetime.now()
         records = models.MXRecord.objects.filter(domain=domain,
                                                  updated__gt=now)
-        if records.count():
+        if records.exists():
             for record in records:
                 yield record
             raise StopIteration()
-        else:
-            models.MXRecord.objects.filter(domain=domain).delete()
 
+        models.MXRecord.objects.filter(domain=domain).delete()
         try:
             answers = dns.resolver.query(domain.name, "MX")
         except dns.resolver.NoAnswer:
@@ -96,7 +97,7 @@ class CheckMXRecords(BaseCommand):
                 if address is not None:
                     record = models.MXRecord.objects.create(
                         domain=domain,
-                        name=str(answer.exchange).strip('.'),
+                        name=str(answer.exchange).strip("."),
                         address=address,
                         updated=now + delta)
                     yield record
@@ -137,90 +138,99 @@ class CheckMXRecords(BaseCommand):
         models.DNSBLResult.objects.bulk_create(to_create)
         if not alerts:
             return
-        emails = options['email']
-        if not options['skip_admin_emails']:
-            emails.extend([a.email for a in domain.admins if a.email])
-        if emails:
-            content = render_to_string(
-                "admin/notifications/domain_in_dnsbl.html", {
-                    "domain": domain, "alerts": alerts
-                })
-            subject = _("[modoboa] DNSBL issue(s) for domain {}").format(
-                domain.name)
-            for email in emails:
-                status, msg = email_utils.sendmail_simple(
-                    self.sender, email,
-                    subject=subject, content=content)
-                if not status:
-                    print(msg)
+        emails = options["email"]
+        if not options["skip_admin_emails"]:
+            emails.extend(
+                domain.admins.exclude(email="").values_list("email", flat=True)
+            )
+        if not len(emails):
+            return
+        content = render_to_string(
+            "admin/notifications/domain_in_dnsbl.html", {
+                "domain": domain, "alerts": alerts
+            })
+        subject = _("[modoboa] DNSBL issue(s) for domain {}").format(
+            domain.name)
+        for email in emails:
+            status, msg = email_utils.sendmail_simple(
+                self.sender, email,
+                subject=subject, content=content)
+            if not status:
+                print(msg)
 
     def check_valid_mx(self, domain, mx_list, valid_mx=None, **options):
-        """Check that domain's MX record exist and is contains in `valid_mx` if
-        the option is provided"""
+        """Check that domain's MX record exist.
+
+        If `valid_mx` is provided, retrieved MX records must be
+        contained in it.
+        """
         alerts = []
         check = False
         mxs = [ipaddress.ip_address(u"%s" % mx.address) for mx in mx_list]
-        if valid_mx:
+        if not mxs:
+            alerts.append(_("Domain {} as no MX record").format(domain))
+        elif valid_mx:
             for subnet in valid_mx:
                 check = True in [mx in subnet for mx in mxs]
                 if check is True:
                     break
-        if not mxs:
-            alerts.append(_("Domain {} as no MX record").format(domain))
-        elif valid_mx and check is False:
-            mx_names = ["{0.name} ({0.address})".format(mx) for mx in mx_list]
-            args = (domain, ", ".join(mx_names))
-            alerts.append(
-                _("Domain {0} got an invalid MX record. Got {1}").format(*args)
-            )
+            if check is False:
+                mx_names = [
+                    "{0.name} ({0.address})".format(mx) for mx in mx_list]
+                alerts.append(
+                    _("Domain {0} got an invalid MX record. Got {1}").format(
+                        domain, ", ".join(mx_names))
+                )
         if not alerts:
             return
-        emails = options['email']
-        if not options['skip_admin_emails']:
-            emails.extend([a.email for a in domain.admins if a.email])
-        if emails:
-            content = render_to_string(
-                "admin/notifications/domain_invalid_mx.html", {
-                    "domain": domain, "valid_mx": valid_mx, "alerts": alerts
-                })
-            subject = _("[modoboa] MX issue(s) for domain {}").format(
-                domain.name)
-            for email in emails:
-                status, msg = email_utils.sendmail_simple(
-                    self.sender, email,
-                    subject=subject, content=content)
-                if not status:
-                    print(msg)
+        emails = options["email"]
+        if not options["skip_admin_emails"]:
+            emails.extend(
+                domain.admins.exclude(email="").values_list("email", flat=True)
+            )
+        if not len(emails):
+            return
+        content = render_to_string(
+            "admin/notifications/domain_invalid_mx.html", {
+                "domain": domain, "valid_mx": valid_mx, "alerts": alerts
+            })
+        subject = _("[modoboa] MX issue(s) for domain {}").format(
+            domain.name)
+        for email in emails:
+            status, msg = email_utils.sendmail_simple(
+                self.sender, email,
+                subject=subject, content=content)
+            if not status:
+                print(msg)
 
     def check_domain(self, domain, timeout=3, ttl=7200, **options):
         """Check specified domain."""
-
         mx_list = list(self.get_mx_records_for_domain(domain, ttl=ttl))
 
         if parameters.get_admin("ENABLE_MX_CHECKS") != "no":
             self.check_valid_mx(domain, mx_list, **options)
 
-        if parameters.get_admin("ENABLE_DNSBL_CHECKS") == "no":
-            return
-        elif options['no_dnsbl'] is True:
+        condition = (
+            parameters.get_admin("ENABLE_DNSBL_CHECKS") == "no" or
+            options["no_dnsbl"] is True)
+        if condition or not mx_list:
             return
-        elif mx_list:
-            jobs = [
-                gevent.spawn(self.query_dnsbl, mx_list, provider)
-                for provider in self.providers]
-            gevent.joinall(jobs, timeout)
-            for job in jobs:
-                if not job.successful():
-                    continue
-                provider, results = job.value
-                self.store_dnsbl_result(domain, provider, results, **options)
+        jobs = [
+            gevent.spawn(self.query_dnsbl, mx_list, provider)
+            for provider in self.providers]
+        gevent.joinall(jobs, timeout)
+        for job in jobs:
+            if not job.successful():
+                continue
+            provider, results = job.value
+            self.store_dnsbl_result(domain, provider, results, **options)
 
     def handle(self, *args, **options):
         """Command entry point."""
         # Check that user provide valid network addresses
-        valid_mx = options['valid_mx']
-        options['valid_mx'] = [ipaddress.ip_network(u"%s" % v)
-                               for v in valid_mx]
+        valid_mx = options["valid_mx"]
+        options["valid_mx"] = [
+            ipaddress.ip_network(u"{}".format(v)) for v in valid_mx]
 
         # Remove deprecated records first
         models.DNSBLResult.objects.exclude(

Few changes.