diff --git a/modoboa/admin/management/commands/subcommands/_dnsbl.py b/modoboa/admin/management/commands/subcommands/_dnsbl.py index ff8695334..ff0a5d24c 100644 --- a/modoboa/admin/management/commands/subcommands/_dnsbl.py +++ b/modoboa/admin/management/commands/subcommands/_dnsbl.py @@ -8,10 +8,14 @@ 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): @@ -19,6 +23,18 @@ class CheckDNSBLCommand(BaseCommand): 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( @@ -37,6 +53,45 @@ class CheckDNSBLCommand(BaseCommand): 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() @@ -59,30 +114,24 @@ class CheckDNSBLCommand(BaseCommand): finally: if address is not None: ip_list.append(address) - print domain.name, ip_list if len(ip_list) == 0: return - if not hasattr(settings, "DNSBL_PROVIDERS"): - providers = constants.DNSBL_PROVIDERS - else: - providers = settings.DNSBL_PROVIDERS jobs = [ gevent.spawn(self.query, ip_list, provider) - for provider in providers] + for provider in self.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) + 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/migrations/0004_auto_20160608_1423.py b/modoboa/admin/migrations/0004_auto_20160608_1423.py index 98072136c..177e0a78c 100644 --- a/modoboa/admin/migrations/0004_auto_20160608_1423.py +++ b/modoboa/admin/migrations/0004_auto_20160608_1423.py @@ -17,9 +17,9 @@ class Migration(migrations.Migration): name='DNSBLResult', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('provider', models.CharField(max_length=254)), + ('provider', models.CharField(max_length=254, db_index=True)), ('mx', models.GenericIPAddressField()), - ('status', models.CharField(blank=True, max_length=45)), + ('status', models.CharField(blank=True, max_length=45, db_index=True)), ('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='admin.Domain')), ], ), diff --git a/modoboa/admin/models/domain.py b/modoboa/admin/models/domain.py index 277db2d40..d51a95d8d 100644 --- a/modoboa/admin/models/domain.py +++ b/modoboa/admin/models/domain.py @@ -232,9 +232,9 @@ class DNSBLResult(models.Model): """Store a DNSBL query result.""" domain = models.ForeignKey(Domain) - provider = models.CharField(max_length=254) + provider = models.CharField(max_length=254, db_index=True) mx = models.GenericIPAddressField() - status = models.CharField(max_length=45, blank=True) + 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_in_dnsbl.html b/modoboa/admin/templates/admin/notifications/domain_in_dnsbl.html new file mode 100644 index 000000000..337a18e6e --- /dev/null +++ b/modoboa/admin/templates/admin/notifications/domain_in_dnsbl.html @@ -0,0 +1,15 @@ +{% load i18n %} + +{% blocktrans trimmed domain=domain.name %} + Modoboa detected that domain {{ domain }} is listed by the following DNSBL providers: +{% endblocktrans %} + +{% for provider, ip in alerts %} + {{ provider }}: {{ ip }} +{% endfor %} + +{% blocktrans trimmed %} + The domain's reputation will be affected and there is a chance that + emails coming from it are considered as spam. You should contact + those providers and ask them to unlist detected IP address(es). +{% endblocktrans %} diff --git a/modoboa/core/app_settings.py b/modoboa/core/app_settings.py index ed93cc03a..c8ac1c080 100644 --- a/modoboa/core/app_settings.py +++ b/modoboa/core/app_settings.py @@ -4,6 +4,7 @@ from django.conf import settings from django.utils.translation import ugettext as _, ugettext_lazy from modoboa.lib import parameters +from modoboa.lib import fields as lib_fields from modoboa.lib.cryptutils import random_key from modoboa.lib.form_utils import ( YesNoField, SeparatorField, InlineRadioSelect @@ -209,6 +210,16 @@ class GeneralParametersForm(parameters.AdminParametersForm): required=False ) + notif_sep = SeparatorField(label=ugettext_lazy("Notifications")) + + sender_address = lib_fields.UTF8EmailField( + label=_("Sender address"), + initial="noreply@modoboa.org", + help_text=_( + "Email address used to send notifications." + ) + ) + sep3 = SeparatorField(label=ugettext_lazy("Miscellaneous")) top_notifications_check_interval = forms.IntegerField(