diff --git a/modoboa/admin/app_settings.py b/modoboa/admin/app_settings.py index efe660930..f551790e3 100644 --- a/modoboa/admin/app_settings.py +++ b/modoboa/admin/app_settings.py @@ -53,6 +53,14 @@ class AdminParametersForm(parameters.AdminParametersForm): dom_sep = SeparatorField(label=ugettext_lazy("Domains")) + enable_mx_checks = YesNoField( + label=ugettext_lazy("Enable MX checks"), + initial="yes", + help_text=ugettext_lazy( + "Check that every domain has a valid MX record" + ) + ) + enable_dnsbl_checks = YesNoField( label=ugettext_lazy("Enable DNSBL checks"), initial="yes", diff --git a/modoboa/admin/management/commands/modo.py b/modoboa/admin/management/commands/modo.py index b5aba2f6d..e5b7d3b40 100644 --- a/modoboa/admin/management/commands/modo.py +++ b/modoboa/admin/management/commands/modo.py @@ -2,7 +2,7 @@ from subcommand.base import SubcommandCommand -from .subcommands._dnsbl import CheckDNSBLCommand +from .subcommands._mx import CheckMXRecords from .subcommands._export import ExportCommand from .subcommands._import import ImportCommand @@ -18,5 +18,5 @@ class Command(SubcommandCommand): subcommands = { "export": ExportCommand, "import": ImportCommand, - "check_dnsbl": CheckDNSBLCommand, + "check_mx": CheckMXRecords, } diff --git a/modoboa/admin/management/commands/subcommands/_dnsbl.py b/modoboa/admin/management/commands/subcommands/_dnsbl.py deleted file mode 100644 index ff0a5d24c..000000000 --- a/modoboa/admin/management/commands/subcommands/_dnsbl.py +++ /dev/null @@ -1,137 +0,0 @@ -"""Management command to check defined domains against DNSBL providers.""" - -import ipaddress - -import dns.resolver -import gevent -from gevent import socket - -from django.conf import settings -from django.core.management.base import BaseCommand -from django.template.loader import render_to_string -from django.utils.functional import cached_property -from django.utils.translation import ugettext as _ - -from modoboa.admin import constants -from modoboa.admin import models -from modoboa.lib import parameters -from modoboa.lib import email_utils - - -class CheckDNSBLCommand(BaseCommand): - """Command class.""" - - help = "Check defined domains against DNSBL providers." - - @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 parameters.get_admin("SENDER_ADDRESS", app="core") - - def add_arguments(self, parser): - """Add extra arguments to command.""" - parser.add_argument( - "--timeout", type=int, default=3, - help="Timeout used for queries.") - - def query(self, ip_list, provider): - """Check given IP against given provider.""" - results = {} - for ip in ip_list: - reverse = ".".join(reversed(ip.split("."))) - pattern = "{}.{}".format(reverse, provider) - try: - results[ip] = socket.gethostbyname(pattern) - except socket.gaierror: - results[ip] = False - return provider, results - - def store_domain_result(self, domain, provider, results): - """Store provider results for domain.""" - alerts = {} - to_create = [] - for ip in results.keys(): - result = "" if not results[ip] else results[ip] - dnsbl_result = models.DNSBLResult.objects.filter( - domain=domain, provider=provider, mx=ip).first() - if dnsbl_result is None: - to_create.append( - models.DNSBLResult( - domain=domain, provider=provider, mx=ip, - status=result) - ) - else: - if not dnsbl_result.status and result: - if domain not in alerts: - alerts[domain] = [] - alerts[domain].append((provider, ip)) - dnsbl_result.status = result - dnsbl_result.save() - models.DNSBLResult.objects.bulk_create(to_create) - if not alerts: - 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 admin in domain.admins: - if not admin.email: - continue - status, msg = email_utils.sendmail_simple( - self.sender, admin.email, - domainsubject=subject, content=content) - if not status: - print msg - - def check_domain(self, domain, timeout): - """Check specified domain.""" - resolver = dns.resolver.Resolver() - try: - answers = resolver.query(domain.name, "MX") - except dns.resolver.NoAnswer: - return - ip_list = [] - for answer in answers: - address = None - try: - ipaddress.ip_address(str(answer.exchange)) - except ValueError: - try: - address = socket.gethostbyname(str(answer.exchange)) - except socket.gaierror: - pass - else: - address = str(answer.exchange) - finally: - if address is not None: - ip_list.append(address) - if len(ip_list) == 0: - return - jobs = [ - gevent.spawn(self.query, ip_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_domain_result(domain, provider, results) - - def handle(self, *args, **options): - """Command entry point.""" - if parameters.get_admin("ENABLE_DNSBL_CHECKS") == "no": - return - # Remove deprecated records first - models.DNSBLResult.objects.exclude( - provider__in=self.providers).delete() - for domain in models.Domain.objects.filter(enabled=True): - self.check_domain(domain, options.get("timeout")) diff --git a/modoboa/admin/management/commands/subcommands/_mx.py b/modoboa/admin/management/commands/subcommands/_mx.py new file mode 100644 index 000000000..0b9a459af --- /dev/null +++ b/modoboa/admin/management/commands/subcommands/_mx.py @@ -0,0 +1,230 @@ +"""Management command to check defined domains.""" + +from datetime import datetime +from datetime import timedelta +import ipaddress + +import dns.resolver + +import gevent +from gevent import socket + +from django.conf import settings +from django.core.management.base import BaseCommand +from django.template.loader import render_to_string +from django.utils.functional import cached_property +from django.utils.translation import ugettext as _ + +from modoboa.admin import constants +from modoboa.admin import models +from modoboa.lib import parameters +from modoboa.lib import email_utils + + +class CheckMXRecords(BaseCommand): + """Command class.""" + + help = "Check defined domains." + + @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 parameters.get_admin("SENDER_ADDRESS", app="core") + + 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( + "--timeout", type=int, default=3, + help="Timeout used for queries.") + parser.add_argument( + "--ttl", type=int, default=7200, + 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: + raise StopIteration() + + delta = timedelta(seconds=ttl) + + for answer in answers: + address = None + try: + ipaddress.ip_address(str(answer.exchange)) + except ValueError: + try: + address = socket.gethostbyname(str(answer.exchange)) + except socket.gaierror: + pass + else: + address = str(answer.exchange) + finally: + if address is not None: + record = models.MXRecord.objects.create( + domain=domain, + name=str(answer.exchange).strip('.'), + address=address, + updated=now + delta) + yield record + + def query_dnsbl(self, mx_list, provider): + """Check given IP against given DNSBL provider.""" + results = {} + for mx in mx_list: + reverse = ".".join(reversed(mx.address.split("."))) + pattern = "{}.{}".format(reverse, provider) + try: + results[mx] = socket.gethostbyname(pattern) + except socket.gaierror: + results[mx] = False + return provider, results + + def store_dnsbl_result(self, domain, provider, results, **options): + """Store DNSBL provider results for domain.""" + alerts = {} + to_create = [] + for mx in results.keys(): + result = "" if not results[mx] else results[mx] + dnsbl_result = models.DNSBLResult.objects.filter( + domain=domain, provider=provider, mx=mx).first() + if dnsbl_result is None: + to_create.append( + models.DNSBLResult( + domain=domain, provider=provider, mx=mx, + status=result) + ) + else: + if not dnsbl_result.status and result: + if domain not in alerts: + alerts[domain] = [] + alerts[domain].append((provider, mx)) + dnsbl_result.status = result + dnsbl_result.save() + 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( + provider__in=self.providers).delete() + + for domain in models.Domain.objects.filter(enabled=True): + self.check_domain(domain, **options) diff --git a/modoboa/admin/migrations/0004_auto_20160608_1423.py b/modoboa/admin/migrations/0004_auto_20160608_1423.py deleted file mode 100644 index 177e0a78c..000000000 --- a/modoboa/admin/migrations/0004_auto_20160608_1423.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.5 on 2016-06-08 12:23 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('admin', '0003_auto_20151118_1215'), - ] - - operations = [ - migrations.CreateModel( - name='DNSBLResult', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('provider', models.CharField(max_length=254, db_index=True)), - ('mx', models.GenericIPAddressField()), - ('status', models.CharField(blank=True, max_length=45, db_index=True)), - ('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='admin.Domain')), - ], - ), - migrations.AlterUniqueTogether( - name='dnsblresult', - unique_together=set([('domain', 'provider', 'mx')]), - ), - ] diff --git a/modoboa/admin/migrations/0004_auto_20160914_0907.py b/modoboa/admin/migrations/0004_auto_20160914_0907.py new file mode 100644 index 000000000..32aa5e148 --- /dev/null +++ b/modoboa/admin/migrations/0004_auto_20160914_0907.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.9 on 2016-09-14 09:07 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('admin', '0003_auto_20151118_1215'), + ] + + operations = [ + migrations.CreateModel( + name='DNSBLResult', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('provider', models.CharField(db_index=True, max_length=254)), + ('status', models.CharField(blank=True, db_index=True, max_length=45)), + ('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='admin.Domain')), + ], + ), + migrations.CreateModel( + name='MXRecord', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=254)), + ('address', models.GenericIPAddressField()), + ('managed', models.BooleanField(default=False)), + ('updated', models.DateTimeField()), + ('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='admin.Domain')), + ], + ), + migrations.CreateModel( + name='SenderAddress', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('address', models.EmailField(max_length=254)), + ('mailbox', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='admin.Mailbox')), + ], + ), + migrations.AddField( + model_name='alias', + name='description', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='alias', + name='expire_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='dnsblresult', + name='mx', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='admin.MXRecord'), + ), + migrations.AlterUniqueTogether( + name='senderaddress', + unique_together=set([('address', 'mailbox')]), + ), + migrations.AlterUniqueTogether( + name='dnsblresult', + unique_together=set([('domain', 'provider', 'mx')]), + ), + ] diff --git a/modoboa/admin/migrations/0005_auto_20160719_0903.py b/modoboa/admin/migrations/0005_auto_20160719_0903.py deleted file mode 100644 index 343ac82b1..000000000 --- a/modoboa/admin/migrations/0005_auto_20160719_0903.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.5 on 2016-07-19 07:03 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('admin', '0004_auto_20160608_1423'), - ] - - operations = [ - migrations.AddField( - model_name='alias', - name='description', - field=models.TextField(blank=True), - ), - migrations.AddField( - model_name='alias', - name='expire_at', - field=models.DateTimeField(blank=True, null=True), - ), - ] diff --git a/modoboa/admin/migrations/0006_auto_20160910_1030.py b/modoboa/admin/migrations/0006_auto_20160910_1030.py deleted file mode 100644 index 75c5bd8f5..000000000 --- a/modoboa/admin/migrations/0006_auto_20160910_1030.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.5 on 2016-09-10 08:30 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('admin', '0005_auto_20160719_0903'), - ] - - operations = [ - migrations.CreateModel( - name='SenderAddress', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('address', models.EmailField(max_length=254)), - ('mailbox', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='admin.Mailbox')), - ], - ), - migrations.AlterUniqueTogether( - name='senderaddress', - unique_together=set([('address', 'mailbox')]), - ), - ] diff --git a/modoboa/admin/models/__init__.py b/modoboa/admin/models/__init__.py index 29205eec3..07a1569ea 100644 --- a/modoboa/admin/models/__init__.py +++ b/modoboa/admin/models/__init__.py @@ -1,7 +1,7 @@ """Admin models.""" from .base import AdminObject -from .domain import Domain, DNSBLResult +from .domain import Domain, MXRecord, DNSBLResult from .domain_alias import DomainAlias from .mailbox import Mailbox, Quota, MailboxOperation, SenderAddress from .alias import Alias, AliasRecipient @@ -15,6 +15,7 @@ __all__ = [ "DomainAlias", "Mailbox", "MailboxOperation", + "MXRecord", "Quota", "SenderAddress", ] diff --git a/modoboa/admin/models/domain.py b/modoboa/admin/models/domain.py index d51a95d8d..fd6d39f3a 100644 --- a/modoboa/admin/models/domain.py +++ b/modoboa/admin/models/domain.py @@ -102,6 +102,10 @@ class Domain(AdminObject): def aliases(self): return self.domainalias_set + @property + def mx_records(self): + self.mxrecord_set + @cached_property def dnsbl_status_color(self): """Shortcut to DNSBL results.""" @@ -220,6 +224,18 @@ class Domain(AdminObject): reversion.register(Domain) +class MXRecord(models.Model): + + domain = models.ForeignKey(Domain) + name = models.CharField(max_length=254) + address = models.GenericIPAddressField() + managed = models.BooleanField(default=False) + updated = models.DateTimeField() + + def __unicode__(self): + return u'{0.name} ({0.address}) for {0.domain} '.format(self) + + class DNSBLQuerySet(models.QuerySet): """Custom manager for DNSBLResultManager.""" @@ -233,7 +249,7 @@ class DNSBLResult(models.Model): domain = models.ForeignKey(Domain) provider = models.CharField(max_length=254, db_index=True) - mx = models.GenericIPAddressField() + mx = models.ForeignKey(MXRecord) status = models.CharField(max_length=45, blank=True, db_index=True) objects = models.Manager.from_queryset(DNSBLQuerySet)() diff --git a/modoboa/admin/templates/admin/notifications/domain_invalid_mx.html b/modoboa/admin/templates/admin/notifications/domain_invalid_mx.html new file mode 100644 index 000000000..34abbf230 --- /dev/null +++ b/modoboa/admin/templates/admin/notifications/domain_invalid_mx.html @@ -0,0 +1,9 @@ +{% load i18n %} + +{% blocktrans trimmed with domain=domain.name %} + Modoboa detected that domain {{ domain }} has no valid MX record: +{% endblocktrans %} + +{% for alert in alerts %} + - {{ alert }} +{% endfor %} diff --git a/modoboa/admin/tests/test_dnsbl.py b/modoboa/admin/tests/test_dnsbl.py deleted file mode 100644 index 57a5e9772..000000000 --- a/modoboa/admin/tests/test_dnsbl.py +++ /dev/null @@ -1,32 +0,0 @@ -"""DNSBL related tests.""" - -from django.core import management -from django.core.urlresolvers import reverse -from django.test import override_settings - -from modoboa.lib.tests import ModoTestCase - -from .. import factories -from .. import models - - -class DNSBLTestCase(ModoTestCase): - """TestCase for DNSBL related features.""" - - @classmethod - def setUpTestData(cls): - """Create some data.""" - super(DNSBLTestCase, cls).setUpTestData() - cls.domain = factories.DomainFactory(name="modoboa.org") - factories.DomainFactory(name="pouet.com") # should not exist - - @override_settings(DNSBL_PROVIDERS=["zen.spamhaus.org"]) - def test_management_command(self): - """Check that command works fine.""" - self.assertEqual(models.DNSBLResult.objects.count(), 0) - management.call_command("modo", "check_dnsbl") - self.assertTrue( - models.DNSBLResult.objects.filter(domain=self.domain).exists()) - response = self.client.get( - reverse("admin:dnsbl_domain_detail", args=[self.domain.pk])) - self.assertEqual(response.status_code, 200) diff --git a/modoboa/admin/tests/test_mx.py b/modoboa/admin/tests/test_mx.py new file mode 100644 index 000000000..7fccef4a5 --- /dev/null +++ b/modoboa/admin/tests/test_mx.py @@ -0,0 +1,78 @@ +"""DNSBL related tests.""" + +from django.core import management +from django.core.urlresolvers import reverse +from django.test import override_settings + +from modoboa.lib.tests import ModoTestCase + +from .. import factories +from .. import models + + +class MXTestCase(ModoTestCase): + """TestCase for DNSBL related features.""" + + @classmethod + def setUpTestData(cls): + """Create some data.""" + super(MXTestCase, cls).setUpTestData() + cls.domain = factories.DomainFactory(name="modoboa.org") + factories.DomainFactory(name="pouet.com") # should not exist + + def test_management_command(self): + """Check that command works fine.""" + self.assertEqual(models.MXRecord.objects.count(), 0) + management.call_command("modo", "check_mx", "--no-dnsbl", + "--valid-mx=127.0.0.1", "--ttl=0") + self.assertTrue( + models.MXRecord.objects.filter(domain=self.domain).exists()) + + # we passed a ttl to 0. this will reset the cache this time + qs = models.MXRecord.objects.filter(domain=self.domain) + id = qs[0].id + management.call_command("modo", "check_mx", "--no-dnsbl", + "--valid-mx=127.0.0.1", "--ttl=7200") + qs = models.MXRecord.objects.filter(domain=self.domain) + self.assertNotEqual(id, qs[0].id) + id = qs[0].id + + # assume that mxrecords ids are the same. means that we taking care of + # ttl + management.call_command("modo", "check_mx", "--no-dnsbl", + "--valid-mx=127.0.0.1") + qs = models.MXRecord.objects.filter(domain=self.domain) + self.assertEqual(id, qs[0].id) + + +class DNSBLTestCase(ModoTestCase): + """TestCase for DNSBL related features.""" + + @classmethod + def setUpTestData(cls): + """Create some data.""" + super(DNSBLTestCase, cls).setUpTestData() + cls.domain = factories.DomainFactory(name="modoboa.org") + factories.DomainFactory(name="pouet.com") # should not exist + + @override_settings(DNSBL_PROVIDERS=["zen.spamhaus.org"]) + def test_management_command(self): + """Check that command works fine.""" + self.assertEqual(models.DNSBLResult.objects.count(), 0) + management.call_command("modo", "check_mx") + self.assertTrue( + models.DNSBLResult.objects.filter(domain=self.domain).exists()) + response = self.client.get( + reverse("admin:dnsbl_domain_detail", args=[self.domain.pk])) + self.assertEqual(response.status_code, 200) + + @override_settings(DNSBL_PROVIDERS=["zen.spamhaus.org"]) + def test_management_command_no_dnsbl(self): + """Check that command works fine without dnsbl.""" + self.assertEqual(models.DNSBLResult.objects.count(), 0) + management.call_command("modo", "check_mx", "--no-dnsbl") + self.assertFalse( + models.DNSBLResult.objects.filter(domain=self.domain).exists()) + response = self.client.get( + reverse("admin:dnsbl_domain_detail", args=[self.domain.pk])) + self.assertEqual(response.status_code, 200)