Mercurial > libervia-backend
annotate libervia/backend/plugins/plugin_comp_email_gateway/__init__.py @ 4309:b56b1eae7994
component email gateway: add multicasting:
XEP-0033 multicasting is now supported both for incoming and outgoing messages. XEP-0033
metadata are converted to suitable Email headers and vice versa.
Email address and JID are both supported, and delivery is done by the gateway when
suitable on incoming messages.
rel 450
author | Goffi <goffi@goffi.org> |
---|---|
date | Thu, 26 Sep 2024 16:12:01 +0200 |
parents | a7ec325246fb |
children | 055930cc81f9 |
rev | line source |
---|---|
4303 | 1 #!/usr/bin/env python3 |
2 | |
3 # Libervia Email Gateway Component | |
4 # Copyright (C) 2009-2024 Jérôme Poisson (goffi@goffi.org) | |
5 | |
6 # This program is free software: you can redistribute it and/or modify | |
7 # it under the terms of the GNU Affero General Public License as published by | |
8 # the Free Software Foundation, either version 3 of the License, or | |
9 # (at your option) any later version. | |
10 | |
11 # This program is distributed in the hope that it will be useful, | |
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 # GNU Affero General Public License for more details. | |
15 | |
16 # You should have received a copy of the GNU Affero General Public License | |
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
18 | |
19 from email.header import decode_header | |
20 from email.message import EmailMessage | |
21 from email.mime.text import MIMEText | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
22 from email.utils import formataddr, getaddresses, parseaddr |
4303 | 23 from functools import partial |
24 import re | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
25 from typing import cast |
4303 | 26 |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
27 from pydantic import BaseModel |
4303 | 28 from twisted.internet import defer, reactor |
29 from twisted.mail import smtp | |
30 from twisted.words.protocols.jabber import jid | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
31 from twisted.words.protocols.jabber import error as jabber_error |
4303 | 32 from twisted.words.protocols.jabber.error import StanzaError |
33 from twisted.words.protocols.jabber.xmlstream import XMPPHandler | |
34 from twisted.words.xish import domish | |
35 from wokkel import data_form, disco, iwokkel | |
36 from zope.interface import implementer | |
37 | |
38 from libervia.backend.core import exceptions | |
39 from libervia.backend.core.constants import Const as C | |
40 from libervia.backend.core.core_types import SatXMPPEntity | |
41 from libervia.backend.core.i18n import D_, _ | |
42 from libervia.backend.core.log import getLogger | |
43 from libervia.backend.memory.persistent import LazyPersistentBinaryDict | |
44 from libervia.backend.memory.sqla import select | |
45 from libervia.backend.memory.sqla_mapping import PrivateIndBin | |
46 from libervia.backend.models.core import MessageData | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
47 from libervia.backend.plugins.plugin_xep_0033 import ( |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
48 RECIPIENT_FIELDS, |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
49 AddressType, |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
50 AddressesData, |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
51 ) |
4303 | 52 from libervia.backend.plugins.plugin_xep_0077 import XEP_0077 |
53 from libervia.backend.plugins.plugin_xep_0106 import XEP_0106 | |
54 from libervia.backend.tools.utils import aio | |
55 | |
56 from .models import Credentials, UserData | |
57 from .imap import IMAPClientFactory | |
58 | |
59 | |
60 log = getLogger(__name__) | |
61 | |
62 IMPORT_NAME = "email-gateway" | |
63 NAME = "Libervia Email Gateway" | |
64 | |
65 PLUGIN_INFO = { | |
66 C.PI_NAME: "Email Gateway Component", | |
67 C.PI_IMPORT_NAME: IMPORT_NAME, | |
68 C.PI_MODES: [C.PLUG_MODE_COMPONENT], | |
69 C.PI_TYPE: C.PLUG_TYPE_ENTRY_POINT, | |
70 C.PI_PROTOCOLS: [], | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
71 C.PI_DEPENDENCIES: ["XEP-0033", "XEP-0077", "XEP-0106"], |
4303 | 72 C.PI_RECOMMENDATIONS: [], |
73 C.PI_MAIN: "EmailGatewayComponent", | |
74 C.PI_HANDLER: C.BOOL_TRUE, | |
75 C.PI_DESCRIPTION: D_( | |
76 "Gateway to handle email. Usual emails are handled as message, while mailing " | |
77 "lists are converted to pubsub blogs." | |
78 ), | |
79 } | |
80 | |
81 CONF_SECTION = f"component {IMPORT_NAME}" | |
82 PREFIX_KEY_CREDENTIALS = "CREDENTIALS_" | |
83 KEY_CREDENTIALS = f"{PREFIX_KEY_CREDENTIALS}{{from_jid}}" | |
84 | |
85 email_pattern = re.compile(r"[^@]+@[^@]+\.[^@]+") | |
86 | |
87 | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
88 class SendMailExtra(BaseModel): |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
89 addresses: AddressesData | None = None |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
90 |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
91 |
4303 | 92 class EmailGatewayComponent: |
93 IMPORT_NAME = IMPORT_NAME | |
94 verbose = 0 | |
95 | |
96 def __init__(self, host): | |
97 self.host = host | |
98 self.client: SatXMPPEntity | None = None | |
99 self.initalized = False | |
100 self.storage: LazyPersistentBinaryDict | None = None | |
101 self._iq_register = cast(XEP_0077, host.plugins["XEP-0077"]) | |
102 self._iq_register.register_handler( | |
103 self._on_registration_form, self._on_registration_submit | |
104 ) | |
105 self._e = cast(XEP_0106, host.plugins["XEP-0106"]) | |
106 # TODO: For the moment, all credentials are kept in cache; we should only keep the | |
107 # X latest. | |
108 self.users_data: dict[jid.JID, UserData] = {} | |
109 host.trigger.add_with_check( | |
110 "message_received", self, self._message_received_trigger, priority=-1000 | |
111 ) | |
112 | |
113 async def _init(self) -> None: | |
114 """Initialisation done after profile is connected""" | |
115 assert self.client is not None | |
116 self.client.identities.append(disco.DiscoIdentity("gateway", "smtp", NAME)) | |
117 self.storage = LazyPersistentBinaryDict(IMPORT_NAME, self.client.profile) | |
118 await self.connect_registered_users() | |
119 | |
120 @aio | |
121 async def get_registered_users(self) -> dict[jid.JID, Credentials]: | |
122 """Retrieve credentials for all registered users | |
123 | |
124 @return: a mapping from user JID to credentials data. | |
125 """ | |
126 assert self.client is not None | |
127 profile_id = self.host.memory.storage.profiles[self.client.profile] | |
128 async with self.host.memory.storage.session() as session: | |
129 query = select(PrivateIndBin).where( | |
130 PrivateIndBin.profile_id == profile_id, | |
131 PrivateIndBin.namespace == IMPORT_NAME, | |
132 PrivateIndBin.key.startswith(PREFIX_KEY_CREDENTIALS), | |
133 ) | |
134 result = await session.execute(query) | |
135 return { | |
136 jid.JID(p.key[len(PREFIX_KEY_CREDENTIALS) :]): p.value | |
137 for p in result.scalars() | |
138 } | |
139 | |
140 async def connect_registered_users(self) -> None: | |
141 """Connected users already registered to the gateway.""" | |
142 registered_data = await self.get_registered_users() | |
143 for user_jid, credentials in registered_data.items(): | |
144 user_data = self.users_data[user_jid] = UserData(credentials=credentials) | |
145 if not credentials["imap_success"]: | |
146 log.warning( | |
147 f"Ignoring unsuccessful IMAP credentials of {user_jid}. This user " | |
148 "won't receive message from this gateway." | |
149 ) | |
150 else: | |
151 try: | |
152 await self.connect_imap(user_jid, user_data) | |
153 except Exception as e: | |
154 log.warning(f"Can't connect {user_jid} to IMAP: {e}.") | |
155 else: | |
156 log.debug(f"Connection to IMAP server successful for {user_jid}.") | |
157 | |
158 def get_handler(self, __) -> XMPPHandler: | |
159 return EmailGatewayHandler() | |
160 | |
161 async def profile_connecting(self, client: SatXMPPEntity) -> None: | |
162 self.client = client | |
163 if not self.initalized: | |
164 await self._init() | |
165 self.initalized = True | |
166 | |
167 def _message_received_trigger( | |
168 self, | |
169 client: SatXMPPEntity, | |
170 message_elt: domish.Element, | |
171 post_treat: defer.Deferred, | |
172 ) -> bool: | |
173 """add the gateway workflow on post treatment""" | |
174 if client != self.client: | |
175 return True | |
176 post_treat.addCallback( | |
177 lambda mess_data: defer.ensureDeferred( | |
178 self.on_message(client, mess_data, message_elt) | |
179 ) | |
180 ) | |
181 return True | |
182 | |
183 async def on_message( | |
184 self, client: SatXMPPEntity, mess_data: MessageData, message_elt: domish.Element | |
185 ) -> dict: | |
186 """Called once message has been parsed | |
187 | |
188 @param client: Client session. | |
189 @param mess_data: Message data. | |
190 @return: Message data. | |
191 """ | |
192 if client != self.client: | |
193 return mess_data | |
194 from_jid = mess_data["from"].userhostJID() | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
195 extra = None |
4303 | 196 if mess_data["type"] not in ("chat", "normal"): |
197 log.warning(f"ignoring message with unexpected type: {mess_data}") | |
198 return mess_data | |
199 if not client.is_local(from_jid): | |
200 log.warning(f"ignoring non local message: {mess_data}") | |
201 return mess_data | |
202 if not mess_data["to"].user: | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
203 addresses = mess_data["extra"].get("addresses") |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
204 if not addresses: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
205 log.warning(f"ignoring message addressed to gateway itself: {mess_data}") |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
206 return mess_data |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
207 else: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
208 to_email = None |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
209 extra = SendMailExtra(addresses=addresses) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
210 else: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
211 try: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
212 to_email = self._e.unescape(mess_data["to"].user) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
213 except ValueError: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
214 raise exceptions.DataError( |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
215 f'Invalid "to" JID, can\'t send message: {message_elt.toXml()}.' |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
216 ) |
4303 | 217 |
218 try: | |
219 body_lang, body = next(iter(mess_data["message"].items())) | |
220 except (KeyError, StopIteration): | |
221 log.warning(f"No body found: {mess_data}") | |
222 body_lang, body = "", "" | |
223 try: | |
224 subject_lang, subject = next(iter(mess_data["subject"].items())) | |
225 except (KeyError, StopIteration): | |
226 subject_lang, subject = "", None | |
227 | |
228 if not body and not subject: | |
229 log.warning(f"Ignoring empty message: {mess_data}") | |
230 return mess_data | |
231 | |
232 try: | |
233 await self.send_email( | |
234 from_jid=from_jid, | |
235 to_email=to_email, | |
236 body=body, | |
237 subject=subject, | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
238 extra=extra, |
4303 | 239 ) |
240 except exceptions.UnknownEntityError: | |
241 log.warning(f"Can't send message, user {from_jid} is not registered.") | |
242 message_error_elt = StanzaError( | |
243 "subscription-required", | |
244 text="User need to register to the gateway before sending emails.", | |
245 ).toResponse(message_elt) | |
246 await client.a_send(message_error_elt) | |
247 raise exceptions.CancelError("User not registered.") | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
248 except StanzaError as e: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
249 log.warning("Can't send message: {e}") |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
250 message_error_elt = e.toResponse(message_elt) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
251 await client.a_send(message_error_elt) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
252 raise exceptions.CancelError("Can't send message: {e}") |
4303 | 253 |
254 return mess_data | |
255 | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
256 def jid_to_email( |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
257 self, client: SatXMPPEntity, address_jid: jid.JID, credentials: dict[str, str] |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
258 ) -> str: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
259 """Convert a JID to an email address. |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
260 |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
261 If JID is from the gateway, email address will be extracted. Otherwise, the |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
262 gateway email will be used, with XMPP address specified in name part. |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
263 |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
264 @param address_jid: JID of the recipient. |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
265 @param credentials: Sender credentials. |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
266 @return: Email address. |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
267 """ |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
268 if address_jid and address_jid.host.endswith(str(client.jid)): |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
269 return self._e.unescape(address_jid.user) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
270 else: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
271 email_address = credentials["user_email"] |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
272 if address_jid: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
273 email_address = formataddr((f"xmpp:{address_jid}", email_address)) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
274 return email_address |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
275 |
4303 | 276 async def send_email( |
277 self, | |
278 from_jid: jid.JID, | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
279 to_email: str | None, |
4303 | 280 body: str, |
281 subject: str | None, | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
282 extra: SendMailExtra | None = None, |
4303 | 283 ) -> None: |
284 """Send an email using sender credentials. | |
285 | |
286 Credentials will be retrieve from cache, or database. | |
287 | |
288 @param from_jid: Bare JID of the sender. | |
289 @param to_email: Email address of the destinee. | |
290 @param body: Body of the email. | |
291 @param subject: Subject of the email. | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
292 @param extra: Extra data. |
4303 | 293 |
294 @raise exceptions.UnknownEntityError: Credentials for "from_jid" can't be found. | |
295 """ | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
296 assert self.client is not None |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
297 if extra is None: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
298 extra = SendMailExtra() |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
299 if to_email is None and (extra.addresses is None or not extra.addresses.to): |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
300 raise exceptions.InternalError( |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
301 '"to_email" can\'t be None if there is no "to" address!' |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
302 ) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
303 |
4303 | 304 # We need a bare jid. |
305 assert self.storage is not None | |
306 assert not from_jid.resource | |
307 try: | |
308 user_data = self.users_data[from_jid] | |
309 except KeyError: | |
310 key = KEY_CREDENTIALS.format(from_jid=from_jid) | |
311 credentials = await self.storage.get(key) | |
312 if credentials is None: | |
313 raise exceptions.UnknownEntityError( | |
314 f"No credentials found for {from_jid}." | |
315 ) | |
316 self.users_data[from_jid] = UserData(credentials) | |
317 else: | |
318 credentials = user_data.credentials | |
319 | |
320 msg = MIMEText(body, "plain", "UTF-8") | |
321 if subject is not None: | |
322 msg["Subject"] = subject | |
323 msg["From"] = formataddr( | |
324 (credentials["user_name"] or None, credentials["user_email"]) | |
325 ) | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
326 if extra.addresses: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
327 assert extra.addresses.to |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
328 main_to_address = extra.addresses.to[0] |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
329 assert main_to_address.jid |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
330 to_email = self.jid_to_email(self.client, main_to_address.jid, credentials) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
331 for field in RECIPIENT_FIELDS: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
332 addresses = getattr(extra.addresses, field) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
333 if not addresses: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
334 continue |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
335 for address in addresses: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
336 if not address.delivered and ( |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
337 address.jid is None or address.jid.host != str(self.client.jid) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
338 ): |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
339 log.warning( |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
340 "Received undelivered message to external JID, this is not " |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
341 "allowed! Cancelling the message sending." |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
342 ) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
343 stanza_err = jabber_error.StanzaError( |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
344 "forbidden", |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
345 text="Multicasting (XEP-0033 addresses) can only be used " |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
346 "with JID from this gateway, not external ones. " |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
347 f" {address.jid} can't be delivered by this gateway and " |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
348 "should be delivered by server instead.", |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
349 ) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
350 raise stanza_err |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
351 email_addresses = [ |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
352 self.jid_to_email(self.client, address.jid, credentials) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
353 for address in addresses |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
354 if address.jid |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
355 ] |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
356 if email_addresses: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
357 msg[field.upper()] = ", ".join(email_addresses) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
358 else: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
359 assert to_email is not None |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
360 msg["To"] = to_email |
4303 | 361 |
362 sender_domain = credentials["user_email"].split("@", 1)[-1] | |
363 | |
364 await smtp.sendmail( | |
365 credentials["smtp_host"].encode(), | |
366 credentials["user_email"].encode(), | |
367 [to_email.encode()], | |
368 msg.as_bytes(), | |
369 senderDomainName=sender_domain, | |
370 port=int(credentials["smtp_port"]), | |
371 username=credentials["smtp_username"].encode(), | |
372 password=credentials["smtp_password"].encode(), | |
373 requireAuthentication=True, | |
374 # TODO: only STARTTLS is supported right now, implicit TLS should be supported | |
375 # too. | |
376 requireTransportSecurity=True, | |
377 ) | |
378 | |
379 async def _on_registration_form( | |
380 self, client: SatXMPPEntity, iq_elt: domish.Element | |
381 ) -> tuple[bool, data_form.Form] | None: | |
382 if client != self.client: | |
383 return | |
384 assert self.storage is not None | |
385 from_jid = jid.JID(iq_elt["from"]) | |
386 key = KEY_CREDENTIALS.format(from_jid=from_jid.userhost()) | |
387 credentials = await self.storage.get(key) or {} | |
388 | |
389 form = data_form.Form(formType="form", title="IMAP/SMTP Credentials") | |
390 | |
391 # Add instructions | |
392 form.instructions = [ | |
393 D_( | |
394 "Please provide your IMAP and SMTP credentials to configure the " | |
395 "connection." | |
396 ) | |
397 ] | |
398 | |
399 # Add identity fields | |
400 form.addField( | |
401 data_form.Field( | |
402 fieldType="text-single", | |
403 var="user_name", | |
404 label="User Name", | |
405 desc=D_('The display name to use in the "From" field of sent emails.'), | |
406 value=credentials.get("user_name"), | |
407 required=True, | |
408 ) | |
409 ) | |
410 | |
411 form.addField( | |
412 data_form.Field( | |
413 fieldType="text-single", | |
414 var="user_email", | |
415 label="User Email", | |
416 desc=D_('The email address to use in the "From" field of sent emails.'), | |
417 value=credentials.get("user_email"), | |
418 required=True, | |
419 ) | |
420 ) | |
421 | |
422 # Add fields for IMAP credentials | |
423 form.addField( | |
424 data_form.Field( | |
425 fieldType="text-single", | |
426 var="imap_host", | |
427 label="IMAP Host", | |
428 desc=D_("IMAP server hostname or IP address"), | |
429 value=credentials.get("imap_host"), | |
430 required=True, | |
431 ) | |
432 ) | |
433 form.addField( | |
434 data_form.Field( | |
435 fieldType="text-single", | |
436 var="imap_port", | |
437 label="IMAP Port", | |
438 desc=D_("IMAP server port (default: 993)"), | |
439 value=credentials.get("imap_port", "993"), | |
440 ) | |
441 ) | |
442 form.addField( | |
443 data_form.Field( | |
444 fieldType="text-single", | |
445 var="imap_username", | |
446 label="IMAP Username", | |
447 desc=D_("Username for IMAP authentication"), | |
448 value=credentials.get("imap_username"), | |
449 required=True, | |
450 ) | |
451 ) | |
452 form.addField( | |
453 data_form.Field( | |
454 fieldType="text-private", | |
455 var="imap_password", | |
456 label="IMAP Password", | |
457 desc=D_("Password for IMAP authentication"), | |
458 value=credentials.get("imap_password"), | |
459 required=True, | |
460 ) | |
461 ) | |
462 | |
463 # Add fields for SMTP credentials | |
464 form.addField( | |
465 data_form.Field( | |
466 fieldType="text-single", | |
467 var="smtp_host", | |
468 label="SMTP Host", | |
469 desc=D_("SMTP server hostname or IP address"), | |
470 value=credentials.get("smtp_host"), | |
471 required=True, | |
472 ) | |
473 ) | |
474 form.addField( | |
475 data_form.Field( | |
476 fieldType="text-single", | |
477 var="smtp_port", | |
478 label="SMTP Port", | |
479 desc=D_("SMTP server port (default: 587)"), | |
480 value=credentials.get("smtp_port", "587"), | |
481 ) | |
482 ) | |
483 form.addField( | |
484 data_form.Field( | |
485 fieldType="text-single", | |
486 var="smtp_username", | |
487 label="SMTP Username", | |
488 desc=D_("Username for SMTP authentication"), | |
489 value=credentials.get("smtp_username"), | |
490 required=True, | |
491 ) | |
492 ) | |
493 form.addField( | |
494 data_form.Field( | |
495 fieldType="text-private", | |
496 var="smtp_password", | |
497 label="SMTP Password", | |
498 desc=D_("Password for SMTP authentication"), | |
499 value=credentials.get("smtp_password"), | |
500 required=True, | |
501 ) | |
502 ) | |
503 | |
504 return bool(credentials), form | |
505 | |
506 def validate_field( | |
507 self, | |
508 form: data_form.Form, | |
509 key: str, | |
510 field_type: str, | |
511 min_value: int | None = None, | |
512 max_value: int | None = None, | |
513 default: str | int | None = None, | |
514 ) -> None: | |
515 """Validate a single field. | |
516 | |
517 @param form: The form containing the fields. | |
518 @param key: The key of the field to validate. | |
519 @param field_type: The expected type of the field value. | |
520 @param min_value: Optional minimum value for integer fields. | |
521 @param max_value: Optional maximum value for integer fields. | |
522 @param default: Default value to use if the field is missing. | |
523 @raise StanzaError: If the field value is invalid or missing. | |
524 """ | |
525 field = form.fields.get(key) | |
526 if field is None: | |
527 if default is None: | |
528 raise StanzaError("bad-request", text=f"{key} is required") | |
529 field = data_form.Field(var=key, value=str(default)) | |
530 form.addField(field) | |
531 | |
532 value = field.value | |
533 if field_type == "int": | |
534 try: | |
535 value = int(value) | |
536 if (min_value is not None and value < min_value) or ( | |
537 max_value is not None and value > max_value | |
538 ): | |
539 raise ValueError | |
540 except (ValueError, TypeError): | |
541 raise StanzaError("bad-request", text=f"Invalid value for {key}: {value}") | |
542 elif field_type == "str": | |
543 if not isinstance(value, str): | |
544 raise StanzaError("bad-request", text=f"Invalid value for {key}: {value}") | |
545 | |
546 # Basic email validation for user_email field | |
547 if key == "user_email": | |
548 # XXX: This is a minimal check. A complete email validation is notoriously | |
549 # difficult. | |
550 if not email_pattern.match(value): | |
551 raise StanzaError( | |
552 "bad-request", text=f"Invalid email address: {value}" | |
553 ) | |
554 | |
555 def validate_imap_smtp_form(self, submit_form: data_form.Form) -> None: | |
556 """Validate the submitted IMAP/SMTP credentials form. | |
557 | |
558 @param submit_form: The submitted form containing IMAP/SMTP credentials. | |
559 @raise StanzaError: If any of the values are invalid. | |
560 """ | |
561 # Validate identity fields | |
562 self.validate_field(submit_form, "user_name", "str") | |
563 self.validate_field(submit_form, "user_email", "str") | |
564 | |
565 # Validate IMAP fields | |
566 self.validate_field(submit_form, "imap_host", "str") | |
567 self.validate_field( | |
568 submit_form, "imap_port", "int", min_value=1, max_value=65535, default=993 | |
569 ) | |
570 self.validate_field(submit_form, "imap_username", "str") | |
571 self.validate_field(submit_form, "imap_password", "str") | |
572 | |
573 # Validate SMTP fields | |
574 self.validate_field(submit_form, "smtp_host", "str") | |
575 self.validate_field( | |
576 submit_form, "smtp_port", "int", min_value=1, max_value=65535, default=587 | |
577 ) | |
578 self.validate_field(submit_form, "smtp_username", "str") | |
579 self.validate_field(submit_form, "smtp_password", "str") | |
580 | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
581 def email_to_jid( |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
582 self, |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
583 client: SatXMPPEntity, |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
584 user_email: str, |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
585 user_jid: jid.JID, |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
586 email_name: str, |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
587 email_addr: str, |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
588 ) -> tuple[jid.JID, str | None]: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
589 """Convert an email address to a JID and extract the name if present. |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
590 |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
591 @param client: Client session. |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
592 @param user_email: Email address of the gateway user. |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
593 @param user_jid: JID of the gateway user. |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
594 @param email_name: Email associated name. |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
595 @param email_addr: Email address. |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
596 @return: Tuple of JID and name (if present). |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
597 """ |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
598 email_name = email_name.strip() |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
599 if email_name.startswith("xmpp:"): |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
600 return jid.JID(email_name[5:]), None |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
601 elif email_addr == user_email: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
602 return (user_jid, None) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
603 else: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
604 return ( |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
605 jid.JID(None, (self._e.escape(email_addr), client.jid.host, None)), |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
606 email_name or None, |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
607 ) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
608 |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
609 async def on_new_email( |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
610 self, user_data: UserData, user_jid: jid.JID, email: EmailMessage |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
611 ) -> None: |
4303 | 612 """Called when a new message has been received. |
613 | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
614 @param user_data: user data, used to map registered user email to corresponding |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
615 jid. |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
616 @param user_jid: JID of the recipient. |
4303 | 617 @param email: Parsed email. |
618 """ | |
619 assert self.client is not None | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
620 user_email = user_data.credentials["user_email"] |
4303 | 621 name, email_addr = parseaddr(email["from"]) |
622 email_addr = email_addr.lower() | |
623 from_jid = jid.JID(None, (self._e.escape(email_addr), self.client.jid.host, None)) | |
624 | |
625 # Get the email body | |
626 body_mime = email.get_body(("plain",)) | |
627 if body_mime is not None: | |
628 charset = body_mime.get_content_charset() or "utf-8" | |
629 body = body_mime.get_payload(decode=True).decode(charset, errors="replace") | |
630 else: | |
631 log.warning(f"No body found in email:\n{email}") | |
632 body = "" | |
633 | |
634 # Decode the subject | |
635 subject = email.get("subject") | |
636 if subject: | |
637 decoded_subject = decode_header(subject) | |
638 subject = "".join( | |
639 [ | |
640 part.decode(encoding or "utf-8") if isinstance(part, bytes) else part | |
641 for part, encoding in decoded_subject | |
642 ] | |
643 ).strip() | |
644 else: | |
645 subject = None | |
646 | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
647 # Parse recipient fields |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
648 kwargs = {} |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
649 for field in RECIPIENT_FIELDS: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
650 email_addresses = email.get_all(field) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
651 if email_addresses: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
652 jids_and_names = [ |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
653 self.email_to_jid(self.client, user_email, user_jid, name, addr) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
654 for name, addr in getaddresses(email_addresses) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
655 ] |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
656 kwargs[field] = [ |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
657 AddressType(jid=jid, desc=name) for jid, name in jids_and_names |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
658 ] |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
659 |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
660 # At least "to" header should be set, so kwargs should never be empty |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
661 assert kwargs |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
662 addresses_data = AddressesData(**kwargs) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
663 |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
664 # Parse reply-to field |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
665 reply_to_addresses = email.get_all("reply-to") |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
666 if reply_to_addresses: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
667 jids_with_names = [ |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
668 self.email_to_jid(self.client, user_email, user_jid, name, addr) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
669 for name, addr in getaddresses(reply_to_addresses) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
670 ] |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
671 addresses_data.replyto = [ |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
672 AddressType(jid=jid, desc=name) for jid, name in jids_with_names |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
673 ] |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
674 |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
675 # Set noreply flag |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
676 # The is no flag to indicate a no-reply message, so we check common user parts in |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
677 # from and reply-to headers. |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
678 from_addresses = [email_addr] |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
679 if reply_to_addresses: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
680 from_addresses.extend( |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
681 addr for a in reply_to_addresses if (addr := parseaddr(a)[1]) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
682 ) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
683 for from_address in from_addresses: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
684 from_user_part = from_address.split("@", 1)[0].lower() |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
685 if from_user_part in ( |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
686 "no-reply", |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
687 "noreply", |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
688 "do-not-reply", |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
689 "donotreply", |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
690 "notification", |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
691 "notifications", |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
692 ): |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
693 addresses_data.noreply = True |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
694 break |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
695 |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
696 if ( |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
697 not addresses_data.replyto |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
698 and not addresses_data.noreply |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
699 and not addresses_data.cc |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
700 and not addresses_data.bcc |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
701 and addresses_data.to == [AddressType(jid=user_jid)] |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
702 ): |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
703 # The main recipient is the only one, and there is no other metadata: there is |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
704 # no need to add addresses metadata. |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
705 extra = None |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
706 else: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
707 for address in addresses_data.addresses: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
708 if address.jid and ( |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
709 address.jid == user_jid or address.jid.host == str(self.client.jid) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
710 ): |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
711 # Those are email address, and have been delivered by the sender, |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
712 # other JID addresses will have to be delivered by us. |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
713 address.delivered = True |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
714 |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
715 extra = { |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
716 "addresses": addresses_data.model_dump(mode="json", exclude_none=True) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
717 } |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
718 |
4303 | 719 client = self.client.get_virtual_client(from_jid) |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
720 await client.sendMessage( |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
721 user_jid, |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
722 {"": body}, |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
723 {"": subject} if subject else None, |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
724 extra=extra, |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
725 ) |
4303 | 726 |
727 async def connect_imap(self, from_jid: jid.JID, user_data: UserData) -> None: | |
728 """Connect to IMAP service. | |
729 | |
730 [self.on_new_email] will be used as callback on new messages. | |
731 | |
732 @param from_jid: JID of the user associated with given credentials. | |
733 @param credentials: Email credentials. | |
734 """ | |
735 credentials = user_data.credentials | |
736 | |
737 connected = defer.Deferred() | |
738 factory = IMAPClientFactory( | |
739 user_data, | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
740 partial(self.on_new_email, user_data, from_jid.userhostJID()), |
4303 | 741 connected, |
742 ) | |
743 reactor.connectTCP( | |
744 credentials["imap_host"], int(credentials["imap_port"]), factory | |
745 ) | |
746 await connected | |
747 | |
748 async def _on_registration_submit( | |
749 self, | |
750 client: SatXMPPEntity, | |
751 iq_elt: domish.Element, | |
752 submit_form: data_form.Form | None, | |
753 ) -> bool | None: | |
754 """Handle registration submit request. | |
755 | |
756 Submit form is validated, and credentials are stored. | |
757 @param client: client session. | |
758 iq_elt: IQ stanza of the submission request. | |
759 submit_form: submit form. | |
760 @return: True if successful. | |
761 None if the callback is not relevant for this request. | |
762 """ | |
763 if client != self.client: | |
764 return | |
765 assert self.storage is not None | |
766 from_jid = jid.JID(iq_elt["from"]).userhostJID() | |
767 | |
768 if submit_form is None: | |
769 # This is an unregistration request. | |
770 try: | |
771 user_data = self.users_data[from_jid] | |
772 except KeyError: | |
773 pass | |
774 else: | |
775 if user_data.imap_client is not None: | |
776 try: | |
777 await user_data.imap_client.logout() | |
778 except Exception: | |
779 log.exception(f"Can't log out {from_jid} from IMAP server.") | |
780 key = KEY_CREDENTIALS.format(from_jid=from_jid) | |
781 await self.storage.adel(key) | |
782 log.info(f"{from_jid} unregistered from this gateway.") | |
783 return True | |
784 | |
785 self.validate_imap_smtp_form(submit_form) | |
786 credentials = {key: field.value for key, field in submit_form.fields.items()} | |
787 user_data = self.users_data.get(from_jid) | |
788 if user_data is None: | |
789 # The user is not in cache, we cache current credentials. | |
790 user_data = self.users_data[from_jid] = UserData(credentials=credentials) | |
791 else: | |
792 # The user is known, we update credentials. | |
793 user_data.credentials = credentials | |
794 key = KEY_CREDENTIALS.format(from_jid=from_jid) | |
795 try: | |
796 await self.connect_imap(from_jid, user_data) | |
797 except Exception as e: | |
798 log.warning(f"Can't connect to IMAP server for {from_jid}") | |
799 credentials["imap_success"] = False | |
800 await self.storage.aset(key, credentials) | |
801 raise e | |
802 else: | |
803 log.debug(f"Connection successful to IMAP server for {from_jid}") | |
804 credentials["imap_success"] = True | |
805 await self.storage.aset(key, credentials) | |
806 return True | |
807 | |
808 | |
809 @implementer(iwokkel.IDisco) | |
810 class EmailGatewayHandler(XMPPHandler): | |
811 | |
812 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): | |
813 return [] | |
814 | |
815 def getDiscoItems(self, requestor, target, nodeIdentifier=""): | |
816 return [] |