diff --git a/modoboa/admin/constants.py b/modoboa/admin/constants.py new file mode 100644 index 000000000..ce41092be --- /dev/null +++ b/modoboa/admin/constants.py @@ -0,0 +1,83 @@ +"""Admin constants.""" + +DNSBL_PROVIDERS = [ + "aspews.ext.sorbs.net", + "b.barracudacentral.org", + "bad.psky.me", + "bl.deadbeef.com", + "bl.emailbasura.org", + "bl.spamcannibal.org", + "bl.spamcop.net", + "blackholes.five-ten-sg.com", + "blacklist.woody.ch", + "bogons.cymru.com", + "cbl.abuseat.org", + "cdl.anti-spam.org.cn", + "combined.abuse.ch", + "combined.rbl.msrbl.net", + "db.wpbl.info", + "dnsbl-1.uceprotect.net", + "dnsbl-2.uceprotect.net", + "dnsbl-3.uceprotect.net", + "dnsbl.cyberlogic.net", + "dnsbl.inps.de", + "dnsbl.njabl.org", + "dnsbl.sorbs.net", + "drone.abuse.ch", + "duinv.aupads.org", + "dul.dnsbl.sorbs.net", + "dul.ru", + "dyna.spamrats.com", + "dynip.rothen.com", + "http.dnsbl.sorbs.net" + "images.rbl.msrbl.net", + "ips.backscatterer.org", + "ix.dnsbl.manitu.net", + "korea.services.net", + "misc.dnsbl.sorbs.net", + "noptr.spamrats.com", + "ohps.dnsbl.net.au", + "omrs.dnsbl.net.au", + "orvedb.aupads.org", + "osps.dnsbl.net.au", + "osrs.dnsbl.net.au", + "owfs.dnsbl.net.au", + "owps.dnsbl.net.au" + "pbl.spamhaus.org", + "phishing.rbl.msrbl.net", + "probes.dnsbl.net.au" + "proxy.bl.gweep.ca", + "proxy.block.transip.nl", + "psbl.surriel.com", + "rbl.interserver.net", + "rdts.dnsbl.net.au", + "relays.bl.gweep.ca", + "relays.bl.kundenserver.de", + "relays.nether.net", + "residential.block.transip.nl", + "ricn.dnsbl.net.au", + "rmst.dnsbl.net.au", + "sbl.spamhaus.org", + "short.rbl.jp", + "smtp.dnsbl.sorbs.net", + "socks.dnsbl.sorbs.net", + "spam.abuse.ch", + "spam.dnsbl.sorbs.net", + "spam.rbl.msrbl.net", + "spam.spamrats.com", + "spamlist.or.kr", + "spamrbl.imp.ch", + "t3direct.dnsbl.net.au", + "tor.dnsbl.sectoor.de", + "torserver.tor.dnsbl.sectoor.de", + "ubl.lashback.com", + "ubl.unsubscore.com", + "virbl.bit.nl", + "virus.rbl.jp", + "virus.rbl.msrbl.net", + "web.dnsbl.sorbs.net", + "wormrbl.imp.ch", + "xbl.spamhaus.org", + "zen.spamhaus.org", + "zombie.dnsbl.sorbs.net", +] diff --git a/modoboa/admin/lib.py b/modoboa/admin/lib.py index 075663131..932a1504e 100644 --- a/modoboa/admin/lib.py +++ b/modoboa/admin/lib.py @@ -84,7 +84,8 @@ def get_domains(user, domfilter=None, searchquery=None, **extrafilters): :rtype: list :return: a list of domains and/or relay domains """ - domains = Domain.objects.get_for_admin(user) + domains = ( + Domain.objects.get_for_admin(user).prefetch_related("domainalias_set")) if domfilter: domains = domains.filter(type=domfilter) if searchquery is not None: diff --git a/modoboa/admin/management/commands/modo.py b/modoboa/admin/management/commands/modo.py index 36f4469ac..b5aba2f6d 100644 --- a/modoboa/admin/management/commands/modo.py +++ b/modoboa/admin/management/commands/modo.py @@ -2,6 +2,7 @@ from subcommand.base import SubcommandCommand +from .subcommands._dnsbl import CheckDNSBLCommand from .subcommands._export import ExportCommand from .subcommands._import import ImportCommand @@ -17,4 +18,5 @@ class Command(SubcommandCommand): subcommands = { "export": ExportCommand, "import": ImportCommand, + "check_dnsbl": CheckDNSBLCommand, } diff --git a/modoboa/admin/management/commands/subcommands/_dnsbl.py b/modoboa/admin/management/commands/subcommands/_dnsbl.py new file mode 100644 index 000000000..79a824586 --- /dev/null +++ b/modoboa/admin/management/commands/subcommands/_dnsbl.py @@ -0,0 +1,75 @@ +"""Management command to check defined domains against DNSBL providers.""" + +import ipaddress + +import dns.resolver +import gevent +from gevent import socket + +from django.core.management.base import BaseCommand + +from modoboa.admin import constants +from modoboa.admin import models + + +class CheckDNSBLCommand(BaseCommand): + """Command class.""" + + help = "Check defined domains against DNSBL providers." + + 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 check_domain(self, domain, timeout): + """Check specified domain.""" + resolver = dns.resolver.Resolver() + answers = resolver.query(domain.name, "MX") + 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) + + jobs = [ + gevent.spawn(self.query, ip_list, provider) + for provider in constants.DNSBL_PROVIDERS] + gevent.joinall(jobs, timeout) + for job in jobs: + if not job.successful(): + continue + provider, results = job.value + for ip in results.keys(): + result = "" if not results[ip] else results[ip] + models.DNSBLResult.objects.update_or_create( + domain=domain, provider=provider, mx=ip, + status=result) + + def handle(self, *args, **options): + """Command entry point.""" + for domain in models.Domain.objects.filter(enabled=True): + self.check_domain(domain, options.get("timeout")) diff --git a/modoboa/admin/migrations/0004_auto_20160608_1423.py b/modoboa/admin/migrations/0004_auto_20160608_1423.py new file mode 100644 index 000000000..98072136c --- /dev/null +++ b/modoboa/admin/migrations/0004_auto_20160608_1423.py @@ -0,0 +1,30 @@ +# -*- 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)), + ('mx', models.GenericIPAddressField()), + ('status', models.CharField(blank=True, max_length=45)), + ('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/models/__init__.py b/modoboa/admin/models/__init__.py index c2c223752..0fdc800d3 100644 --- a/modoboa/admin/models/__init__.py +++ b/modoboa/admin/models/__init__.py @@ -1,10 +1,10 @@ from .base import AdminObject -from .domain import Domain +from .domain import Domain, DNSBLResult from .domain_alias import DomainAlias from .mailbox import Mailbox, Quota, MailboxOperation from .alias import Alias, AliasRecipient __all__ = [ - 'Domain', 'DomainAlias', 'Mailbox', 'Quota', 'Alias', + 'Domain', 'DNSBLResult', 'DomainAlias', 'Mailbox', 'Quota', 'Alias', 'MailboxOperation', 'AdminObject', 'AliasRecipient' ] diff --git a/modoboa/admin/models/domain.py b/modoboa/admin/models/domain.py index 8134fe401..2dc24b73d 100644 --- a/modoboa/admin/models/domain.py +++ b/modoboa/admin/models/domain.py @@ -101,6 +101,10 @@ class Domain(AdminObject): def aliases(self): return self.domainalias_set + def is_in_dnsbl(self): + """Shortcut to DNSBL results.""" + return self.dnsblresult_set.blacklisted().exists() + def add_admin(self, account): """Add a new administrator to this domain. @@ -207,3 +211,26 @@ class Domain(AdminObject): domalias.post_create(creator) reversion.register(Domain) + + +class DNSBLQuerySet(models.QuerySet): + """Custom manager for DNSBLResultManager.""" + + def blacklisted(self): + """Return blacklisted results.""" + return self.exclude(status="") + + +class DNSBLResult(models.Model): + """Store a DNSBL query result.""" + + domain = models.ForeignKey(Domain) + provider = models.CharField(max_length=254) + mx = models.GenericIPAddressField() + status = models.CharField(max_length=45, blank=True) + + objects = models.Manager.from_queryset(DNSBLQuerySet)() + + class Meta: + app_label = "admin" + unique_together = [("domain", "provider", "mx")] diff --git a/modoboa/admin/templates/admin/dnsbl_domain_detail.html b/modoboa/admin/templates/admin/dnsbl_domain_detail.html new file mode 100644 index 000000000..d68210708 --- /dev/null +++ b/modoboa/admin/templates/admin/dnsbl_domain_detail.html @@ -0,0 +1,30 @@ +{% extends "common/generic_modal.html" %} + +{% load i18n %} + +{% block modalbody %} + {% if domain.is_in_dnsbl %} +
| {% trans "Provider" %} | +{% trans "Address" %} | +{% trans "Status" %} | +
|---|---|---|
| {{ result.provider }} | +{{ result.mx }} | ++ {% if result.status %}{% trans "listed" %}{% else %}{% endif %} | +