Mercurial > libervia-backend
annotate libervia/backend/plugins/plugin_comp_email_gateway/__init__.py @ 4314:6a70fcd93a7a
plugin XEP-0131: Stanza Headers and Internet Metadata implementation:
- SHIM is now supported and put in `msg_data["extra"]["headers"]`.
- `Keywords` are converted from and to list of string in `msg_data["extra"]["keywords"]`
field (if present in headers on message sending, values are merged).
- Python minimal version upgraded to 3.11 due to use of `StrEnum`.
rel 451
author | Goffi <goffi@goffi.org> |
---|---|
date | Sat, 28 Sep 2024 15:56:04 +0200 |
parents | b56b1eae7994 |
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 [] |