From 5178e460c43e867060e45b1d6438238a6e7654ec Mon Sep 17 00:00:00 2001 From: Sabrina Demagny Date: Fri, 21 Mar 2025 17:10:18 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(domains)=20notify=20support=20when=20?= =?UTF-8?q?domain=20status=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit During the scheduled task to check domains, send an email notification to domain support if a status has changed. --- CHANGELOG.md | 1 + .../locale/fr_FR/LC_MESSAGES/django.mo | Bin 13676 -> 13676 bytes src/backend/mailbox_manager/models.py | 70 ++++++++++++- src/backend/mailbox_manager/tasks.py | 1 + .../mailbox_manager/tests/test_tasks.py | 98 ++++++++++++++++-- src/mail/mjml/maildomain_action_required.mjml | 45 ++++++++ src/mail/mjml/maildomain_enabled.mjml | 45 ++++++++ src/mail/mjml/maildomain_failed.mjml | 47 +++++++++ 8 files changed, 296 insertions(+), 11 deletions(-) create mode 100644 src/mail/mjml/maildomain_action_required.mjml create mode 100644 src/mail/mjml/maildomain_enabled.mjml create mode 100644 src/mail/mjml/maildomain_failed.mjml diff --git a/CHANGELOG.md b/CHANGELOG.md index 63b601f..99c6d5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to ### Added +- ✨(domains) notify support when domain status changes #668 - ✨(domains) define domain check interval as a settings - ✨(oidc) add simple introspection backend #832 - 🧑‍💻(tasks) run management commands #814 diff --git a/src/backend/locale/fr_FR/LC_MESSAGES/django.mo b/src/backend/locale/fr_FR/LC_MESSAGES/django.mo index 09190c6e647ba871834ba0050d12729170ab47f0..df82c36f014128b4d615472e5d8e31ee77db8008 100644 GIT binary patch delta 20 bcmaEp^(JeBj2yeUf`O%#k@03_IZtr_QR)U! delta 20 bcmaEp^(JeBj2yd(f}x?6iP>glIZtr_QK$w? diff --git a/src/backend/mailbox_manager/models.py b/src/backend/mailbox_manager/models.py index 4f1373f..dbbb92f 100644 --- a/src/backend/mailbox_manager/models.py +++ b/src/backend/mailbox_manager/models.py @@ -2,12 +2,17 @@ Declare and configure the models for the People additional application : mailbox_manager """ +import logging +import smtplib + from django.conf import settings from django.contrib.auth.base_user import AbstractBaseUser -from django.core import exceptions, validators +from django.contrib.sites.models import Site +from django.core import exceptions, mail, validators from django.db import models +from django.template.loader import render_to_string from django.utils.text import slugify -from django.utils.translation import gettext +from django.utils.translation import get_language, gettext, override from django.utils.translation import gettext_lazy as _ from core.models import BaseInvitation, BaseModel, Organization, User @@ -18,6 +23,28 @@ from mailbox_manager.enums import ( MailDomainStatusChoices, ) +logger = logging.getLogger(__name__) + + +STATUS_NOTIFICATION_MAILS = { + # new status domain: (mail subject, mail template html, mail template text) + MailDomainStatusChoices.ENABLED: ( + _("[La Suite] Your domain is ready"), + "mail/html/maildomain_enabled.html", + "mail/text/maildomain_enabled.txt", + ), + MailDomainStatusChoices.ACTION_REQUIRED: ( + _("[La Suite] Your domain requires action"), + "mail/html/maildomain_action_required.html", + "mail/text/maildomain_action_required.txt", + ), + MailDomainStatusChoices.FAILED: ( + _("[La Suite] Your domain has failed"), + "mail/html/maildomain_failed.html", + "mail/text/maildomain_failed.txt", + ), +} + class MailDomain(BaseModel): """Domain names from which we will create email addresses (mailboxes).""" @@ -104,6 +131,45 @@ class MailDomain(BaseModel): bool(self.organization) and self.status == MailDomainStatusChoices.ENABLED ) + def notify_status_change(self, recipients=None, language=None): + """ + Notify the support team that the domain status has changed. + """ + subject, template_html, template_text = STATUS_NOTIFICATION_MAILS.get( + self.status, (None, None, None) + ) + if not subject: + return + context = { + "title": subject, + "domain_name": self.name, + "manage_domain_url": ( + f"{Site.objects.get_current().domain}/mail-domains/{self.slug}/" + ), + } + try: + with override(language or get_language()): + mail.send_mail( + subject, + render_to_string(template_text, context), + settings.EMAIL_FROM, + recipients or [self.support_email], + html_message=render_to_string(template_html, context), + fail_silently=False, + ) + except smtplib.SMTPException as exception: + logger.error( + "Notification email to %s was not sent: %s", + self.support_email, + exception, + ) + else: + logger.info( + "Information about domain %s sent to %s.", + self.name, + self.support_email, + ) + class MailDomainAccess(BaseModel): """Allow to manage users' accesses to mail domains.""" diff --git a/src/backend/mailbox_manager/tasks.py b/src/backend/mailbox_manager/tasks.py index 5b7a379..498e974 100644 --- a/src/backend/mailbox_manager/tasks.py +++ b/src/backend/mailbox_manager/tasks.py @@ -63,5 +63,6 @@ def fetch_domains_status_task(status: str): logger.error("Failed to fetch status for domain %s: %s", domain.name, err) else: if old_status != domain.status: + domain.notify_status_change() changed_domains.append(f"{domain.name} ({domain.status})") return changed_domains diff --git a/src/backend/mailbox_manager/tests/test_tasks.py b/src/backend/mailbox_manager/tests/test_tasks.py index 567b233..33ae753 100644 --- a/src/backend/mailbox_manager/tests/test_tasks.py +++ b/src/backend/mailbox_manager/tests/test_tasks.py @@ -4,7 +4,11 @@ Unit tests for mailbox manager tasks. import json import re +from unittest import mock +from django.conf import settings +from django.contrib.sites.models import Site +from django.template.loader import render_to_string from django.test import override_settings import pytest @@ -12,7 +16,11 @@ import responses from mailbox_manager import enums, factories, tasks -from .fixtures.dimail import CHECK_DOMAIN_BROKEN_INTERNAL, CHECK_DOMAIN_OK +from .fixtures.dimail import ( + CHECK_DOMAIN_BROKEN_EXTERNAL, + CHECK_DOMAIN_BROKEN_INTERNAL, + CHECK_DOMAIN_OK, +) pytestmark = pytest.mark.django_db @@ -30,12 +38,18 @@ def test_fetch_domain_status_task_success(): # pylint: disable=too-many-locals domain_failed = factories.MailDomainFactory( status=enums.MailDomainStatusChoices.FAILED ) + domain_pending = factories.MailDomainFactory( + status=enums.MailDomainStatusChoices.PENDING + ) body_content_ok1 = CHECK_DOMAIN_OK.copy() body_content_ok1["name"] = domain_enabled1.name - body_content_broken = CHECK_DOMAIN_BROKEN_INTERNAL.copy() - body_content_broken["name"] = domain_enabled2.name + body_content_broken_internal = CHECK_DOMAIN_BROKEN_INTERNAL.copy() + body_content_broken_internal["name"] = domain_enabled2.name + + body_content_broken_external = CHECK_DOMAIN_BROKEN_EXTERNAL.copy() + body_content_broken_external["name"] = domain_pending.name body_content_ok2 = CHECK_DOMAIN_OK.copy() body_content_ok2["name"] = domain_disabled.name @@ -44,7 +58,8 @@ def test_fetch_domain_status_task_success(): # pylint: disable=too-many-locals body_content_ok3["name"] = domain_failed.name for domain, body_content in [ (domain_enabled1, body_content_ok1), - (domain_enabled2, body_content_broken), + (domain_enabled2, body_content_broken_internal), + (domain_pending, body_content_broken_external), (domain_failed, body_content_ok3), ]: # Mock dimail API with success response @@ -59,18 +74,20 @@ def test_fetch_domain_status_task_success(): # pylint: disable=too-many-locals responses.add( responses.GET, re.compile(rf".*/domains/{domain_enabled2.name}/fix/"), - body=json.dumps(body_content_broken), + body=json.dumps(body_content_broken_internal), status=200, content_type="application/json", ) - tasks.fetch_domains_status_task(enums.MailDomainStatusChoices.ENABLED) - tasks.fetch_domains_status_task(enums.MailDomainStatusChoices.FAILED) - tasks.fetch_domains_status_task(enums.MailDomainStatusChoices.ACTION_REQUIRED) - tasks.fetch_domains_status_task(enums.MailDomainStatusChoices.PENDING) + with mock.patch("django.core.mail.send_mail") as mock_send: + tasks.fetch_domains_status_task(enums.MailDomainStatusChoices.ENABLED) + tasks.fetch_domains_status_task(enums.MailDomainStatusChoices.FAILED) + tasks.fetch_domains_status_task(enums.MailDomainStatusChoices.ACTION_REQUIRED) + tasks.fetch_domains_status_task(enums.MailDomainStatusChoices.PENDING) domain_enabled1.refresh_from_db() domain_enabled2.refresh_from_db() domain_disabled.refresh_from_db() domain_failed.refresh_from_db() + domain_pending.refresh_from_db() # Nothing change for the first domain enable assert domain_enabled1.status == enums.MailDomainStatusChoices.ENABLED # Status of the second activated domain has changed to failure @@ -79,6 +96,69 @@ def test_fetch_domain_status_task_success(): # pylint: disable=too-many-locals assert domain_failed.status == enums.MailDomainStatusChoices.ENABLED # Disabled domain was excluded assert domain_disabled.status == enums.MailDomainStatusChoices.DISABLED + # Pending domain has changed to action required + assert domain_pending.status == enums.MailDomainStatusChoices.ACTION_REQUIRED + + # Check that the notification email was sent + assert mock_send.call_count == 3 + domain_enabled2_context = { + "title": "[La Suite] Your domain has failed", + "domain_name": domain_enabled2.name, + "manage_domain_url": ( + f"{Site.objects.get_current().domain}/mail-domains/{domain_enabled2.slug}/" + ), + } + domain_pending_context = { + "title": "[La Suite] Your domain requires action", + "domain_name": domain_pending.name, + "manage_domain_url": ( + f"{Site.objects.get_current().domain}/mail-domains/{domain_pending.slug}/" + ), + } + domain_failed_context = { + "title": "[La Suite] Your domain is ready", + "domain_name": domain_failed.name, + "manage_domain_url": ( + f"{Site.objects.get_current().domain}/mail-domains/{domain_failed.slug}/" + ), + } + calls = [ + mock.call( + "[La Suite] Your domain has failed", + render_to_string( + "mail/text/maildomain_failed.txt", domain_enabled2_context + ), + settings.EMAIL_FROM, + [domain_enabled2.support_email], + html_message=render_to_string( + "mail/html/maildomain_failed.html", domain_enabled2_context + ), + fail_silently=False, + ), + mock.call( + "[La Suite] Your domain requires action", + render_to_string( + "mail/text/maildomain_action_required.txt", domain_pending_context + ), + settings.EMAIL_FROM, + [domain_pending.support_email], + html_message=render_to_string( + "mail/html/maildomain_action_required.html", domain_pending_context + ), + fail_silently=False, + ), + mock.call( + "[La Suite] Your domain is ready", + render_to_string("mail/text/maildomain_enabled.txt", domain_failed_context), + settings.EMAIL_FROM, + [domain_failed.support_email], + html_message=render_to_string( + "mail/html/maildomain_enabled.html", domain_failed_context + ), + fail_silently=False, + ), + ] + mock_send.assert_has_calls(calls, any_order=True) @override_settings(MAIL_CHECK_DOMAIN_INTERVAL=0) diff --git a/src/mail/mjml/maildomain_action_required.mjml b/src/mail/mjml/maildomain_action_required.mjml new file mode 100644 index 0000000..f2456ba --- /dev/null +++ b/src/mail/mjml/maildomain_action_required.mjml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + {% trans "Some actions are required on your domain" %} + + + + {% trans "Hello," %} + + {% blocktranslate with name=domain_name trimmed %} + Your domain {{ name }} cannot be used until the required actions have been completed. + {% endblocktranslate %} + + {% trans "To solve this problem, please log in La Régie via ProConnect and follow instructions, by following this link:" %} + + arrow + {% trans "Go to La Régie"%} + + + + +

