diff --git a/tests/unit/test_email_gateway.py b/tests/unit/test_email_gateway.py
new file mode 100644
index 00000000..ae5e16d8
--- /dev/null
+++ b/tests/unit/test_email_gateway.py
@@ -0,0 +1,170 @@
+#!/usr/bin/env python3
+
+# Libervia: an XMPP client
+# Copyright (C) 2009-2024 Jérôme Poisson (goffi@goffi.org)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from email.utils import formataddr
+from unittest.mock import AsyncMock, MagicMock
+import pytest
+from pytest_twisted import ensureDeferred as ed
+from twisted.words.protocols.jabber import jid
+from email.message import EmailMessage
+from libervia.backend.plugins.plugin_comp_email_gateway import EmailGatewayComponent
+from libervia.backend.plugins.plugin_comp_email_gateway.models import (
+ Credentials,
+ UserData,
+)
+
+
+class TestEmailGatewayComponent:
+ @pytest.fixture
+ def email_gw(self, host):
+ email_gw = EmailGatewayComponent(host)
+ email_gw.storage = MagicMock()
+ email_gw.users_data = {}
+ return email_gw
+
+ def test_jid_to_email_gateway_jid(self, email_gw):
+ """JID from the gateway is converted to an email address."""
+ client = MagicMock()
+ client.jid = jid.JID("gateway.example.org")
+ address_jid = jid.JID(r"user\40some-email-domain.example@gateway.example.org")
+ credentials = {"user_email": "user@example.org"}
+ result = email_gw.jid_to_email(client, address_jid, credentials)
+ assert result == "user@some-email-domain.example"
+
+ def test_jid_to_email_non_gateway_jid(self, email_gw):
+ """Non-gateway JID is converted to an email address with ``xmpp:``."""
+ client = MagicMock()
+ client.jid = jid.JID("gateway.example.org")
+ address_jid = jid.JID("external@example.org")
+ credentials = {"user_email": "user@example.org"}
+ result = email_gw.jid_to_email(client, address_jid, credentials)
+ assert result == '"xmpp:external@example.org" '
+
+ def test_email_to_jid_user_email(self, email_gw):
+ """User email returns user JID if no "xmpp:" scheme is used."""
+ client = MagicMock()
+ client.jid = jid.JID("gateway.example.org")
+ email_name = ""
+ email_addr = "user@example.org"
+ user_email = "user@example.org"
+ user_jid = jid.JID("gw-user@example.org")
+ result, name = email_gw.email_to_jid(
+ client, user_email, user_jid, email_name, email_addr
+ )
+ assert result == jid.JID("gw-user@example.org")
+ assert name is None
+
+ def test_email_to_jid_xmpp_address(self, email_gw):
+ """Email address with XMPP in name part is converted to a JID."""
+ client = MagicMock()
+ client.jid = jid.JID("gateway.example.org")
+ email_name = "xmpp:user@example.net"
+ email_addr = "user@example.org"
+ user_email = "user@example.org"
+ user_jid = jid.JID("gw-user@example.org")
+ result, name = email_gw.email_to_jid(
+ client, user_email, user_jid, email_name, email_addr
+ )
+ assert result == jid.JID("user@example.net")
+ assert name is None
+
+ def test_email_to_jid_regular_email(self, email_gw):
+ """Regular email address is converted to a JID with escaped local part."""
+ client = MagicMock()
+ client.jid = jid.JID("gateway.example.org")
+ email_name = "User Name"
+ email_addr = "user@some-email-domain.example"
+ user_email = "user@example.org"
+ user_jid = jid.JID("gw-user@example.org")
+ result, name = email_gw.email_to_jid(
+ client, user_email, user_jid, email_name, email_addr
+ )
+ assert result == jid.JID(r"user\40some-email-domain.example@gateway.example.org")
+ assert name == "User Name"
+
+ @ed
+ async def test_on_new_email_single_recipient(self, email_gw):
+ """Email with a single recipient is correctly processed and sent as a message."""
+ client = MagicMock()
+ client.get_virtual_client = lambda __: client
+ client.jid = jid.JID("gateway.example.org")
+ client.sendMessage = AsyncMock()
+ email_gw.client = client
+
+ email = EmailMessage()
+ email["from"] = formataddr(("User Name", "sender@somewhere.example"))
+ email["to"] = "user@example.org"
+ email.set_content("Hello, world!")
+
+ to_jid = jid.JID("gw-user@example.org")
+ user_email = "user@example.org"
+ user_data = UserData(Credentials({"user_email": user_email}))
+ await email_gw.on_new_email(user_data, to_jid, email)
+
+ client.sendMessage.assert_called_once()
+ call_args = client.sendMessage.call_args[0]
+ assert call_args[0] == to_jid
+ assert call_args[1] == {"": "Hello, world!\n"}
+ assert call_args[2] == None
+ client.sendMessage.assert_called_once_with(
+ to_jid, {"": "Hello, world!\n"}, None, extra=None
+ )
+
+ @ed
+ async def test_on_new_email_multiple_recipients(self, email_gw):
+ """Email with multiple recipients is correctly processed and sent ."""
+ client = MagicMock()
+ client.get_virtual_client = lambda __: client
+ client.jid = jid.JID("gateway.example.org")
+ client.sendMessage = AsyncMock()
+ email_gw.client = client
+
+ email = EmailMessage()
+ email["from"] = formataddr(("User Name", "user@example.org"))
+ email["to"] = "user@example.org"
+ email["cc"] = "recipient2@example.org"
+ email["bcc"] = "recipient3@example.org"
+ email.set_content("Hello, world!")
+
+ user_jid = jid.JID("gw-user@example.org")
+ user_email = "user@example.org"
+ user_data = UserData(Credentials({"user_email": user_email}))
+ await email_gw.on_new_email(user_data, user_jid, email)
+
+ client.sendMessage.assert_called_once_with(
+ user_jid,
+ {"": "Hello, world!\n"},
+ None,
+ extra={
+ "addresses": {
+ "to": [{"jid": "gw-user@example.org", "delivered": True}],
+ "cc": [
+ {
+ "jid": "recipient2\\40example.org@gateway.example.org",
+ "delivered": True,
+ }
+ ],
+ "bcc": [
+ {
+ "jid": "recipient3\\40example.org@gateway.example.org",
+ "delivered": True,
+ }
+ ],
+ }
+ },
+ )