annotate libervia/backend/plugins/plugin_comp_email_gateway/__init__.py @ 4306:94e0968987cd

plugin XEP-0033: code modernisation, improve delivery, data validation: - Code has been rewritten using Pydantic models and `async` coroutines for data validation and cleaner element parsing/generation. - Delivery has been completely rewritten. It now works even if server doesn't support multicast, and send to local multicast service first. Delivering to local multicast service first is due to bad support of XEP-0033 in server (notably Prosody which has an incomplete implementation), and the current impossibility to detect if a sub-domain service handles fully multicast or only for local domains. This is a workaround to have a good balance between backward compatilibity and use of bandwith, and to make it work with the incoming email gateway implementation (the gateway will only deliver to entities of its own domain). - disco feature checking now uses `async` corountines. `host` implementation still use Deferred return values for compatibility with legacy code. rel 450
author Goffi <goffi@goffi.org>
date Thu, 26 Sep 2024 16:12:01 +0200
parents a7ec325246fb
children b56b1eae7994
Ignore whitespace changes - Everywhere: Within whitespace: At end of lines:
rev   line source
4303
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
1 #!/usr/bin/env python3
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
2
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
3 # Libervia Email Gateway Component
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
4 # Copyright (C) 2009-2024 Jérôme Poisson (goffi@goffi.org)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
5
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
6 # This program is free software: you can redistribute it and/or modify
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
7 # it under the terms of the GNU Affero General Public License as published by
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
8 # the Free Software Foundation, either version 3 of the License, or
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
9 # (at your option) any later version.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
10
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
11 # This program is distributed in the hope that it will be useful,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
14 # GNU Affero General Public License for more details.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
15
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
16 # You should have received a copy of the GNU Affero General Public License
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
18
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
19 from dataclasses import dataclass
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
20 from email.header import decode_header
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
21 from email.message import EmailMessage
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
22 from email.mime.text import MIMEText
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
23 from email.utils import formataddr, parseaddr
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
24 from functools import partial
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
25 import re
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
26 from typing import Any, cast
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
27
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
28 from twisted.internet import defer, reactor
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
29 from twisted.mail import smtp
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
30 from twisted.words.protocols.jabber import jid
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
31 from twisted.words.protocols.jabber.error import StanzaError
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
32 from twisted.words.protocols.jabber.xmlstream import XMPPHandler
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
33 from twisted.words.xish import domish
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
34 from wokkel import data_form, disco, iwokkel
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
35 from zope.interface import implementer
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
36
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
37 from libervia.backend.core import exceptions
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
38 from libervia.backend.core.constants import Const as C
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
39 from libervia.backend.core.core_types import SatXMPPEntity
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
40 from libervia.backend.core.i18n import D_, _
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
41 from libervia.backend.core.log import getLogger
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
42 from libervia.backend.memory.persistent import LazyPersistentBinaryDict
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
43 from libervia.backend.memory.sqla import select
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
44 from libervia.backend.memory.sqla_mapping import PrivateIndBin
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
45 from libervia.backend.models.core import MessageData
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
46 from libervia.backend.plugins.plugin_xep_0077 import XEP_0077
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
47 from libervia.backend.plugins.plugin_xep_0106 import XEP_0106
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
48 from libervia.backend.tools.utils import aio
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
49
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
50 from .models import Credentials, UserData
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
51 from .imap import IMAPClientFactory
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
52
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
53
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
54 log = getLogger(__name__)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
55
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
56 IMPORT_NAME = "email-gateway"
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
57 NAME = "Libervia Email Gateway"
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
58
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
59 PLUGIN_INFO = {
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
60 C.PI_NAME: "Email Gateway Component",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
61 C.PI_IMPORT_NAME: IMPORT_NAME,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
62 C.PI_MODES: [C.PLUG_MODE_COMPONENT],
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
63 C.PI_TYPE: C.PLUG_TYPE_ENTRY_POINT,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
64 C.PI_PROTOCOLS: [],
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
65 C.PI_DEPENDENCIES: ["XEP-0077", "XEP-0106"],
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
66 C.PI_RECOMMENDATIONS: [],
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
67 C.PI_MAIN: "EmailGatewayComponent",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
68 C.PI_HANDLER: C.BOOL_TRUE,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
69 C.PI_DESCRIPTION: D_(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
70 "Gateway to handle email. Usual emails are handled as message, while mailing "
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
71 "lists are converted to pubsub blogs."
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
72 ),
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
73 }
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
74
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
75 CONF_SECTION = f"component {IMPORT_NAME}"
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
76 PREFIX_KEY_CREDENTIALS = "CREDENTIALS_"
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
77 KEY_CREDENTIALS = f"{PREFIX_KEY_CREDENTIALS}{{from_jid}}"
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
78
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
79 email_pattern = re.compile(r"[^@]+@[^@]+\.[^@]+")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
80
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
81
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
82 class EmailGatewayComponent:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
83 IMPORT_NAME = IMPORT_NAME
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
84 verbose = 0
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
85
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
86 def __init__(self, host):
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
87 self.host = host
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
88 self.client: SatXMPPEntity | None = None
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
89 self.initalized = False
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
90 self.storage: LazyPersistentBinaryDict | None = None
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
91 self._iq_register = cast(XEP_0077, host.plugins["XEP-0077"])
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
92 self._iq_register.register_handler(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
93 self._on_registration_form, self._on_registration_submit
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
94 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
95 self._e = cast(XEP_0106, host.plugins["XEP-0106"])
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
96 # TODO: For the moment, all credentials are kept in cache; we should only keep the
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
97 # X latest.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
98 self.users_data: dict[jid.JID, UserData] = {}
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
99 host.trigger.add_with_check(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
100 "message_received", self, self._message_received_trigger, priority=-1000
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
101 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
102
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
103 async def _init(self) -> None:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
104 """Initialisation done after profile is connected"""
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
105 assert self.client is not None
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
106 self.client.identities.append(disco.DiscoIdentity("gateway", "smtp", NAME))
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
107 self.storage = LazyPersistentBinaryDict(IMPORT_NAME, self.client.profile)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
108 await self.connect_registered_users()
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
109
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
110 @aio
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
111 async def get_registered_users(self) -> dict[jid.JID, Credentials]:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
112 """Retrieve credentials for all registered users
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
113
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
114 @return: a mapping from user JID to credentials data.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
115 """
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
116 assert self.client is not None
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
117 profile_id = self.host.memory.storage.profiles[self.client.profile]
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
118 async with self.host.memory.storage.session() as session:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
119 query = select(PrivateIndBin).where(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
120 PrivateIndBin.profile_id == profile_id,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
121 PrivateIndBin.namespace == IMPORT_NAME,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
122 PrivateIndBin.key.startswith(PREFIX_KEY_CREDENTIALS),
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
123 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
124 result = await session.execute(query)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
125 return {
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
126 jid.JID(p.key[len(PREFIX_KEY_CREDENTIALS) :]): p.value
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
127 for p in result.scalars()
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
128 }
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
129
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
130 async def connect_registered_users(self) -> None:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
131 """Connected users already registered to the gateway."""
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
132 registered_data = await self.get_registered_users()
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
133 for user_jid, credentials in registered_data.items():
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
134 user_data = self.users_data[user_jid] = UserData(credentials=credentials)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
135 if not credentials["imap_success"]:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
136 log.warning(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
137 f"Ignoring unsuccessful IMAP credentials of {user_jid}. This user "
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
138 "won't receive message from this gateway."
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
139 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
140 else:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
141 try:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
142 await self.connect_imap(user_jid, user_data)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
143 except Exception as e:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
144 log.warning(f"Can't connect {user_jid} to IMAP: {e}.")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
145 else:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
146 log.debug(f"Connection to IMAP server successful for {user_jid}.")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
147
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
148 def get_handler(self, __) -> XMPPHandler:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
149 return EmailGatewayHandler()
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
150
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
151 async def profile_connecting(self, client: SatXMPPEntity) -> None:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
152 self.client = client
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
153 if not self.initalized:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
154 await self._init()
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
155 self.initalized = True
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
156
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
157 def _message_received_trigger(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
158 self,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
159 client: SatXMPPEntity,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
160 message_elt: domish.Element,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
161 post_treat: defer.Deferred,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
162 ) -> bool:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
163 """add the gateway workflow on post treatment"""
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
164 if client != self.client:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
165 return True
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
166 post_treat.addCallback(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
167 lambda mess_data: defer.ensureDeferred(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
168 self.on_message(client, mess_data, message_elt)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
169 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
170 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
171 return True
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
172
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
173 async def on_message(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
174 self, client: SatXMPPEntity, mess_data: MessageData, message_elt: domish.Element
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
175 ) -> dict:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
176 """Called once message has been parsed
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
177
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
178 @param client: Client session.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
179 @param mess_data: Message data.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
180 @return: Message data.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
181 """
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
182 if client != self.client:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
183 return mess_data
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
184 from_jid = mess_data["from"].userhostJID()
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
185 if mess_data["type"] not in ("chat", "normal"):
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
186 log.warning(f"ignoring message with unexpected type: {mess_data}")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
187 return mess_data
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
188 if not client.is_local(from_jid):
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
189 log.warning(f"ignoring non local message: {mess_data}")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
190 return mess_data
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
191 if not mess_data["to"].user:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
192 log.warning(f"ignoring message addressed to gateway itself: {mess_data}")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
193 return mess_data
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
194
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
195 try:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
196 to_email = self._e.unescape(mess_data["to"].user)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
197 except ValueError:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
198 raise exceptions.DataError(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
199 f'Invalid "to" JID, can\'t send message: {message_elt.toXml()}.'
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
200 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
201
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
202 try:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
203 body_lang, body = next(iter(mess_data["message"].items()))
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
204 except (KeyError, StopIteration):
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
205 log.warning(f"No body found: {mess_data}")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
206 body_lang, body = "", ""
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
207 try:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
208 subject_lang, subject = next(iter(mess_data["subject"].items()))
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
209 except (KeyError, StopIteration):
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
210 subject_lang, subject = "", None
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
211
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
212 if not body and not subject:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
213 log.warning(f"Ignoring empty message: {mess_data}")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
214 return mess_data
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
215
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
216 try:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
217 await self.send_email(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
218 from_jid=from_jid,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
219 to_email=to_email,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
220 body=body,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
221 subject=subject,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
222 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
223 except exceptions.UnknownEntityError:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
224 log.warning(f"Can't send message, user {from_jid} is not registered.")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
225 message_error_elt = StanzaError(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
226 "subscription-required",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
227 text="User need to register to the gateway before sending emails.",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
228 ).toResponse(message_elt)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
229 await client.a_send(message_error_elt)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
230
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
231 raise exceptions.CancelError("User not registered.")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
232
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
233 return mess_data
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
234
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
235 async def send_email(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
236 self,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
237 from_jid: jid.JID,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
238 to_email: str,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
239 body: str,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
240 subject: str | None,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
241 ) -> None:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
242 """Send an email using sender credentials.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
243
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
244 Credentials will be retrieve from cache, or database.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
245
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
246 @param from_jid: Bare JID of the sender.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
247 @param to_email: Email address of the destinee.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
248 @param body: Body of the email.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
249 @param subject: Subject of the email.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
250
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
251 @raise exceptions.UnknownEntityError: Credentials for "from_jid" can't be found.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
252 """
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
253 # We need a bare jid.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
254 assert self.storage is not None
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
255 assert not from_jid.resource
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
256 try:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
257 user_data = self.users_data[from_jid]
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
258 except KeyError:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
259 key = KEY_CREDENTIALS.format(from_jid=from_jid)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
260 credentials = await self.storage.get(key)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
261 if credentials is None:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
262 raise exceptions.UnknownEntityError(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
263 f"No credentials found for {from_jid}."
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
264 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
265 self.users_data[from_jid] = UserData(credentials)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
266 else:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
267 credentials = user_data.credentials
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
268
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
269 msg = MIMEText(body, "plain", "UTF-8")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
270 if subject is not None:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
271 msg["Subject"] = subject
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
272 msg["From"] = formataddr(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
273 (credentials["user_name"] or None, credentials["user_email"])
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
274 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
275 msg["To"] = to_email
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
276
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
277 sender_domain = credentials["user_email"].split("@", 1)[-1]
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
278
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
279 await smtp.sendmail(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
280 credentials["smtp_host"].encode(),
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
281 credentials["user_email"].encode(),
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
282 [to_email.encode()],
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
283 msg.as_bytes(),
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
284 senderDomainName=sender_domain,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
285 port=int(credentials["smtp_port"]),
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
286 username=credentials["smtp_username"].encode(),
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
287 password=credentials["smtp_password"].encode(),
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
288 requireAuthentication=True,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
289 # TODO: only STARTTLS is supported right now, implicit TLS should be supported
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
290 # too.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
291 requireTransportSecurity=True,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
292 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
293
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
294 async def _on_registration_form(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
295 self, client: SatXMPPEntity, iq_elt: domish.Element
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
296 ) -> tuple[bool, data_form.Form] | None:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
297 if client != self.client:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
298 return
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
299 assert self.storage is not None
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
300 from_jid = jid.JID(iq_elt["from"])
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
301 key = KEY_CREDENTIALS.format(from_jid=from_jid.userhost())
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
302 credentials = await self.storage.get(key) or {}
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
303
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
304 form = data_form.Form(formType="form", title="IMAP/SMTP Credentials")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
305
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
306 # Add instructions
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
307 form.instructions = [
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
308 D_(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
309 "Please provide your IMAP and SMTP credentials to configure the "
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
310 "connection."
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
311 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
312 ]
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
313
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
314 # Add identity fields
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
315 form.addField(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
316 data_form.Field(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
317 fieldType="text-single",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
318 var="user_name",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
319 label="User Name",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
320 desc=D_('The display name to use in the "From" field of sent emails.'),
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
321 value=credentials.get("user_name"),
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
322 required=True,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
323 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
324 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
325
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
326 form.addField(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
327 data_form.Field(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
328 fieldType="text-single",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
329 var="user_email",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
330 label="User Email",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
331 desc=D_('The email address to use in the "From" field of sent emails.'),
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
332 value=credentials.get("user_email"),
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
333 required=True,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
334 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
335 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
336
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
337 # Add fields for IMAP credentials
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
338 form.addField(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
339 data_form.Field(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
340 fieldType="text-single",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
341 var="imap_host",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
342 label="IMAP Host",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
343 desc=D_("IMAP server hostname or IP address"),
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
344 value=credentials.get("imap_host"),
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
345 required=True,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
346 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
347 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
348 form.addField(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
349 data_form.Field(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
350 fieldType="text-single",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
351 var="imap_port",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
352 label="IMAP Port",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
353 desc=D_("IMAP server port (default: 993)"),
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
354 value=credentials.get("imap_port", "993"),
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
355 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
356 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
357 form.addField(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
358 data_form.Field(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
359 fieldType="text-single",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
360 var="imap_username",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
361 label="IMAP Username",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
362 desc=D_("Username for IMAP authentication"),
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
363 value=credentials.get("imap_username"),
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
364 required=True,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
365 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
366 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
367 form.addField(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
368 data_form.Field(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
369 fieldType="text-private",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
370 var="imap_password",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
371 label="IMAP Password",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
372 desc=D_("Password for IMAP authentication"),
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
373 value=credentials.get("imap_password"),
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
374 required=True,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
375 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
376 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
377
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
378 # Add fields for SMTP credentials
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
379 form.addField(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
380 data_form.Field(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
381 fieldType="text-single",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
382 var="smtp_host",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
383 label="SMTP Host",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
384 desc=D_("SMTP server hostname or IP address"),
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
385 value=credentials.get("smtp_host"),
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
386 required=True,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
387 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
388 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
389 form.addField(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
390 data_form.Field(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
391 fieldType="text-single",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
392 var="smtp_port",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
393 label="SMTP Port",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
394 desc=D_("SMTP server port (default: 587)"),
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
395 value=credentials.get("smtp_port", "587"),
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
396 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
397 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
398 form.addField(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
399 data_form.Field(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
400 fieldType="text-single",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
401 var="smtp_username",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
402 label="SMTP Username",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
403 desc=D_("Username for SMTP authentication"),
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
404 value=credentials.get("smtp_username"),
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
405 required=True,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
406 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
407 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
408 form.addField(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
409 data_form.Field(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
410 fieldType="text-private",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
411 var="smtp_password",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
412 label="SMTP Password",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
413 desc=D_("Password for SMTP authentication"),
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
414 value=credentials.get("smtp_password"),
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
415 required=True,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
416 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
417 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
418
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
419 return bool(credentials), form
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
420
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
421 def validate_field(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
422 self,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
423 form: data_form.Form,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
424 key: str,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
425 field_type: str,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
426 min_value: int | None = None,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
427 max_value: int | None = None,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
428 default: str | int | None = None,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
429 ) -> None:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
430 """Validate a single field.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
431
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
432 @param form: The form containing the fields.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
433 @param key: The key of the field to validate.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
434 @param field_type: The expected type of the field value.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
435 @param min_value: Optional minimum value for integer fields.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
436 @param max_value: Optional maximum value for integer fields.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
437 @param default: Default value to use if the field is missing.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
438 @raise StanzaError: If the field value is invalid or missing.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
439 """
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
440 field = form.fields.get(key)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
441 if field is None:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
442 if default is None:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
443 raise StanzaError("bad-request", text=f"{key} is required")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
444 field = data_form.Field(var=key, value=str(default))
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
445 form.addField(field)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
446
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
447 value = field.value
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
448 if field_type == "int":
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
449 try:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
450 value = int(value)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
451 if (min_value is not None and value < min_value) or (
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
452 max_value is not None and value > max_value
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
453 ):
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
454 raise ValueError
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
455 except (ValueError, TypeError):
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
456 raise StanzaError("bad-request", text=f"Invalid value for {key}: {value}")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
457 elif field_type == "str":
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
458 if not isinstance(value, str):
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
459 raise StanzaError("bad-request", text=f"Invalid value for {key}: {value}")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
460
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
461 # Basic email validation for user_email field
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
462 if key == "user_email":
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
463 # XXX: This is a minimal check. A complete email validation is notoriously
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
464 # difficult.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
465 if not email_pattern.match(value):
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
466 raise StanzaError(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
467 "bad-request", text=f"Invalid email address: {value}"
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
468 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
469
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
470 def validate_imap_smtp_form(self, submit_form: data_form.Form) -> None:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
471 """Validate the submitted IMAP/SMTP credentials form.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
472
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
473 @param submit_form: The submitted form containing IMAP/SMTP credentials.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
474 @raise StanzaError: If any of the values are invalid.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
475 """
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
476 # Validate identity fields
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
477 self.validate_field(submit_form, "user_name", "str")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
478 self.validate_field(submit_form, "user_email", "str")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
479
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
480 # Validate IMAP fields
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
481 self.validate_field(submit_form, "imap_host", "str")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
482 self.validate_field(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
483 submit_form, "imap_port", "int", min_value=1, max_value=65535, default=993
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
484 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
485 self.validate_field(submit_form, "imap_username", "str")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
486 self.validate_field(submit_form, "imap_password", "str")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
487
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
488 # Validate SMTP fields
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
489 self.validate_field(submit_form, "smtp_host", "str")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
490 self.validate_field(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
491 submit_form, "smtp_port", "int", min_value=1, max_value=65535, default=587
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
492 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
493 self.validate_field(submit_form, "smtp_username", "str")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
494 self.validate_field(submit_form, "smtp_password", "str")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
495
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
496 async def on_new_email(self, to_jid: jid.JID, email: EmailMessage) -> None:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
497 """Called when a new message has been received.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
498
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
499 @param to_jid: JID of the recipient.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
500 @param email: Parsed email.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
501 """
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
502 assert self.client is not None
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
503 name, email_addr = parseaddr(email["from"])
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
504 email_addr = email_addr.lower()
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
505 from_jid = jid.JID(None, (self._e.escape(email_addr), self.client.jid.host, None))
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
506
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
507 # Get the email body
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
508 body_mime = email.get_body(("plain",))
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
509 if body_mime is not None:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
510 charset = body_mime.get_content_charset() or "utf-8"
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
511 body = body_mime.get_payload(decode=True).decode(charset, errors="replace")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
512 else:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
513 log.warning(f"No body found in email:\n{email}")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
514 body = ""
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
515
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
516 # Decode the subject
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
517 subject = email.get("subject")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
518 if subject:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
519 decoded_subject = decode_header(subject)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
520 subject = "".join(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
521 [
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
522 part.decode(encoding or "utf-8") if isinstance(part, bytes) else part
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
523 for part, encoding in decoded_subject
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
524 ]
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
525 ).strip()
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
526 else:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
527 subject = None
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
528
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
529 client = self.client.get_virtual_client(from_jid)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
530 await client.sendMessage(to_jid, {"": body}, {"": subject} if subject else None)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
531
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
532 async def connect_imap(self, from_jid: jid.JID, user_data: UserData) -> None:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
533 """Connect to IMAP service.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
534
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
535 [self.on_new_email] will be used as callback on new messages.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
536
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
537 @param from_jid: JID of the user associated with given credentials.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
538 @param credentials: Email credentials.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
539 """
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
540 credentials = user_data.credentials
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
541
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
542 connected = defer.Deferred()
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
543 factory = IMAPClientFactory(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
544 user_data,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
545 partial(self.on_new_email, from_jid.userhostJID()),
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
546 connected,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
547 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
548 reactor.connectTCP(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
549 credentials["imap_host"], int(credentials["imap_port"]), factory
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
550 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
551 await connected
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
552
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
553 async def _on_registration_submit(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
554 self,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
555 client: SatXMPPEntity,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
556 iq_elt: domish.Element,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
557 submit_form: data_form.Form | None,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
558 ) -> bool | None:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
559 """Handle registration submit request.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
560
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
561 Submit form is validated, and credentials are stored.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
562 @param client: client session.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
563 iq_elt: IQ stanza of the submission request.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
564 submit_form: submit form.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
565 @return: True if successful.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
566 None if the callback is not relevant for this request.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
567 """
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
568 if client != self.client:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
569 return
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
570 assert self.storage is not None
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
571 from_jid = jid.JID(iq_elt["from"]).userhostJID()
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
572
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
573 if submit_form is None:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
574 # This is an unregistration request.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
575 try:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
576 user_data = self.users_data[from_jid]
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
577 except KeyError:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
578 pass
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
579 else:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
580 if user_data.imap_client is not None:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
581 try:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
582 await user_data.imap_client.logout()
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
583 except Exception:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
584 log.exception(f"Can't log out {from_jid} from IMAP server.")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
585 key = KEY_CREDENTIALS.format(from_jid=from_jid)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
586 await self.storage.adel(key)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
587 log.info(f"{from_jid} unregistered from this gateway.")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
588 return True
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
589
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
590 self.validate_imap_smtp_form(submit_form)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
591 credentials = {key: field.value for key, field in submit_form.fields.items()}
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
592 user_data = self.users_data.get(from_jid)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
593 if user_data is None:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
594 # The user is not in cache, we cache current credentials.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
595 user_data = self.users_data[from_jid] = UserData(credentials=credentials)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
596 else:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
597 # The user is known, we update credentials.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
598 user_data.credentials = credentials
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
599 key = KEY_CREDENTIALS.format(from_jid=from_jid)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
600 try:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
601 await self.connect_imap(from_jid, user_data)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
602 except Exception as e:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
603 log.warning(f"Can't connect to IMAP server for {from_jid}")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
604 credentials["imap_success"] = False
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
605 await self.storage.aset(key, credentials)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
606 raise e
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
607 else:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
608 log.debug(f"Connection successful to IMAP server for {from_jid}")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
609 credentials["imap_success"] = True
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
610 await self.storage.aset(key, credentials)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
611 return True
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
612
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
613
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
614 @implementer(iwokkel.IDisco)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
615 class EmailGatewayHandler(XMPPHandler):
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
616
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
617 def getDiscoInfo(self, requestor, target, nodeIdentifier=""):
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
618 return []
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
619
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
620 def getDiscoItems(self, requestor, target, nodeIdentifier=""):
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
621 return []