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/subcommands/_mx.py b/modoboa/admin/management/commands/subcommands/_mx.py index 0ff974be4..04dc42223 100644 --- a/modoboa/admin/management/commands/subcommands/_mx.py +++ b/modoboa/admin/management/commands/subcommands/_mx.py @@ -1,8 +1,8 @@ """Management command to check defined domains.""" -import ipaddress from datetime import datetime from datetime import timedelta +import ipaddress import dns.resolver @@ -42,10 +42,18 @@ class CheckMXRecords(BaseCommand): """Add extra arguments to command.""" parser.add_argument( "--valid-mx", type=str, action='append', default=[], - help="Valid MX ips.") + 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.") @@ -54,6 +62,8 @@ 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""" try: answers = dns.resolver.query(domain.name, "MX") except dns.resolver.NoAnswer: @@ -88,7 +98,7 @@ class CheckMXRecords(BaseCommand): yield record def query_dnsbl(self, mx_list, provider): - """Check given IP against given provider.""" + """Check given IP against given DNSBL provider.""" results = {} for mx in mx_list: reverse = ".".join(reversed(mx.address.split("."))) @@ -99,8 +109,8 @@ class CheckMXRecords(BaseCommand): results[mx] = False return provider, results - def store_dnsbl_result(self, domain, provider, results): - """Store provider results for domain.""" + def store_dnsbl_result(self, domain, provider, results, **options): + """Store DNSBL provider results for domain.""" alerts = {} to_create = [] for mx in results.keys(): @@ -123,64 +133,74 @@ class CheckMXRecords(BaseCommand): 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, - subject=subject, content=content) - if not status: - print(msg) + 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 - mx_names = [mx.name for mx in mx_list] - mxs = mx_names + [mx.address for mx in mx_list] + mxs = [ipaddress.ip_address(u"%s" % mx.address) for mx in mx_list] if valid_mx: - check = True in [mx in mxs for mx in 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)) + alerts.append(_("Domain {} as no MX record").format(domain)) elif valid_mx and check is False: - args = (domain, ', '.join(mx_names)) + 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) + _("Domain {0} got an invalid MX record. Got {1}").format(*args) ) if not alerts: 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 admin in domain.admins: - if not admin.email: - continue - status, msg = email_utils.sendmail_simple( - self.sender, admin.email, - subject=subject, content=content) - if not status: - print(msg) + 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)) - self.check_valid_mx(domain, mx_list, **options) + 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 - else: + elif mx_list: jobs = [ gevent.spawn(self.query_dnsbl, mx_list, provider) for provider in self.providers] @@ -189,10 +209,15 @@ class CheckMXRecords(BaseCommand): if not job.successful(): continue provider, results = job.value - self.store_dnsbl_result(domain, provider, results) + 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() 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/migrations/0007_mxrecord.py b/modoboa/admin/migrations/0007_mxrecord.py deleted file mode 100644 index 759b5b203..000000000 --- a/modoboa/admin/migrations/0007_mxrecord.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.9 on 2016-09-13 09:16 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('admin', '0006_auto_20160910_1030'), - ] - - operations = [ - 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')), - ], - ), - ] diff --git a/modoboa/admin/migrations/0008_auto_20160913_1414.py b/modoboa/admin/migrations/0008_auto_20160913_1414.py deleted file mode 100644 index 53e6d69bb..000000000 --- a/modoboa/admin/migrations/0008_auto_20160913_1414.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.9 on 2016-09-13 14:14 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('admin', '0007_mxrecord'), - ] - - operations = [ - migrations.AlterField( - model_name='dnsblresult', - name='mx', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='admin.MXRecord'), - ), - ]