✨(mailbox) send new mailbox confirmation email
send mailbox information upon creating a new mailbox
This commit is contained in:
committed by
Marie
parent
b69ce001c8
commit
5ded297df6
@@ -17,6 +17,7 @@ and this project adheres to
|
|||||||
- ✨(frontend) show username on AccountDropDown #412
|
- ✨(frontend) show username on AccountDropDown #412
|
||||||
- 🥅(frontend) improve add & update group forms error handling #387
|
- 🥅(frontend) improve add & update group forms error handling #387
|
||||||
- ✨(frontend) allow group members filtering #363
|
- ✨(frontend) allow group members filtering #363
|
||||||
|
- ✨(mailbox) send new mailbox confirmation email #397
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"""Client serializers for People's mailbox manager app."""
|
"""Client serializers for People's mailbox manager app."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from core.api.serializers import UserSerializer
|
from core.api.serializers import UserSerializer
|
||||||
@@ -21,9 +23,24 @@ class MailboxSerializer(serializers.ModelSerializer):
|
|||||||
"""
|
"""
|
||||||
Override create function to fire a request on mailbox creation.
|
Override create function to fire a request on mailbox creation.
|
||||||
"""
|
"""
|
||||||
|
# send new mailbox request to dimail
|
||||||
client = DimailAPIClient()
|
client = DimailAPIClient()
|
||||||
client.send_mailbox_request(validated_data, self.context["request"].user.sub)
|
response = client.send_mailbox_request(
|
||||||
return models.Mailbox.objects.create(**validated_data)
|
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):
|
class MailDomainSerializer(serializers.ModelSerializer):
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from logging import Logger
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from django.test.utils import override_settings
|
from django.test.utils import override_settings
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import responses
|
import responses
|
||||||
@@ -290,6 +291,53 @@ def test_api_mailboxes__domain_viewer_provisioning_api_not_called():
|
|||||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
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(
|
@pytest.mark.parametrize(
|
||||||
"role",
|
"role",
|
||||||
[enums.MailDomainRoleChoices.ADMIN, enums.MailDomainRoleChoices.OWNER],
|
[enums.MailDomainRoleChoices.ADMIN, enums.MailDomainRoleChoices.OWNER],
|
||||||
@@ -542,7 +590,7 @@ def test_api_mailboxes__send_correct_logger_infos(mock_info, mock_error):
|
|||||||
|
|
||||||
# Logger
|
# Logger
|
||||||
assert not mock_error.called
|
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] == (
|
assert mock_info.call_args_list[0][0] == (
|
||||||
"Token succesfully granted by mail-provisioning API.",
|
"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),
|
str(access.domain),
|
||||||
access.user.sub,
|
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"],
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
"""A minimalist client to synchronize with mailbox provisioning API."""
|
"""A minimalist client to synchronize with mailbox provisioning API."""
|
||||||
|
|
||||||
|
import smtplib
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
from django.conf import settings
|
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
|
import requests
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@@ -94,15 +98,10 @@ class DimailAPIClient:
|
|||||||
raise error
|
raise error
|
||||||
|
|
||||||
if response.status_code == status.HTTP_201_CREATED:
|
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(
|
logger.info(
|
||||||
"Mailbox successfully created on domain %s by user %s",
|
"Mailbox successfully created on domain %s by user %s",
|
||||||
str(mailbox["domain"]),
|
str(mailbox["domain"]),
|
||||||
user_sub,
|
user_sub,
|
||||||
extra=extra,
|
|
||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@@ -119,3 +118,40 @@ class DimailAPIClient:
|
|||||||
|
|
||||||
logger.error("[DIMAIL] unexpected error : %s", error_content)
|
logger.error("[DIMAIL] unexpected error : %s", error_content)
|
||||||
raise SystemError(f"Unexpected response from dimail: {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,
|
||||||
|
)
|
||||||
|
|||||||
@@ -265,10 +265,7 @@ class Base(Configuration):
|
|||||||
# Mail
|
# Mail
|
||||||
EMAIL_BACKEND = values.Value("django.core.mail.backends.smtp.EmailBackend")
|
EMAIL_BACKEND = values.Value("django.core.mail.backends.smtp.EmailBackend")
|
||||||
EMAIL_HOST = values.Value(None)
|
EMAIL_HOST = values.Value(None)
|
||||||
EMAIL_HOST_USER = values.Value(None)
|
|
||||||
EMAIL_HOST_PASSWORD = values.Value(None)
|
|
||||||
EMAIL_PORT = values.PositiveIntegerValue(None)
|
EMAIL_PORT = values.PositiveIntegerValue(None)
|
||||||
EMAIL_USE_TLS = values.BooleanValue(False)
|
|
||||||
EMAIL_USE_SSL = values.BooleanValue(False)
|
EMAIL_USE_SSL = values.BooleanValue(False)
|
||||||
EMAIL_FROM = values.Value("from@example.com")
|
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)
|
OIDC_TIMEOUT = values.Value(None, environ_name="OIDC_TIMEOUT", environ_prefix=None)
|
||||||
|
|
||||||
# MAILBOX-PROVISIONING API
|
# MAILBOX-PROVISIONING API
|
||||||
|
WEBMAIL_URL = values.Value(
|
||||||
|
default=None,
|
||||||
|
environ_name="WEBMAIL_URL",
|
||||||
|
environ_prefix=None,
|
||||||
|
)
|
||||||
MAIL_PROVISIONING_API_URL = values.Value(
|
MAIL_PROVISIONING_API_URL = values.Value(
|
||||||
default="http://host.docker.internal:8001",
|
default="http://host.docker.internal:8001",
|
||||||
environ_name="MAIL_PROVISIONING_API_URL",
|
environ_name="MAIL_PROVISIONING_API_URL",
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ backend:
|
|||||||
POSTGRES_USER: dinum
|
POSTGRES_USER: dinum
|
||||||
POSTGRES_PASSWORD: pass
|
POSTGRES_PASSWORD: pass
|
||||||
REDIS_URL: redis://default:pass@redis-master:6379/1
|
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_URL: "http://host.docker.internal:8001"
|
||||||
MAIL_PROVISIONING_API_CREDENTIALS:
|
MAIL_PROVISIONING_API_CREDENTIALS:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ backend:
|
|||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: redis.redis.libre.sh
|
name: redis.redis.libre.sh
|
||||||
key: url
|
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_URL: "https://api.dev.ox.numerique.gouv.fr"
|
||||||
MAIL_PROVISIONING_API_CREDENTIALS:
|
MAIL_PROVISIONING_API_CREDENTIALS:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ backend:
|
|||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: redis.redis.libre.sh
|
name: redis.redis.libre.sh
|
||||||
key: url
|
key: url
|
||||||
|
WEBMAIL_URL: "https://webmail.numerique.gouv.fr"
|
||||||
MAIL_PROVISIONING_API_URL: "https://api.dev.ox.numerique.gouv.fr"
|
MAIL_PROVISIONING_API_URL: "https://api.dev.ox.numerique.gouv.fr"
|
||||||
MAIL_PROVISIONING_API_CREDENTIALS:
|
MAIL_PROVISIONING_API_CREDENTIALS:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ backend:
|
|||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: redis.redis.libre.sh
|
name: redis.redis.libre.sh
|
||||||
key: url
|
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_URL: "https://api.dev.ox.numerique.gouv.fr"
|
||||||
MAIL_PROVISIONING_API_CREDENTIALS:
|
MAIL_PROVISIONING_API_CREDENTIALS:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
|
|||||||
47
src/mail/mjml/new_mailbox.mjml
Normal file
47
src/mail/mjml/new_mailbox.mjml
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<mjml>
|
||||||
|
<mj-include path="./partial/header.mjml" />
|
||||||
|
|
||||||
|
<mj-body mj-class="bg--blue-100">
|
||||||
|
<mj-wrapper css-class="wrapper" padding="0 40px 40px 40px">
|
||||||
|
<mj-section background-url="{% base64_static 'images/mail-header-background.png' %}" background-size="cover" background-repeat="no-repeat" background-position="0 -30px">
|
||||||
|
<mj-column>
|
||||||
|
<mj-image align="center" src="{% base64_static 'images/logo-suite-numerique.png' %}" width="250px" align="left" alt="{% trans 'La Suite Numérique' %}" />
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
<mj-section mj-class="bg--white-100" padding="30px 20px 60px 20px">
|
||||||
|
<mj-column>
|
||||||
|
<mj-text font-size="14px">
|
||||||
|
<p>{{ title }}</p>
|
||||||
|
</mj-text>
|
||||||
|
|
||||||
|
<!-- Welcome Message -->
|
||||||
|
<mj-text>
|
||||||
|
<h1>{% trans "Your new mailbox is ready !" %}</h1>
|
||||||
|
</mj-text>
|
||||||
|
<mj-divider border-width="1px" border-style="solid" border-color="#DDDDDD" width="30%" align="left"/>
|
||||||
|
|
||||||
|
<mj-image src="{% base64_static 'images/logo.png' %}" width="157px" align="left" alt="{% trans 'Logo' %}" />
|
||||||
|
|
||||||
|
<!-- Main Message -->
|
||||||
|
<mj-text>{% trans "Here are your credentials ! " %}</mj-text>
|
||||||
|
<mj-text>{% trans "Email address : "%}{{ mailbox_data.email }}</mj-text>
|
||||||
|
<mj-text>{% trans "Temporary password : "%}{{ mailbox_data.password }}</mj-text>
|
||||||
|
<mj-text>{% trans "You can access your mails on " %}<a href="//{{ webmail_url }}"</a>.</mj-text>
|
||||||
|
|
||||||
|
<mj-button href="//{{ site.domain }}" background-color="#000091" color="white" padding-bottom="30px">
|
||||||
|
{% trans "Visit La Régie" %}
|
||||||
|
</mj-button>
|
||||||
|
|
||||||
|
<!-- Signature -->
|
||||||
|
<mj-text>
|
||||||
|
<p>{% trans "Sincerely," %}</p>
|
||||||
|
<p>{% trans "The La Suite Numérique Team" %}</p>
|
||||||
|
</mj-text>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
</mj-wrapper>
|
||||||
|
</mj-body>
|
||||||
|
|
||||||
|
<mj-include path="./partial/footer.mjml" />
|
||||||
|
</mjml>
|
||||||
|
|
||||||
Reference in New Issue
Block a user