diff --git a/modoboa/core/app_settings.py b/modoboa/core/app_settings.py index fa254a47d..6cdc3bc86 100644 --- a/modoboa/core/app_settings.py +++ b/modoboa/core/app_settings.py @@ -318,6 +318,21 @@ class GeneralParametersForm(param_forms.AdminParametersForm): "Automatically checks if a newer version is available") ) + send_new_versions_email = YesNoField( + label=ugettext_lazy("Send an email when new versions are found"), + initial=False, + help_text=ugettext_lazy( + "Send an email to notify admins about new versions" + ) + ) + new_versions_email_rcpt = lib_fields.UTF8EmailField( + label=_("Recipient"), + initial="postmaster@yourdomain.test", + help_text=_( + "Recipient of new versions notification emails." + ) + ) + send_statistics = YesNoField( label=ugettext_lazy("Send statistics"), initial=True, @@ -385,6 +400,8 @@ class GeneralParametersForm(param_forms.AdminParametersForm): "ldap_groups_search_base": "authentication_type=ldap", "check_new_versions": "enable_api_communication=True", "send_statistics": "enable_api_communication=True", + "send_new_versions_email": "check_new_versions=True", + "new_versions_email_rcpt": "check_new_versions=True" } def __init__(self, *args, **kwargs): diff --git a/modoboa/core/handlers.py b/modoboa/core/handlers.py index 4f2b063c6..a0833c22b 100644 --- a/modoboa/core/handlers.py +++ b/modoboa/core/handlers.py @@ -122,7 +122,7 @@ def check_for_new_versions(sender, include_all, **kwargs): request = get_request() if not request.user.is_superuser: return [] - status, extensions = utils.check_for_updates(request) + status, extensions = utils.check_for_updates() if not status: return [{"id": "newversionavailable"}] if include_all else [] return [{ diff --git a/modoboa/core/management/commands/communicate_with_public_api.py b/modoboa/core/management/commands/communicate_with_public_api.py index eb1bc59d5..eac121bf3 100644 --- a/modoboa/core/management/commands/communicate_with_public_api.py +++ b/modoboa/core/management/commands/communicate_with_public_api.py @@ -1,11 +1,15 @@ """Management command to communicate with public API.""" +from django.core.mail import EmailMessage from django.core.management.base import BaseCommand, CommandError +from django.template.loader import render_to_string +from django.utils.translation import ugettext as _ from modoboa.admin import models as admin_models from modoboa.core.extensions import exts_pool from modoboa.lib import api_client from ... import models +from ... import utils class Command(BaseCommand): @@ -13,6 +17,40 @@ class Command(BaseCommand): help = "Communicate with Modoboa public API." # NOQA:A003 + def send_notification(self, local_config, extensions): + """Send email notification about updates.""" + updates = {} + to_create = [] + for extension in extensions: + if "update" not in extension: + continue + qset = models.ExtensionUpdateHistory.objects.filter( + extension=extension["name"], version=extension["last_version"] + ) + if not qset.exists(): + updates[extension["name"]] = extension["last_version"] + to_create.append( + models.ExtensionUpdateHistory( + extension=extension["name"], + version=extension["last_version"] + ) + ) + if not updates: + return + content = render_to_string( + "core/notifications/update_available.html", { + "updates": updates + }) + subject = _("[modoboa] Update(s) available") + sender = local_config.parameters.get_value("sender_address") + recipient = local_config.parameters.get_value( + "new_versions_email_rcpt") + msg = EmailMessage( + subject, content.strip(), sender, [recipient], + ) + msg.send() + models.ExtensionUpdateHistory.objects.bulk_create(to_create) + def handle(self, *args, **options): """Command entry point.""" local_config = ( @@ -34,6 +72,11 @@ class Command(BaseCommand): local_config.save() + if local_config.parameters.get_value("send_new_versions_email"): + update_avail, extensions = utils.check_for_updates() + if update_avail: + self.send_notification(local_config, extensions) + if not local_config.parameters.get_value("send_statistics"): return extensions = [ext["name"] for ext in exts_pool.list_all()] diff --git a/modoboa/core/migrations/0018_auto_20200121_1659.py b/modoboa/core/migrations/0018_auto_20200121_1659.py new file mode 100644 index 000000000..bd7c3ba35 --- /dev/null +++ b/modoboa/core/migrations/0018_auto_20200121_1659.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.18 on 2020-01-21 15:59 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0017_auto_20190705_1045'), + ] + + operations = [ + migrations.CreateModel( + name='ExtensionUpdateHistory', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('extension', models.CharField(max_length=100)), + ('version', models.CharField(max_length=30)), + ], + ), + migrations.AlterUniqueTogether( + name='extensionupdatehistory', + unique_together=set([('extension', 'version')]), + ), + ] diff --git a/modoboa/core/mocks.py b/modoboa/core/mocks.py index 5637ac21e..6cc508470 100644 --- a/modoboa/core/mocks.py +++ b/modoboa/core/mocks.py @@ -39,3 +39,15 @@ def modo_api_versions(url, request): {"name": "modoboa", "version": "9.0.0", "url": ""}, ] } + + +@httmock.urlmatch( + netloc=r"api\.modoboa\.org$", path=r"^/1/versions/", method="get") +def modo_api_versions_no_update(url, request): + """Simulate versions check.""" + return { + "status_code": 200, + "content": [ + {"name": "modoboa", "version": "0.0.0", "url": ""}, + ] + } diff --git a/modoboa/core/models.py b/modoboa/core/models.py index 89046f3c7..f51218dd1 100644 --- a/modoboa/core/models.py +++ b/modoboa/core/models.py @@ -448,3 +448,16 @@ class LocalConfig(models.Model): """Load parameter manager.""" super(LocalConfig, self).__init__(*args, **kwargs) self.parameters = param_tools.Manager("global", self._parameters) + + +class ExtensionUpdateHistory(models.Model): + """Keeps track of update notifications.""" + + extension = models.CharField(max_length=100) + version = models.CharField(max_length=30) + + class Meta: + unique_together = [("extension", "version")] + + def __str__(self): + return "{}: {}".format(self.extension, self.name) diff --git a/modoboa/core/templates/core/notifications/update_available.html b/modoboa/core/templates/core/notifications/update_available.html new file mode 100644 index 000000000..c5b98982d --- /dev/null +++ b/modoboa/core/templates/core/notifications/update_available.html @@ -0,0 +1,11 @@ +{% load i18n %} + +{% blocktrans count counter=updates|length %} + The following update is available: +{% plural %} + The following updates are available: +{% endblocktrans %} + +{% for extension, version in updates.items %} + {{ extension }}: {{ version }} +{% endfor %} diff --git a/modoboa/core/tests/test_core.py b/modoboa/core/tests/test_core.py index 14a04e50a..c9f34e8be 100644 --- a/modoboa/core/tests/test_core.py +++ b/modoboa/core/tests/test_core.py @@ -5,6 +5,7 @@ from io import StringIO import httmock from dateutil.relativedelta import relativedelta +from django.core import mail from django.core import management from django.test import TestCase from django.urls import reverse @@ -198,7 +199,37 @@ class APICommunicationTestCase(ModoTestCase): mocks.modo_api_versions): management.call_command("communicate_with_public_api") self.assertEqual(models.LocalConfig.objects.first().api_pk, 100) + self.assertEqual(len(mail.outbox), 0) url = reverse("core:information") response = self.ajax_request("get", url) self.assertIn("9.0.0", response["content"]) + + # Enable notifications + self.set_global_parameter("send_new_versions_email", True) + with httmock.HTTMock( + mocks.modo_api_instance_search, + mocks.modo_api_instance_create, + mocks.modo_api_instance_update, + mocks.modo_api_versions): + management.call_command("communicate_with_public_api") + self.assertEqual(len(mail.outbox), 1) + + # Call once again and check no new notification has been sent + self.set_global_parameter("send_new_versions_email", True) + with httmock.HTTMock( + mocks.modo_api_instance_search, + mocks.modo_api_instance_create, + mocks.modo_api_instance_update, + mocks.modo_api_versions): + management.call_command("communicate_with_public_api") + self.assertEqual(len(mail.outbox), 1) + + # Make sure no new notification is sent when no updates + with httmock.HTTMock( + mocks.modo_api_instance_search, + mocks.modo_api_instance_create, + mocks.modo_api_instance_update, + mocks.modo_api_versions_no_update): + management.call_command("communicate_with_public_api") + self.assertEqual(len(mail.outbox), 1) diff --git a/modoboa/core/tests/test_views.py b/modoboa/core/tests/test_views.py index 584179be9..f011ef876 100644 --- a/modoboa/core/tests/test_views.py +++ b/modoboa/core/tests/test_views.py @@ -74,7 +74,9 @@ SETTINGS_SAMPLE = { "core-ldap_sync_bind_dn": "", "core-ldap_sync_bind_password": "", "core-ldap_sync_account_dn_template": "", - "core-ldap_sync_delete_remote_account": False + "core-ldap_sync_delete_remote_account": False, + "core-send_new_versions_email": False, + "core-new_versions_email_rcpt": "postmaster@domain.test" } diff --git a/modoboa/core/utils.py b/modoboa/core/utils.py index 1630f150d..9425581fb 100644 --- a/modoboa/core/utils.py +++ b/modoboa/core/utils.py @@ -21,7 +21,7 @@ def parse_map_file(path): return content -def check_for_updates(request): +def check_for_updates(): """Check if a new version of Modoboa is available.""" local_config = models.LocalConfig.objects.first() client = ModoAPIClient() diff --git a/modoboa/core/views/admin.py b/modoboa/core/views/admin.py index ee4a8b5c9..881b396b9 100644 --- a/modoboa/core/views/admin.py +++ b/modoboa/core/views/admin.py @@ -53,7 +53,7 @@ def parameters(request, tplname="core/parameters.html"): @login_required @user_passes_test(lambda u: u.is_superuser) def information(request, tplname="core/information.html"): - status, extensions = check_for_updates(request) + status, extensions = check_for_updates() return render_to_json_response({ "content": render_to_string(tplname, { "update_avail": status,