Compare commits

...

4 Commits

Author SHA1 Message Date
Sabrina Demagny ad4d65ae92 🔖(patch) release version 1.4.3
Update all version files and changelog for patch release.
2024-11-12 20:00:53 +01:00
Marie PUPO JEAMMET 3a18dce636 (dimail) synchronize mailboxes from dimail to our db
Synchronize mailboxes existing on dimail's api and not on our side,
on domains we are administrating.
2024-11-12 19:59:34 +01:00
Sabrina Demagny 617000ccf8 🔖(patch) release version 1.4.2
Update all version files and changelog for patch release.
2024-11-12 14:01:39 +01:00
Sabrina Demagny d75605b280 🐛(mail) fix button display on outlook
In confirmation email of mailbox creation,
button "Go to La Messagerie" disappears on Outlook.
We try to fix here this display bug.
2024-11-12 10:45:06 +01:00
14 changed files with 288 additions and 22 deletions
+15 -1
View File
@@ -8,6 +8,18 @@ and this project adheres to
## [Unreleased]
## [1.4.3] - 2024-11-12
### Added
- ✨(dimail) synchronize mailboxes from dimail to our db #453
## [1.4.2] - 2024-11-12
### Fixed
- 🐛(mail) fix display button on outlook
## [1.4.1] - 2024-10-23
### Fixed
@@ -124,7 +136,9 @@ and this project adheres to
- ✨(domains) create and manage domains on admin + API
- ✨(domains) mailbox creation + link to email provisioning API
[unreleased]: https://github.com/numerique-gouv/people/compare/v1.4.1...main
[unreleased]: https://github.com/numerique-gouv/people/compare/v1.4.3...main
[1.4.3]: https://github.com/numerique-gouv/people/releases/v1.4.3
[1.4.2]: https://github.com/numerique-gouv/people/releases/v1.4.2
[1.4.1]: https://github.com/numerique-gouv/people/releases/v1.4.1
[1.4.0]: https://github.com/numerique-gouv/people/releases/v1.4.0
[1.3.1]: https://github.com/numerique-gouv/people/releases/v1.3.1
+35 -8
View File
@@ -1,9 +1,35 @@
"""Admin classes and registrations for People's mailbox manager app."""
from django.contrib import admin
from django.contrib import admin, messages
from django.utils.translation import gettext_lazy as _
from requests import exceptions
from mailbox_manager import models
from mailbox_manager.utils.dimail import DimailAPIClient
@admin.action(description=_("Synchronise from dimail"))
def sync_mailboxes_from_dimail(modeladmin, request, queryset): # pylint: disable=unused-argument
"""Admin action to synchronize existing mailboxes from dimail to our database."""
client = DimailAPIClient()
for domain in queryset:
try:
imported_mailboxes = client.synchronize_mailboxes_from_dimail(domain)
except exceptions.HTTPError as err:
messages.error(
request,
_(f"Synchronisation failed for {domain.name} with message: [{err}]"),
)
else:
messages.success(
request,
_(
f"Synchronisation succeed for {domain.name}. "
f"Imported mailboxes: {', '.join(imported_mailboxes)}"
),
)
class UserMailDomainAccessInline(admin.TabularInline):
@@ -28,6 +54,14 @@ class MailDomainAdmin(admin.ModelAdmin):
search_fields = ("name",)
readonly_fields = ["created_at", "slug"]
inlines = (UserMailDomainAccessInline,)
actions = (sync_mailboxes_from_dimail,)
@admin.register(models.Mailbox)
class MailboxAdmin(admin.ModelAdmin):
"""Admin for mailbox model."""
list_display = ("__str__", "first_name", "last_name")
@admin.register(models.MailDomainAccess)
@@ -50,10 +84,3 @@ class MailDomainAccessInline(admin.TabularInline):
autocomplete_fields = ["user", "domain"]
model = models.MailDomainAccess
readonly_fields = ("created_at", "updated_at")
@admin.register(models.Mailbox)
class MailboxAdmin(admin.ModelAdmin):
"""Admin for mailbox model."""
list_display = ("__str__", "first_name", "last_name")
@@ -11,6 +11,7 @@ from django.test.utils import override_settings
from django.utils.translation import gettext_lazy as _
import pytest
import requests
import responses
from rest_framework import status
from rest_framework.test import APIClient
@@ -531,7 +532,7 @@ def test_api_mailboxes__handling_dimail_unexpected_error(mock_error):
content_type="application/json",
)
with pytest.raises(SystemError):
with pytest.raises(requests.exceptions.HTTPError):
response = client.post(
f"/api/v1.0/mail-domains/{access.domain.slug}/mailboxes/",
mailbox_data,
@@ -0,0 +1,156 @@
"""
Unit tests for dimail client
"""
import re
from email.errors import HeaderParseError, NonASCIILocalPartDefect
from logging import Logger
from unittest import mock
import pytest
import responses
from rest_framework import status
from mailbox_manager import factories, models
from mailbox_manager.utils.dimail import DimailAPIClient
pytestmark = pytest.mark.django_db
def test_dimail_synchronization__already_sync():
"""
No mailbox should be created when everything is already synced.
"""
domain = factories.MailDomainEnabledFactory()
factories.MailboxFactory.create_batch(3, domain=domain)
pre_sync_mailboxes = models.Mailbox.objects.filter(domain=domain)
assert pre_sync_mailboxes.count() == 3
dimail_client = DimailAPIClient()
with responses.RequestsMock() as rsps:
# Ensure successful response using "responses":
rsps.add(
rsps.GET,
re.compile(r".*/token/"),
body='{"access_token": "dimail_people_token"}',
status=status.HTTP_200_OK,
content_type="application/json",
)
rsps.add(
rsps.GET,
re.compile(rf".*/domains/{domain.name}/mailboxes/"),
body=str(
[
{
"type": "mailbox",
"status": "broken",
"email": f"{mailbox.local_part}@{domain.name}",
"givenName": mailbox.first_name,
"surName": mailbox.last_name,
"displayName": f"{mailbox.first_name} {mailbox.last_name}",
}
for mailbox in pre_sync_mailboxes
]
),
status=status.HTTP_200_OK,
content_type="application/json",
)
imported_mailboxes = dimail_client.synchronize_mailboxes_from_dimail(domain)
post_sync_mailboxes = models.Mailbox.objects.filter(domain=domain)
assert post_sync_mailboxes.count() == 3
assert imported_mailboxes == []
assert set(models.Mailbox.objects.filter(domain=domain)) == set(pre_sync_mailboxes)
@mock.patch.object(Logger, "warning")
def test_dimail_synchronization__synchronize_mailboxes(mock_warning):
"""A mailbox existing solely on dimail should be synchronized
upon calling sync function on its domain"""
domain = factories.MailDomainEnabledFactory()
assert not models.Mailbox.objects.exists()
dimail_client = DimailAPIClient()
with responses.RequestsMock() as rsps:
# Ensure successful response using "responses":
rsps.add(
rsps.GET,
re.compile(r".*/token/"),
body='{"access_token": "dimail_people_token"}',
status=status.HTTP_200_OK,
content_type="application/json",
)
mailbox_valid = {
"type": "mailbox",
"status": "broken",
"email": f"oxadmin@{domain.name}",
"givenName": "Admin",
"surName": "Context",
"displayName": "Context Admin",
}
mailbox_with_wrong_domain = {
"type": "mailbox",
"status": "broken",
"email": "johndoe@wrongdomain.com",
"givenName": "John",
"surName": "Doe",
"displayName": "John Doe",
}
mailbox_with_invalid_domain = {
"type": "mailbox",
"status": "broken",
"email": f"naw@ake@{domain.name}",
"givenName": "Joe",
"surName": "Doe",
"displayName": "Joe Doe",
}
mailbox_with_invalid_local_part = {
"type": "mailbox",
"status": "broken",
"email": f"obalmaské@{domain.name}",
"givenName": "Jean",
"surName": "Vang",
"displayName": "Jean Vang",
}
rsps.add(
rsps.GET,
re.compile(rf".*/domains/{domain.name}/mailboxes/"),
body=str(
[
mailbox_valid,
mailbox_with_wrong_domain,
mailbox_with_invalid_domain,
mailbox_with_invalid_local_part,
]
),
status=status.HTTP_200_OK,
content_type="application/json",
)
imported_mailboxes = dimail_client.synchronize_mailboxes_from_dimail(domain)
# 3 imports failed: wrong domain, HeaderParseError, NonASCIILocalPartDefect
assert mock_warning.call_count == 3
# first we try to import email with a wrong domain
assert mock_warning.call_args_list[0][0] == (
"Import of email %s failed because of a wrong domain",
mailbox_with_wrong_domain["email"],
)
# then we try to import email with invalid domain
invalid_mailbox_log = mock_warning.call_args_list[1][0]
assert invalid_mailbox_log[1] == mailbox_with_invalid_domain["email"]
assert isinstance(invalid_mailbox_log[2], HeaderParseError)
# finally we try to import email with non ascii local part
non_ascii_mailbox_log = mock_warning.call_args_list[2][0]
assert non_ascii_mailbox_log[1] == mailbox_with_invalid_local_part["email"]
assert isinstance(non_ascii_mailbox_log[2], NonASCIILocalPartDefect)
mailbox = models.Mailbox.objects.get()
assert mailbox.local_part == "oxadmin"
assert imported_mailboxes == [mailbox_valid["email"]]
+69 -1
View File
@@ -1,6 +1,9 @@
"""A minimalist client to synchronize with mailbox provisioning API."""
import ast
import smtplib
from email.errors import HeaderParseError, NonASCIILocalPartDefect
from email.headerregistry import Address
from logging import getLogger
from django.conf import settings
@@ -13,6 +16,8 @@ import requests
from rest_framework import status
from urllib3.util import Retry
from mailbox_manager import models
logger = getLogger(__name__)
adapter = requests.adapters.HTTPAdapter(
@@ -123,7 +128,7 @@ class DimailAPIClient:
logger.error(
"[DIMAIL] unexpected error : %s %s", response.status_code, error_content
)
raise SystemError(
raise requests.exceptions.HTTPError(
f"Unexpected response from dimail: {response.status_code} {error_content}"
)
@@ -163,3 +168,66 @@ class DimailAPIClient:
recipient,
exception,
)
def synchronize_mailboxes_from_dimail(self, domain):
"""Synchronize mailboxes from dimail - open xchange to our database.
This is useful in case of acquisition of a pre-existing mail domain.
Mailboxes created here are not new mailboxes and will not trigger mail notification."""
try:
response = session.get(
f"{self.API_URL}/domains/{domain.name}/mailboxes/",
headers=self.get_headers(),
verify=True,
timeout=10,
)
except requests.exceptions.ConnectionError as error:
logger.error(
"Connection error while trying to reach %s.",
self.API_URL,
exc_info=error,
)
raise error
if response.status_code != status.HTTP_200_OK:
return self.pass_dimail_unexpected_response(response)
dimail_mailboxes = ast.literal_eval(
response.content.decode("utf-8")
) # format output str to proper list
people_mailboxes = models.Mailbox.objects.filter(domain=domain)
imported_mailboxes = []
for dimail_mailbox in dimail_mailboxes:
if not dimail_mailbox["email"] in [
str(people_mailbox) for people_mailbox in people_mailboxes
]:
try:
# sometimes dimail api returns email from another domain,
# so we decide to exclude this kind of email
address = Address(addr_spec=dimail_mailbox["email"])
if address.domain == domain.name:
# creates a mailbox on our end
mailbox = models.Mailbox.objects.create(
first_name=dimail_mailbox["givenName"],
last_name=dimail_mailbox["surName"],
local_part=address.username,
domain=domain,
secondary_email=dimail_mailbox[
"email"
], # secondary email is mandatory. Unfortunately, dimail doesn't
# store any. We temporarily give current email as secondary email.
)
imported_mailboxes.append(str(mailbox))
else:
logger.warning(
"Import of email %s failed because of a wrong domain",
dimail_mailbox["email"],
)
except (HeaderParseError, NonASCIILocalPartDefect) as err:
logger.warning(
"Import of email %s failed with error %s",
dimail_mailbox["email"],
err,
)
return imported_mailboxes
+1 -1
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "people"
version = "1.4.1"
version = "1.4.3"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "app-desk",
"version": "1.4.1",
"version": "1.4.3",
"private": true,
"scripts": {
"dev": "next dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "app-e2e",
"version": "1.4.1",
"version": "1.4.3",
"private": true,
"scripts": {
"lint": "eslint . --ext .ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "people",
"version": "1.4.1",
"version": "1.4.3",
"private": true,
"workspaces": {
"packages": [
@@ -1,6 +1,6 @@
{
"name": "eslint-config-people",
"version": "1.4.1",
"version": "1.4.3",
"license": "MIT",
"scripts": {
"lint": "eslint --ext .js ."
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "packages-i18n",
"version": "1.4.1",
"version": "1.4.3",
"private": true,
"scripts": {
"extract-translation": "yarn extract-translation:desk",
@@ -1,7 +1,7 @@
image:
repository: lasuite/people-backend
pullPolicy: Always
tag: "v1.4.1"
tag: "v1.4.3"
backend:
migrateJobAnnotations:
@@ -113,7 +113,7 @@ frontend:
image:
repository: lasuite/people-frontend
pullPolicy: Always
tag: "v1.4.1"
tag: "v1.4.3"
ingress:
enabled: true
@@ -1,7 +1,7 @@
image:
repository: lasuite/people-backend
pullPolicy: Always
tag: "v1.4.1"
tag: "v1.4.3"
backend:
migrateJobAnnotations:
@@ -113,7 +113,7 @@ frontend:
image:
repository: lasuite/people-frontend
pullPolicy: Always
tag: "v1.4.1"
tag: "v1.4.3"
ingress:
enabled: true
+1 -1
View File
@@ -26,7 +26,7 @@
</mj-section>
<mj-section padding="0px 20px">
<mj-column>
<mj-button href="//{{ webmail_url }}" background-color="#000091" color="white" padding="30px 0px">
<mj-button background-color="#000091" color="white" href="{{ webmail_url }}">
{% trans "Go to La Messagerie" %}
</mj-button>
<!-- Signature -->