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 %} + + + + + + + + + + {% for result in domain.dnsblresult_set.blacklisted %} + + + + + + {% endfor %} + +
{% trans "Provider" %}{% trans "Address" %}{% trans "Status" %}
{{ result.provider }}{{ result.mx }} + {% if result.status %}{% trans "listed" %}{% else %}{% endif %}
+ {% else %} +
+ {% trans "Everything is fine, this domain is not listed." %}
+ {% endif %} +{% endblock %} diff --git a/modoboa/admin/templates/admin/domain_detail.html b/modoboa/admin/templates/admin/domain_detail.html index adfc9eef4..cbac67289 100644 --- a/modoboa/admin/templates/admin/domain_detail.html +++ b/modoboa/admin/templates/admin/domain_detail.html @@ -17,9 +17,17 @@
-

{% trans "Information" %}

+
+

{% trans "Information" %}

+
-
{% trans "Creation date" %}{{ domain.creation }}
+
+ {% trans "Authentication" %} + DNSBL +
+
+ {% trans "Creation date" %}{{ domain.creation }} +
{% trans "Last modification date" %}{{ domain.last_modification }}
{% trans "Domain aliases" %}{{ domain.domainalias_count }}
{% trans "Mailboxes" %}{{ domain.mailbox_count }}
diff --git a/modoboa/admin/templates/admin/domains.html b/modoboa/admin/templates/admin/domains.html index ec4025f0c..1e0b5e390 100644 --- a/modoboa/admin/templates/admin/domains.html +++ b/modoboa/admin/templates/admin/domains.html @@ -25,5 +25,6 @@ $(document).ready(function() { {% trans "Name" %} {% trans "Alias(es)" %} {% trans "Tags" %} +{% trans "Authentication" %} {% trans "Actions" %} {% endblock %} diff --git a/modoboa/admin/templates/admin/domains_table.html b/modoboa/admin/templates/admin/domains_table.html index b579c4070..c209dee64 100644 --- a/modoboa/admin/templates/admin/domains_table.html +++ b/modoboa/admin/templates/admin/domains_table.html @@ -5,6 +5,11 @@ {{ domain }}{% if not domain.enabled %} ({% trans "disabled" %}){% endif %} {% domain_aliases domain %} {% render_tags domain.tags %} + + + DNSBL + + {% domain_actions user domain %} {% endfor %} diff --git a/modoboa/admin/urls.py b/modoboa/admin/urls.py index cffbc927b..fb5ae7376 100644 --- a/modoboa/admin/urls.py +++ b/modoboa/admin/urls.py @@ -12,6 +12,8 @@ urlpatterns = patterns( url(r'^domains/$', 'domains', name="domain_list"), url(r'^domains/(?P\d+)/$', views.DomainDetailView.as_view(), name="domain_detail"), + url(r'^domains/(?P\d+)/dnsbl/$', views.DNSBLDomainDetailView.as_view(), + name="dnsbl_domain_detail"), url(r'^domains/list/$', '_domains', name="_domain_list"), url(r'^domains/flatlist/$', 'domains_list', name="domain_flat_list"), url(r'^domains/stats/$', 'domain_statistics', name="domain_statistics"), diff --git a/modoboa/admin/views/__init__.py b/modoboa/admin/views/__init__.py index dacf92012..008fb6858 100644 --- a/modoboa/admin/views/__init__.py +++ b/modoboa/admin/views/__init__.py @@ -1,8 +1,10 @@ """Shortcuts.""" +from .dnsbl import DNSBLDomainDetailView from .domain import DomainDetailView __all__ = [ + "DNSBLDomainDetailView", "DomainDetailView", ] diff --git a/modoboa/admin/views/dnsbl.py b/modoboa/admin/views/dnsbl.py new file mode 100644 index 000000000..dad479483 --- /dev/null +++ b/modoboa/admin/views/dnsbl.py @@ -0,0 +1,32 @@ +"""DNSBL related views.""" + +from django.views import generic + +from django.utils.translation import ugettext as _ + +from django.contrib.auth import mixins as auth_mixins + +from .. import models + + +class DNSBLDomainDetailView( + auth_mixins.PermissionRequiredMixin, generic.DetailView): + """DetailView for Domain.""" + + model = models.Domain + permission_required = "admin.view_domain" + template_name = "admin/dnsbl_domain_detail.html" + + def get_queryset(self): + """Add some prefetching.""" + return ( + super(DNSBLDomainDetailView, self).get_queryset() + .prefetch_related("dnsblresult_set")) + + def get_context_data(self, **kwargs): + """Add extra variables.""" + context = super(DNSBLDomainDetailView, self).get_context_data(**kwargs) + context.update({ + "title": _("DNSBL summary for {}").format(self.object.name) + }) + return context diff --git a/modoboa/admin/views/domain.py b/modoboa/admin/views/domain.py index 622d529b5..4bc5039cf 100644 --- a/modoboa/admin/views/domain.py +++ b/modoboa/admin/views/domain.py @@ -164,6 +164,13 @@ class DomainDetailView( model = Domain permission_required = "admin.view_domain" + def get_queryset(self): + """Add some prefetching.""" + return ( + super(DomainDetailView, self).get_queryset() + .prefetch_related("domainalias_set", "mailbox_set", "alias_set") + ) + def get_context_data(self, **kwargs): """Include extra widgets.""" context = super(DomainDetailView, self).get_context_data(**kwargs) diff --git a/requirements.txt b/requirements.txt index f8abf92e7..5f8ef1932 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,4 +15,5 @@ progressbar==2.3 djangorestframework==3.3.1 django-rest-swagger==0.3.5 django-subcommand2==0.1.1 - +gevent==1.1.1 +ipaddress