{% trans "Regards," %}

+

{% trans "La Suite Team" %}

+
+ + +
+
+
+
+ +
+ diff --git a/src/mail/mjml/maildomain_enabled.mjml b/src/mail/mjml/maildomain_enabled.mjml new file mode 100644 index 0000000..c93f35c --- /dev/null +++ b/src/mail/mjml/maildomain_enabled.mjml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + {% trans "Your domain is ready" %} + + + + {% trans "Hurray!" %} + + {% blocktranslate with name=domain_name trimmed %} + Your domain {{ name }} can be used now. + {% endblocktranslate %} + + {% trans "To do so, please log in La Régie via ProConnect, by following this link:" %} + + arrow + {% trans "Go to La Régie"%} + + + + +

{% trans "Regards," %}

+

{% trans "La Suite Team" %}

+
+ + +
+
+
+
+ +
+ diff --git a/src/mail/mjml/maildomain_failed.mjml b/src/mail/mjml/maildomain_failed.mjml new file mode 100644 index 0000000..4c8574a --- /dev/null +++ b/src/mail/mjml/maildomain_failed.mjml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + {% trans "Your domain has failed" %} + + + + {% trans "Hello," %} + + {% blocktranslate with name=domain_name trimmed %} + The domain {{ name }} has encountered an error. As long as this error persists, all related mailboxes will remain disabled. + Technical support is currently working on the issue. + {% endblocktranslate %} + + {% trans "You can track the status of your domain on the management interface."%} +
{% trans "To do this, please log in to La Régie via ProConnect, by following this link:" %}
+ + arrow + {% trans "Go to La Régie"%} + + + + +

{% trans "Regards," %}

+

{% trans "La Suite Team" %}

+
+ + +
+
+
+
+ +
+