From 5ded297df6744ddcaebb16fa1123d1d2ca5b6988 Mon Sep 17 00:00:00 2001 From: Marie PUPO JEAMMET Date: Fri, 20 Sep 2024 18:08:02 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(mailbox)=20send=20new=20mailbox=20con?= =?UTF-8?q?firmation=20email?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit send mailbox information upon creating a new mailbox --- CHANGELOG.md | 1 + .../mailbox_manager/api/serializers.py | 21 +++- .../mailboxes/test_api_mailboxes_create.py | 105 +++++++++++++++++- src/backend/mailbox_manager/utils/dimail.py | 48 +++++++- src/backend/people/settings.py | 8 +- src/helm/env.d/dev/values.desk.yaml.gotmpl | 1 + .../env.d/preprod/values.desk.yaml.gotmpl | 1 + .../env.d/production/values.desk.yaml.gotmpl | 1 + .../env.d/staging/values.desk.yaml.gotmpl | 1 + src/mail/mjml/new_mailbox.mjml | 47 ++++++++ 10 files changed, 222 insertions(+), 12 deletions(-) create mode 100644 src/mail/mjml/new_mailbox.mjml diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c17c51..31f05cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to - ✨(frontend) show username on AccountDropDown #412 - 🥅(frontend) improve add & update group forms error handling #387 - ✨(frontend) allow group members filtering #363 +- ✨(mailbox) send new mailbox confirmation email #397 ### Changed diff --git a/src/backend/mailbox_manager/api/serializers.py b/src/backend/mailbox_manager/api/serializers.py index 0d60e3e..0a4dcb9 100644 --- a/src/backend/mailbox_manager/api/serializers.py +++ b/src/backend/mailbox_manager/api/serializers.py @@ -1,5 +1,7 @@ """Client serializers for People's mailbox manager app.""" +import json + from rest_framework import serializers from core.api.serializers import UserSerializer @@ -21,9 +23,24 @@ class MailboxSerializer(serializers.ModelSerializer): """ Override create function to fire a request on mailbox creation. """ + # send new mailbox request to dimail client = DimailAPIClient() - client.send_mailbox_request(validated_data, self.context["request"].user.sub) - return models.Mailbox.objects.create(**validated_data) + response = client.send_mailbox_request( + validated_data, self.context["request"].user.sub + ) + + # fix format to have actual json, and remove uuid + mailbox_data = json.loads(response.content.decode("utf-8").replace("'", '"')) + del mailbox_data["uuid"] + + # actually save mailbox on our database + instance = models.Mailbox.objects.create(**validated_data) + + # send confirmation email + client.send_new_mailbox_notification( + recipient=validated_data["secondary_email"], mailbox_data=mailbox_data + ) + return instance class MailDomainSerializer(serializers.ModelSerializer): diff --git a/src/backend/mailbox_manager/tests/api/mailboxes/test_api_mailboxes_create.py b/src/backend/mailbox_manager/tests/api/mailboxes/test_api_mailboxes_create.py index c540e85..346ccb2 100644 --- a/src/backend/mailbox_manager/tests/api/mailboxes/test_api_mailboxes_create.py +++ b/src/backend/mailbox_manager/tests/api/mailboxes/test_api_mailboxes_create.py @@ -8,6 +8,7 @@ from logging import Logger from unittest import mock from django.test.utils import override_settings +from django.utils.translation import gettext_lazy as _ import pytest import responses @@ -290,6 +291,53 @@ def test_api_mailboxes__domain_viewer_provisioning_api_not_called(): assert response.status_code == status.HTTP_403_FORBIDDEN +@mock.patch.object(Logger, "error") +def test_api_mailboxes__dimail_unauthorized(mock_error): + """ + Wrong secret but a secret corresponding to another user/domain + i.e. secret from another domain/user on dimail API. + """ + + # creating all needed objects + access = factories.MailDomainAccessFactory(role=enums.MailDomainRoleChoices.OWNER) + + client = APIClient() + client.force_login(access.user) + mailbox_data = serializers.MailboxSerializer( + factories.MailboxFactory.build(domain=access.domain) + ).data + + with responses.RequestsMock() as rsps: + # Ensure successful response using "responses": + rsps.add( + rsps.GET, + re.compile(r".*/token/"), + body='{"access_token": "domain_owner_token"}', + status=status.HTTP_200_OK, + content_type="application/json", + ) + rsp = rsps.add( + rsps.POST, + re.compile(rf".*/domains/{access.domain.name}/mailboxes/"), + status=status.HTTP_403_FORBIDDEN, + content_type="application/json", + ) + + response = client.post( + f"/api/v1.0/mail-domains/{access.domain.slug}/mailboxes/", + mailbox_data, + format="json", + ) + + assert mock_error.call_count == 1 + assert mock_error.call_args_list[0][0] == ( + "[DIMAIL] 403 Forbidden: please check the mail domain secret of %s", + access.domain.name, + ) + assert rsp.call_count == 1 + assert response.status_code == status.HTTP_403_FORBIDDEN + + @pytest.mark.parametrize( "role", [enums.MailDomainRoleChoices.ADMIN, enums.MailDomainRoleChoices.OWNER], @@ -542,7 +590,7 @@ def test_api_mailboxes__send_correct_logger_infos(mock_info, mock_error): # Logger assert not mock_error.called - assert mock_info.call_count == 3 + assert mock_info.call_count == 4 assert mock_info.call_args_list[0][0] == ( "Token succesfully granted by mail-provisioning API.", ) @@ -551,3 +599,58 @@ def test_api_mailboxes__send_correct_logger_infos(mock_info, mock_error): str(access.domain), access.user.sub, ) + + +@mock.patch.object(Logger, "info") +def test_api_mailboxes__sends_new_mailbox_notification(mock_info): + """ + Creating a new mailbox should send confirmation email + to secondary email. + """ + access = factories.MailDomainAccessFactory(role=enums.MailDomainRoleChoices.OWNER) + + client = APIClient() + client.force_login(access.user) + mailbox_data = serializers.MailboxSerializer( + factories.MailboxFactory.build(domain=access.domain) + ).data + + with responses.RequestsMock() as rsps: + # Ensure successful response using "responses": + rsps.add( + rsps.GET, + re.compile(r".*/token/"), + body='{"access_token": "domain_owner_token"}', + status=status.HTTP_200_OK, + content_type="application/json", + ) + rsps.add( + rsps.POST, + re.compile(rf".*/domains/{access.domain.name}/mailboxes/"), + body=str( + { + "email": f"{mailbox_data['local_part']}@{access.domain.name}", + "password": "newpass", + "uuid": "uuid", + } + ), + status=status.HTTP_201_CREATED, + content_type="application/json", + ) + with mock.patch("django.core.mail.send_mail") as mock_send: + client.post( + f"/api/v1.0/mail-domains/{access.domain.slug}/mailboxes/", + mailbox_data, + format="json", + ) + + assert mock_send.call_count == 1 + assert mock_send.mock_calls[0][1][0] == "Your new mailbox information" + assert mock_send.mock_calls[0][1][3][0] == mailbox_data["secondary_email"] + + assert mock_info.call_count == 4 + assert mock_info.call_args_list[2][0] == ( + "Information for mailbox %s sent to %s.", + f"{mailbox_data['local_part']}@{access.domain.name}", + mailbox_data["secondary_email"], + ) diff --git a/src/backend/mailbox_manager/utils/dimail.py b/src/backend/mailbox_manager/utils/dimail.py index d38fad9..42057c1 100644 --- a/src/backend/mailbox_manager/utils/dimail.py +++ b/src/backend/mailbox_manager/utils/dimail.py @@ -1,9 +1,13 @@ """A minimalist client to synchronize with mailbox provisioning API.""" +import smtplib from logging import getLogger from django.conf import settings -from django.core import exceptions +from django.contrib.sites.models import Site +from django.core import exceptions, mail +from django.template.loader import render_to_string +from django.utils.translation import gettext_lazy as _ import requests from rest_framework import status @@ -94,15 +98,10 @@ class DimailAPIClient: raise error if response.status_code == status.HTTP_201_CREATED: - extra = {"response": response.content.decode("utf-8")} - # This a temporary broken solution. Password will soon be sent - # from OX servers but their prod is not ready. - # In the meantime, we log mailbox info (including password !) logger.info( "Mailbox successfully created on domain %s by user %s", str(mailbox["domain"]), user_sub, - extra=extra, ) return response @@ -119,3 +118,40 @@ class DimailAPIClient: logger.error("[DIMAIL] unexpected error : %s", error_content) raise SystemError(f"Unexpected response from dimail: {error_content}") + + def send_new_mailbox_notification(self, recipient, mailbox_data): + """ + Send email to confirm mailbox creation + and send new mailbox information. + """ + + template_vars = { + "title": _("Your new mailbox information"), + "site": Site.objects.get_current(), + "webmail_url": settings.WEBMAIL_URL, + "mailbox_data": mailbox_data, + } + + msg_html = render_to_string("mail/html/new_mailbox.html", template_vars) + msg_plain = render_to_string("mail/text/new_mailbox.txt", template_vars) + + try: + mail.send_mail( + template_vars["title"], + msg_plain, + settings.EMAIL_FROM, + [recipient], + html_message=msg_html, + fail_silently=False, + ) + logger.info( + "Information for mailbox %s sent to %s.", + mailbox_data["email"], + recipient, + ) + except smtplib.SMTPException as exception: + logger.error( + "Mailbox confirmation email to %s was not sent: %s", + recipient, + exception, + ) diff --git a/src/backend/people/settings.py b/src/backend/people/settings.py index d281e41..2975124 100755 --- a/src/backend/people/settings.py +++ b/src/backend/people/settings.py @@ -265,10 +265,7 @@ class Base(Configuration): # Mail EMAIL_BACKEND = values.Value("django.core.mail.backends.smtp.EmailBackend") EMAIL_HOST = values.Value(None) - EMAIL_HOST_USER = values.Value(None) - EMAIL_HOST_PASSWORD = values.Value(None) EMAIL_PORT = values.PositiveIntegerValue(None) - EMAIL_USE_TLS = values.BooleanValue(False) EMAIL_USE_SSL = values.BooleanValue(False) EMAIL_FROM = values.Value("from@example.com") @@ -416,6 +413,11 @@ class Base(Configuration): OIDC_TIMEOUT = values.Value(None, environ_name="OIDC_TIMEOUT", environ_prefix=None) # MAILBOX-PROVISIONING API + WEBMAIL_URL = values.Value( + default=None, + environ_name="WEBMAIL_URL", + environ_prefix=None, + ) MAIL_PROVISIONING_API_URL = values.Value( default="http://host.docker.internal:8001", environ_name="MAIL_PROVISIONING_API_URL", diff --git a/src/helm/env.d/dev/values.desk.yaml.gotmpl b/src/helm/env.d/dev/values.desk.yaml.gotmpl index cc1db18..0a95a41 100644 --- a/src/helm/env.d/dev/values.desk.yaml.gotmpl +++ b/src/helm/env.d/dev/values.desk.yaml.gotmpl @@ -50,6 +50,7 @@ backend: POSTGRES_USER: dinum POSTGRES_PASSWORD: pass REDIS_URL: redis://default:pass@redis-master:6379/1 + WEBMAIL_URL: "https://onestendev.yapasdewebmail.fr" MAIL_PROVISIONING_API_URL: "http://host.docker.internal:8001" MAIL_PROVISIONING_API_CREDENTIALS: secretKeyRef: diff --git a/src/helm/env.d/preprod/values.desk.yaml.gotmpl b/src/helm/env.d/preprod/values.desk.yaml.gotmpl index 9bb8292..2b5ef14 100644 --- a/src/helm/env.d/preprod/values.desk.yaml.gotmpl +++ b/src/helm/env.d/preprod/values.desk.yaml.gotmpl @@ -84,6 +84,7 @@ backend: secretKeyRef: name: redis.redis.libre.sh key: url + WEBMAIL_URL: "https://webmail.test.ox.numerique.gouv.fr" MAIL_PROVISIONING_API_URL: "https://api.dev.ox.numerique.gouv.fr" MAIL_PROVISIONING_API_CREDENTIALS: secretKeyRef: diff --git a/src/helm/env.d/production/values.desk.yaml.gotmpl b/src/helm/env.d/production/values.desk.yaml.gotmpl index 157fa53..57729de 100644 --- a/src/helm/env.d/production/values.desk.yaml.gotmpl +++ b/src/helm/env.d/production/values.desk.yaml.gotmpl @@ -84,6 +84,7 @@ backend: secretKeyRef: name: redis.redis.libre.sh key: url + WEBMAIL_URL: "https://webmail.numerique.gouv.fr" MAIL_PROVISIONING_API_URL: "https://api.dev.ox.numerique.gouv.fr" MAIL_PROVISIONING_API_CREDENTIALS: secretKeyRef: diff --git a/src/helm/env.d/staging/values.desk.yaml.gotmpl b/src/helm/env.d/staging/values.desk.yaml.gotmpl index b181fef..08352de 100644 --- a/src/helm/env.d/staging/values.desk.yaml.gotmpl +++ b/src/helm/env.d/staging/values.desk.yaml.gotmpl @@ -98,6 +98,7 @@ backend: secretKeyRef: name: redis.redis.libre.sh key: url + WEBMAIL_URL: "https://webmail.test.ox.numerique.gouv.fr" MAIL_PROVISIONING_API_URL: "https://api.dev.ox.numerique.gouv.fr" MAIL_PROVISIONING_API_CREDENTIALS: secretKeyRef: diff --git a/src/mail/mjml/new_mailbox.mjml b/src/mail/mjml/new_mailbox.mjml new file mode 100644 index 0000000..0fb6601 --- /dev/null +++ b/src/mail/mjml/new_mailbox.mjml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + +

{{ title }}

+
+ + + +

{% trans "Your new mailbox is ready !" %}

+
+ + + + + + {% trans "Here are your credentials ! " %} + {% trans "Email address : "%}{{ mailbox_data.email }} + {% trans "Temporary password : "%}{{ mailbox_data.password }} + {% trans "You can access your mails on " %}. + + + {% trans "Visit La Régie" %} + + + + +

{% trans "Sincerely," %}

+

{% trans "The La Suite Numérique Team" %}

+
+
+
+
+
+ + +
+