Mercurial > libervia-backend
annotate libervia/backend/plugins/plugin_comp_email_gateway/__init__.py @ 4342:17fa953c8cd7
core (types): improve `SatXMPPEntity` core type and type hints.
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 13 Jan 2025 01:23:10 +0100 |
parents | 7c0b7ecb816f |
children | 54df67d5646c |
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 |
4337
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
24 import hashlib |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
25 from pathlib import Path |
4303 | 26 import re |
4337
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
27 import shutil |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
28 import tempfile |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
29 from typing import TYPE_CHECKING, NamedTuple, cast |
4303 | 30 |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
31 from pydantic import BaseModel |
4303 | 32 from twisted.internet import defer, reactor |
4337
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
33 from twisted.internet.threads import deferToThread |
4303 | 34 from twisted.mail import smtp |
35 from twisted.words.protocols.jabber import jid | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
36 from twisted.words.protocols.jabber import error as jabber_error |
4303 | 37 from twisted.words.protocols.jabber.error import StanzaError |
38 from twisted.words.protocols.jabber.xmlstream import XMPPHandler | |
39 from twisted.words.xish import domish | |
40 from wokkel import data_form, disco, iwokkel | |
41 from zope.interface import implementer | |
42 | |
43 from libervia.backend.core import exceptions | |
44 from libervia.backend.core.constants import Const as C | |
4337
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
45 from libervia.backend.core.core_types import SatXMPPComponent, SatXMPPEntity |
4303 | 46 from libervia.backend.core.i18n import D_, _ |
47 from libervia.backend.core.log import getLogger | |
48 from libervia.backend.memory.persistent import LazyPersistentBinaryDict | |
49 from libervia.backend.memory.sqla import select | |
50 from libervia.backend.memory.sqla_mapping import PrivateIndBin | |
51 from libervia.backend.models.core import MessageData | |
4338
7c0b7ecb816f
component email gateway: Add a pubsub service:
Goffi <goffi@goffi.org>
parents:
4337
diff
changeset
|
52 from libervia.backend.plugins.plugin_comp_email_gateway.pubsub_service import ( |
7c0b7ecb816f
component email gateway: Add a pubsub service:
Goffi <goffi@goffi.org>
parents:
4337
diff
changeset
|
53 EmailGWPubsubService, |
7c0b7ecb816f
component email gateway: Add a pubsub service:
Goffi <goffi@goffi.org>
parents:
4337
diff
changeset
|
54 ) |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
55 from libervia.backend.plugins.plugin_xep_0033 import ( |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
56 AddressType, |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
57 AddressesData, |
4337
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
58 RECIPIENT_FIELDS, |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
59 ) |
4303 | 60 from libervia.backend.plugins.plugin_xep_0077 import XEP_0077 |
61 from libervia.backend.plugins.plugin_xep_0106 import XEP_0106 | |
4337
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
62 from libervia.backend.plugins.plugin_xep_0131 import HeadersData, Urgency, XEP_0131 |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
63 from libervia.backend.plugins.plugin_xep_0498 import XEP_0498 |
4303 | 64 from libervia.backend.tools.utils import aio |
65 | |
4337
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
66 from .imap import IMAPClientFactory |
4303 | 67 from .models import Credentials, UserData |
4337
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
68 |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
69 if TYPE_CHECKING: |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
70 from libervia.backend.core.main import LiberviaBackend |
4303 | 71 |
72 | |
73 log = getLogger(__name__) | |
74 | |
75 IMPORT_NAME = "email-gateway" | |
76 NAME = "Libervia Email Gateway" | |
77 | |
78 PLUGIN_INFO = { | |
79 C.PI_NAME: "Email Gateway Component", | |
80 C.PI_IMPORT_NAME: IMPORT_NAME, | |
81 C.PI_MODES: [C.PLUG_MODE_COMPONENT], | |
82 C.PI_TYPE: C.PLUG_TYPE_ENTRY_POINT, | |
83 C.PI_PROTOCOLS: [], | |
4337
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
84 C.PI_DEPENDENCIES: ["XEP-0033", "XEP-0077", "XEP-0106", "XEP-0498"], |
4303 | 85 C.PI_RECOMMENDATIONS: [], |
86 C.PI_MAIN: "EmailGatewayComponent", | |
87 C.PI_HANDLER: C.BOOL_TRUE, | |
88 C.PI_DESCRIPTION: D_( | |
89 "Gateway to handle email. Usual emails are handled as message, while mailing " | |
90 "lists are converted to pubsub blogs." | |
91 ), | |
92 } | |
93 | |
94 CONF_SECTION = f"component {IMPORT_NAME}" | |
95 PREFIX_KEY_CREDENTIALS = "CREDENTIALS_" | |
96 KEY_CREDENTIALS = f"{PREFIX_KEY_CREDENTIALS}{{from_jid}}" | |
97 | |
98 email_pattern = re.compile(r"[^@]+@[^@]+\.[^@]+") | |
99 | |
100 | |
4337
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
101 class FileMetadata(NamedTuple): |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
102 path: Path |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
103 hash: str |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
104 size: int |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
105 |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
106 |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
107 class SendMailExtra(BaseModel): |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
108 addresses: AddressesData | None = None |
4317
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
109 headers: HeadersData | None = None |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
110 |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
111 |
4303 | 112 class EmailGatewayComponent: |
113 IMPORT_NAME = IMPORT_NAME | |
114 verbose = 0 | |
115 | |
4337
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
116 def __init__(self, host: "LiberviaBackend") -> None: |
4303 | 117 self.host = host |
4337
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
118 self.client: SatXMPPComponent | None = None |
4303 | 119 self.initalized = False |
120 self.storage: LazyPersistentBinaryDict | None = None | |
121 self._iq_register = cast(XEP_0077, host.plugins["XEP-0077"]) | |
122 self._iq_register.register_handler( | |
123 self._on_registration_form, self._on_registration_submit | |
124 ) | |
125 self._e = cast(XEP_0106, host.plugins["XEP-0106"]) | |
4317
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
126 self._shim = cast(XEP_0131, host.plugins["XEP-0131"]) |
4337
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
127 self._pfs = cast(XEP_0498, host.plugins["XEP-0498"]) |
4303 | 128 # TODO: For the moment, all credentials are kept in cache; we should only keep the |
129 # X latest. | |
130 self.users_data: dict[jid.JID, UserData] = {} | |
4337
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
131 self.files_path = self.host.get_local_path(None, C.FILES_DIR) |
4303 | 132 host.trigger.add_with_check( |
133 "message_received", self, self._message_received_trigger, priority=-1000 | |
134 ) | |
135 | |
136 async def _init(self) -> None: | |
137 """Initialisation done after profile is connected""" | |
138 assert self.client is not None | |
139 self.client.identities.append(disco.DiscoIdentity("gateway", "smtp", NAME)) | |
140 self.storage = LazyPersistentBinaryDict(IMPORT_NAME, self.client.profile) | |
141 await self.connect_registered_users() | |
142 | |
143 @aio | |
144 async def get_registered_users(self) -> dict[jid.JID, Credentials]: | |
145 """Retrieve credentials for all registered users | |
146 | |
147 @return: a mapping from user JID to credentials data. | |
148 """ | |
149 assert self.client is not None | |
150 profile_id = self.host.memory.storage.profiles[self.client.profile] | |
151 async with self.host.memory.storage.session() as session: | |
152 query = select(PrivateIndBin).where( | |
153 PrivateIndBin.profile_id == profile_id, | |
154 PrivateIndBin.namespace == IMPORT_NAME, | |
155 PrivateIndBin.key.startswith(PREFIX_KEY_CREDENTIALS), | |
156 ) | |
157 result = await session.execute(query) | |
158 return { | |
159 jid.JID(p.key[len(PREFIX_KEY_CREDENTIALS) :]): p.value | |
160 for p in result.scalars() | |
161 } | |
162 | |
163 async def connect_registered_users(self) -> None: | |
164 """Connected users already registered to the gateway.""" | |
165 registered_data = await self.get_registered_users() | |
166 for user_jid, credentials in registered_data.items(): | |
167 user_data = self.users_data[user_jid] = UserData(credentials=credentials) | |
168 if not credentials["imap_success"]: | |
169 log.warning( | |
170 f"Ignoring unsuccessful IMAP credentials of {user_jid}. This user " | |
171 "won't receive message from this gateway." | |
172 ) | |
173 else: | |
174 try: | |
175 await self.connect_imap(user_jid, user_data) | |
176 except Exception as e: | |
177 log.warning(f"Can't connect {user_jid} to IMAP: {e}.") | |
178 else: | |
179 log.debug(f"Connection to IMAP server successful for {user_jid}.") | |
180 | |
4338
7c0b7ecb816f
component email gateway: Add a pubsub service:
Goffi <goffi@goffi.org>
parents:
4337
diff
changeset
|
181 def get_handler(self, __) -> tuple[XMPPHandler, XMPPHandler]: |
7c0b7ecb816f
component email gateway: Add a pubsub service:
Goffi <goffi@goffi.org>
parents:
4337
diff
changeset
|
182 return EmailGatewayHandler(), EmailGWPubsubService(self) |
4303 | 183 |
184 async def profile_connecting(self, client: SatXMPPEntity) -> None: | |
4337
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
185 assert isinstance(client, SatXMPPComponent) |
4303 | 186 self.client = client |
187 if not self.initalized: | |
188 await self._init() | |
189 self.initalized = True | |
190 | |
191 def _message_received_trigger( | |
192 self, | |
193 client: SatXMPPEntity, | |
194 message_elt: domish.Element, | |
195 post_treat: defer.Deferred, | |
196 ) -> bool: | |
197 """add the gateway workflow on post treatment""" | |
198 if client != self.client: | |
199 return True | |
200 post_treat.addCallback( | |
201 lambda mess_data: defer.ensureDeferred( | |
202 self.on_message(client, mess_data, message_elt) | |
203 ) | |
204 ) | |
205 return True | |
206 | |
207 async def on_message( | |
208 self, client: SatXMPPEntity, mess_data: MessageData, message_elt: domish.Element | |
209 ) -> dict: | |
210 """Called once message has been parsed | |
211 | |
212 @param client: Client session. | |
213 @param mess_data: Message data. | |
214 @return: Message data. | |
215 """ | |
216 if client != self.client: | |
217 return mess_data | |
218 from_jid = mess_data["from"].userhostJID() | |
4317
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
219 extra_kw = {} |
4303 | 220 if mess_data["type"] not in ("chat", "normal"): |
221 log.warning(f"ignoring message with unexpected type: {mess_data}") | |
222 return mess_data | |
223 if not client.is_local(from_jid): | |
224 log.warning(f"ignoring non local message: {mess_data}") | |
225 return mess_data | |
226 if not mess_data["to"].user: | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
227 addresses = mess_data["extra"].get("addresses") |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
228 if not addresses: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
229 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
|
230 return mess_data |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
231 else: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
232 to_email = None |
4317
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
233 extra_kw["addresses"] = addresses |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
234 else: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
235 try: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
236 to_email = self._e.unescape(mess_data["to"].user) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
237 except ValueError: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
238 raise exceptions.DataError( |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
239 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
|
240 ) |
4303 | 241 |
4317
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
242 self._shim.move_keywords_to_headers(mess_data["extra"]) |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
243 headers = mess_data["extra"].get("headers") |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
244 if headers: |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
245 extra_kw["headers"] = headers |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
246 |
4303 | 247 try: |
248 body_lang, body = next(iter(mess_data["message"].items())) | |
249 except (KeyError, StopIteration): | |
250 log.warning(f"No body found: {mess_data}") | |
251 body_lang, body = "", "" | |
252 try: | |
253 subject_lang, subject = next(iter(mess_data["subject"].items())) | |
254 except (KeyError, StopIteration): | |
255 subject_lang, subject = "", None | |
256 | |
257 if not body and not subject: | |
258 log.warning(f"Ignoring empty message: {mess_data}") | |
259 return mess_data | |
260 | |
261 try: | |
262 await self.send_email( | |
263 from_jid=from_jid, | |
264 to_email=to_email, | |
265 body=body, | |
266 subject=subject, | |
4317
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
267 extra=SendMailExtra(**extra_kw) if extra_kw else None, |
4303 | 268 ) |
269 except exceptions.UnknownEntityError: | |
270 log.warning(f"Can't send message, user {from_jid} is not registered.") | |
271 message_error_elt = StanzaError( | |
272 "subscription-required", | |
273 text="User need to register to the gateway before sending emails.", | |
274 ).toResponse(message_elt) | |
275 await client.a_send(message_error_elt) | |
276 raise exceptions.CancelError("User not registered.") | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
277 except StanzaError as e: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
278 log.warning("Can't send message: {e}") |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
279 message_error_elt = e.toResponse(message_elt) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
280 await client.a_send(message_error_elt) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
281 raise exceptions.CancelError("Can't send message: {e}") |
4303 | 282 |
283 return mess_data | |
284 | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
285 def jid_to_email( |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
286 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
|
287 ) -> str: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
288 """Convert a JID to an email address. |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
289 |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
290 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
|
291 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
|
292 |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
293 @param address_jid: JID of the recipient. |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
294 @param credentials: Sender credentials. |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
295 @return: Email address. |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
296 """ |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
297 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
|
298 return self._e.unescape(address_jid.user) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
299 else: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
300 email_address = credentials["user_email"] |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
301 if address_jid: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
302 email_address = formataddr((f"xmpp:{address_jid}", email_address)) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
303 return email_address |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
304 |
4303 | 305 async def send_email( |
306 self, | |
307 from_jid: jid.JID, | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
308 to_email: str | None, |
4303 | 309 body: str, |
310 subject: str | None, | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
311 extra: SendMailExtra | None = None, |
4303 | 312 ) -> None: |
313 """Send an email using sender credentials. | |
314 | |
315 Credentials will be retrieve from cache, or database. | |
316 | |
317 @param from_jid: Bare JID of the sender. | |
318 @param to_email: Email address of the destinee. | |
319 @param body: Body of the email. | |
320 @param subject: Subject of the email. | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
321 @param extra: Extra data. |
4303 | 322 |
323 @raise exceptions.UnknownEntityError: Credentials for "from_jid" can't be found. | |
324 """ | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
325 assert self.client is not None |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
326 if extra is None: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
327 extra = SendMailExtra() |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
328 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
|
329 raise exceptions.InternalError( |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
330 '"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
|
331 ) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
332 |
4303 | 333 # We need a bare jid. |
334 assert self.storage is not None | |
335 assert not from_jid.resource | |
336 try: | |
337 user_data = self.users_data[from_jid] | |
338 except KeyError: | |
339 key = KEY_CREDENTIALS.format(from_jid=from_jid) | |
340 credentials = await self.storage.get(key) | |
341 if credentials is None: | |
342 raise exceptions.UnknownEntityError( | |
343 f"No credentials found for {from_jid}." | |
344 ) | |
345 self.users_data[from_jid] = UserData(credentials) | |
346 else: | |
347 credentials = user_data.credentials | |
348 | |
349 msg = MIMEText(body, "plain", "UTF-8") | |
350 if subject is not None: | |
351 msg["Subject"] = subject | |
352 msg["From"] = formataddr( | |
353 (credentials["user_name"] or None, credentials["user_email"]) | |
354 ) | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
355 if extra.addresses: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
356 assert extra.addresses.to |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
357 main_to_address = extra.addresses.to[0] |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
358 assert main_to_address.jid |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
359 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
|
360 for field in RECIPIENT_FIELDS: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
361 addresses = getattr(extra.addresses, field) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
362 if not addresses: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
363 continue |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
364 for address in addresses: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
365 if not address.delivered and ( |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
366 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
|
367 ): |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
368 log.warning( |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
369 "Received undelivered message to external JID, this is not " |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
370 "allowed! Cancelling the message sending." |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
371 ) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
372 stanza_err = jabber_error.StanzaError( |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
373 "forbidden", |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
374 text="Multicasting (XEP-0033 addresses) can only be used " |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
375 "with JID from this gateway, not external ones. " |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
376 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
|
377 "should be delivered by server instead.", |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
378 ) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
379 raise stanza_err |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
380 email_addresses = [ |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
381 self.jid_to_email(self.client, address.jid, credentials) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
382 for address in addresses |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
383 if address.jid |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
384 ] |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
385 if email_addresses: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
386 msg[field.upper()] = ", ".join(email_addresses) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
387 else: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
388 assert to_email is not None |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
389 msg["To"] = to_email |
4303 | 390 |
391 sender_domain = credentials["user_email"].split("@", 1)[-1] | |
392 | |
4317
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
393 if extra.headers: |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
394 if extra.headers.keywords: |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
395 msg["Keywords"] = extra.headers.keywords |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
396 if extra.headers.urgency: |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
397 urgency = extra.headers.urgency |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
398 if urgency == Urgency.medium: |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
399 importance = "normal" |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
400 else: |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
401 importance = urgency |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
402 msg["Importance"] = importance |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
403 |
4303 | 404 await smtp.sendmail( |
405 credentials["smtp_host"].encode(), | |
406 credentials["user_email"].encode(), | |
407 [to_email.encode()], | |
408 msg.as_bytes(), | |
409 senderDomainName=sender_domain, | |
410 port=int(credentials["smtp_port"]), | |
411 username=credentials["smtp_username"].encode(), | |
412 password=credentials["smtp_password"].encode(), | |
413 requireAuthentication=True, | |
414 # TODO: only STARTTLS is supported right now, implicit TLS should be supported | |
415 # too. | |
416 requireTransportSecurity=True, | |
417 ) | |
418 | |
419 async def _on_registration_form( | |
420 self, client: SatXMPPEntity, iq_elt: domish.Element | |
421 ) -> tuple[bool, data_form.Form] | None: | |
422 if client != self.client: | |
423 return | |
424 assert self.storage is not None | |
425 from_jid = jid.JID(iq_elt["from"]) | |
426 key = KEY_CREDENTIALS.format(from_jid=from_jid.userhost()) | |
427 credentials = await self.storage.get(key) or {} | |
428 | |
429 form = data_form.Form(formType="form", title="IMAP/SMTP Credentials") | |
430 | |
431 # Add instructions | |
432 form.instructions = [ | |
433 D_( | |
434 "Please provide your IMAP and SMTP credentials to configure the " | |
435 "connection." | |
436 ) | |
437 ] | |
438 | |
439 # Add identity fields | |
440 form.addField( | |
441 data_form.Field( | |
442 fieldType="text-single", | |
443 var="user_name", | |
444 label="User Name", | |
445 desc=D_('The display name to use in the "From" field of sent emails.'), | |
446 value=credentials.get("user_name"), | |
447 required=True, | |
448 ) | |
449 ) | |
450 | |
451 form.addField( | |
452 data_form.Field( | |
453 fieldType="text-single", | |
454 var="user_email", | |
455 label="User Email", | |
456 desc=D_('The email address to use in the "From" field of sent emails.'), | |
457 value=credentials.get("user_email"), | |
458 required=True, | |
459 ) | |
460 ) | |
461 | |
462 # Add fields for IMAP credentials | |
463 form.addField( | |
464 data_form.Field( | |
465 fieldType="text-single", | |
466 var="imap_host", | |
467 label="IMAP Host", | |
468 desc=D_("IMAP server hostname or IP address"), | |
469 value=credentials.get("imap_host"), | |
470 required=True, | |
471 ) | |
472 ) | |
473 form.addField( | |
474 data_form.Field( | |
475 fieldType="text-single", | |
476 var="imap_port", | |
477 label="IMAP Port", | |
478 desc=D_("IMAP server port (default: 993)"), | |
479 value=credentials.get("imap_port", "993"), | |
480 ) | |
481 ) | |
482 form.addField( | |
483 data_form.Field( | |
484 fieldType="text-single", | |
485 var="imap_username", | |
486 label="IMAP Username", | |
487 desc=D_("Username for IMAP authentication"), | |
488 value=credentials.get("imap_username"), | |
489 required=True, | |
490 ) | |
491 ) | |
492 form.addField( | |
493 data_form.Field( | |
494 fieldType="text-private", | |
495 var="imap_password", | |
496 label="IMAP Password", | |
497 desc=D_("Password for IMAP authentication"), | |
498 value=credentials.get("imap_password"), | |
499 required=True, | |
500 ) | |
501 ) | |
502 | |
503 # Add fields for SMTP credentials | |
504 form.addField( | |
505 data_form.Field( | |
506 fieldType="text-single", | |
507 var="smtp_host", | |
508 label="SMTP Host", | |
509 desc=D_("SMTP server hostname or IP address"), | |
510 value=credentials.get("smtp_host"), | |
511 required=True, | |
512 ) | |
513 ) | |
514 form.addField( | |
515 data_form.Field( | |
516 fieldType="text-single", | |
517 var="smtp_port", | |
518 label="SMTP Port", | |
519 desc=D_("SMTP server port (default: 587)"), | |
520 value=credentials.get("smtp_port", "587"), | |
521 ) | |
522 ) | |
523 form.addField( | |
524 data_form.Field( | |
525 fieldType="text-single", | |
526 var="smtp_username", | |
527 label="SMTP Username", | |
528 desc=D_("Username for SMTP authentication"), | |
529 value=credentials.get("smtp_username"), | |
530 required=True, | |
531 ) | |
532 ) | |
533 form.addField( | |
534 data_form.Field( | |
535 fieldType="text-private", | |
536 var="smtp_password", | |
537 label="SMTP Password", | |
538 desc=D_("Password for SMTP authentication"), | |
539 value=credentials.get("smtp_password"), | |
540 required=True, | |
541 ) | |
542 ) | |
543 | |
544 return bool(credentials), form | |
545 | |
546 def validate_field( | |
547 self, | |
548 form: data_form.Form, | |
549 key: str, | |
550 field_type: str, | |
551 min_value: int | None = None, | |
552 max_value: int | None = None, | |
553 default: str | int | None = None, | |
554 ) -> None: | |
555 """Validate a single field. | |
556 | |
557 @param form: The form containing the fields. | |
558 @param key: The key of the field to validate. | |
559 @param field_type: The expected type of the field value. | |
560 @param min_value: Optional minimum value for integer fields. | |
561 @param max_value: Optional maximum value for integer fields. | |
562 @param default: Default value to use if the field is missing. | |
563 @raise StanzaError: If the field value is invalid or missing. | |
564 """ | |
565 field = form.fields.get(key) | |
566 if field is None: | |
567 if default is None: | |
568 raise StanzaError("bad-request", text=f"{key} is required") | |
569 field = data_form.Field(var=key, value=str(default)) | |
570 form.addField(field) | |
571 | |
572 value = field.value | |
573 if field_type == "int": | |
574 try: | |
575 value = int(value) | |
576 if (min_value is not None and value < min_value) or ( | |
577 max_value is not None and value > max_value | |
578 ): | |
579 raise ValueError | |
580 except (ValueError, TypeError): | |
581 raise StanzaError("bad-request", text=f"Invalid value for {key}: {value}") | |
582 elif field_type == "str": | |
583 if not isinstance(value, str): | |
584 raise StanzaError("bad-request", text=f"Invalid value for {key}: {value}") | |
585 | |
586 # Basic email validation for user_email field | |
587 if key == "user_email": | |
588 # XXX: This is a minimal check. A complete email validation is notoriously | |
589 # difficult. | |
590 if not email_pattern.match(value): | |
591 raise StanzaError( | |
592 "bad-request", text=f"Invalid email address: {value}" | |
593 ) | |
594 | |
595 def validate_imap_smtp_form(self, submit_form: data_form.Form) -> None: | |
596 """Validate the submitted IMAP/SMTP credentials form. | |
597 | |
598 @param submit_form: The submitted form containing IMAP/SMTP credentials. | |
599 @raise StanzaError: If any of the values are invalid. | |
600 """ | |
601 # Validate identity fields | |
602 self.validate_field(submit_form, "user_name", "str") | |
603 self.validate_field(submit_form, "user_email", "str") | |
604 | |
605 # Validate IMAP fields | |
606 self.validate_field(submit_form, "imap_host", "str") | |
607 self.validate_field( | |
608 submit_form, "imap_port", "int", min_value=1, max_value=65535, default=993 | |
609 ) | |
610 self.validate_field(submit_form, "imap_username", "str") | |
611 self.validate_field(submit_form, "imap_password", "str") | |
612 | |
613 # Validate SMTP fields | |
614 self.validate_field(submit_form, "smtp_host", "str") | |
615 self.validate_field( | |
616 submit_form, "smtp_port", "int", min_value=1, max_value=65535, default=587 | |
617 ) | |
618 self.validate_field(submit_form, "smtp_username", "str") | |
619 self.validate_field(submit_form, "smtp_password", "str") | |
620 | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
621 def email_to_jid( |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
622 self, |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
623 client: SatXMPPEntity, |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
624 user_email: str, |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
625 user_jid: jid.JID, |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
626 email_name: str, |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
627 email_addr: str, |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
628 ) -> tuple[jid.JID, str | None]: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
629 """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
|
630 |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
631 @param client: Client session. |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
632 @param user_email: Email address of the gateway user. |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
633 @param user_jid: JID of the gateway user. |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
634 @param email_name: Email associated name. |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
635 @param email_addr: Email address. |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
636 @return: Tuple of JID and name (if present). |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
637 """ |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
638 email_name = email_name.strip() |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
639 if email_name.startswith("xmpp:"): |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
640 return jid.JID(email_name[5:]), None |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
641 elif email_addr == user_email: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
642 return (user_jid, None) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
643 else: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
644 return ( |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
645 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
|
646 email_name or None, |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
647 ) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
648 |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
649 async def on_new_email( |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
650 self, user_data: UserData, user_jid: jid.JID, email: EmailMessage |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
651 ) -> None: |
4303 | 652 """Called when a new message has been received. |
653 | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
654 @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
|
655 jid. |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
656 @param user_jid: JID of the recipient. |
4303 | 657 @param email: Parsed email. |
658 """ | |
659 assert self.client is not None | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
660 user_email = user_data.credentials["user_email"] |
4303 | 661 name, email_addr = parseaddr(email["from"]) |
662 email_addr = email_addr.lower() | |
663 from_jid = jid.JID(None, (self._e.escape(email_addr), self.client.jid.host, None)) | |
664 | |
665 # Get the email body | |
666 body_mime = email.get_body(("plain",)) | |
667 if body_mime is not None: | |
668 charset = body_mime.get_content_charset() or "utf-8" | |
669 body = body_mime.get_payload(decode=True).decode(charset, errors="replace") | |
670 else: | |
671 log.warning(f"No body found in email:\n{email}") | |
672 body = "" | |
673 | |
674 # Decode the subject | |
675 subject = email.get("subject") | |
676 if subject: | |
677 decoded_subject = decode_header(subject) | |
678 subject = "".join( | |
679 [ | |
680 part.decode(encoding or "utf-8") if isinstance(part, bytes) else part | |
681 for part, encoding in decoded_subject | |
682 ] | |
683 ).strip() | |
684 else: | |
685 subject = None | |
686 | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
687 # Parse recipient fields |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
688 kwargs = {} |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
689 for field in RECIPIENT_FIELDS: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
690 email_addresses = email.get_all(field) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
691 if email_addresses: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
692 jids_and_names = [ |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
693 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
|
694 for name, addr in getaddresses(email_addresses) |
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 kwargs[field] = [ |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
697 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
|
698 ] |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
699 |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
700 # 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
|
701 assert kwargs |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
702 addresses_data = AddressesData(**kwargs) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
703 |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
704 # Parse reply-to field |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
705 reply_to_addresses = email.get_all("reply-to") |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
706 if reply_to_addresses: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
707 jids_with_names = [ |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
708 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
|
709 for name, addr in getaddresses(reply_to_addresses) |
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 addresses_data.replyto = [ |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
712 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
|
713 ] |
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 # Set noreply flag |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
716 # 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
|
717 # from and reply-to headers. |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
718 from_addresses = [email_addr] |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
719 if reply_to_addresses: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
720 from_addresses.extend( |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
721 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
|
722 ) |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
723 for from_address in from_addresses: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
724 from_user_part = from_address.split("@", 1)[0].lower() |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
725 if from_user_part in ( |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
726 "no-reply", |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
727 "noreply", |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
728 "do-not-reply", |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
729 "donotreply", |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
730 "notification", |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
731 "notifications", |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
732 ): |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
733 addresses_data.noreply = True |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
734 break |
4317
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
735 extra = {} |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
736 |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
737 if ( |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
738 not addresses_data.replyto |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
739 and not addresses_data.noreply |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
740 and not addresses_data.cc |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
741 and not addresses_data.bcc |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
742 and addresses_data.to == [AddressType(jid=user_jid)] |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
743 ): |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
744 # 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
|
745 # no need to add addresses metadata. |
4317
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
746 pass |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
747 else: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
748 for address in addresses_data.addresses: |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
749 if address.jid and ( |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
750 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
|
751 ): |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
752 # 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
|
753 # other JID addresses will have to be delivered by us. |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
754 address.delivered = True |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
755 |
4317
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
756 extra["addresses"] = addresses_data.model_dump(mode="json", exclude_none=True) |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
757 |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
758 # We look for interesting headers |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
759 headers = {} |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
760 keywords_headers = email.get_all("keywords") |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
761 if keywords_headers: |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
762 keywords = ",".join(keywords_headers) |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
763 headers["keywords"] = keywords |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
764 |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
765 importance = email["importance"] |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
766 if importance: |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
767 # We convert to urgency |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
768 if importance in ("low", "high"): |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
769 headers["urgency"] = importance |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
770 elif importance == "normal": |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
771 headers["urgency"] = "medium" |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
772 else: |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
773 log.warning("Ignoring invalid importance header: {importance!r}") |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
774 |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
775 if headers: |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
776 extra["headers"] = HeadersData(**headers).model_dump( |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
777 mode="json", exclude_none=True |
055930cc81f9
component email gateway: Add support for XEP-0131 headers:
Goffi <goffi@goffi.org>
parents:
4309
diff
changeset
|
778 ) |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
779 |
4337
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
780 # Handle attachments |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
781 for part in email.iter_attachments(): |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
782 await self.handle_attachment(part, user_jid) |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
783 |
4303 | 784 client = self.client.get_virtual_client(from_jid) |
4337
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
785 |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
786 await client.sendMessage( |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
787 user_jid, |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
788 {"": body}, |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
789 {"": subject} if subject else None, |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
790 extra=extra, |
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
791 ) |
4303 | 792 |
4337
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
793 async def handle_attachment(self, part: EmailMessage, recipient_jid: jid.JID) -> None: |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
794 """Handle an attachment from an email. |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
795 |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
796 @param part: The object representing the attachment. |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
797 @param recipient_jid: JID of the recipient to whom the attachment is being sent. |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
798 """ |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
799 assert self.client is not None |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
800 content_type = part.get_content_type() |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
801 filename = part.get_filename() or "attachment" |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
802 log.debug(f"Handling attachment: {filename} ({content_type})") |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
803 file_metadata = await deferToThread(self._save_attachment, part) |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
804 if file_metadata is not None: |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
805 log.debug(f"Attachment {filename!r} saved to {file_metadata.path}") |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
806 try: |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
807 await self.host.memory.set_file( |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
808 self.client, |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
809 filename, |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
810 file_hash=file_metadata.hash, |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
811 hash_algo="sha-256", |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
812 size=file_metadata.size, |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
813 namespace=PLUGIN_INFO[C.PI_IMPORT_NAME], |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
814 mime_type=content_type, |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
815 owner=recipient_jid, |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
816 ) |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
817 except Exception: |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
818 log.exception(f"Failed to register file {filename!r}") |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
819 |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
820 def _save_attachment(self, part: EmailMessage) -> FileMetadata | None: |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
821 """Save the attachment to files path. |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
822 |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
823 This method must be executed in a thread with deferToThread to avoid blocking the |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
824 reactor with IO operations if the attachment is large. |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
825 |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
826 @param part: The object representing the attachment. |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
827 @return: Attachment data, or None if an error occurs. |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
828 @raises IOError: Can't save the attachment. |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
829 """ |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
830 temp_file = None |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
831 try: |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
832 with tempfile.NamedTemporaryFile(delete=False) as temp_file: |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
833 payload = part.get_payload(decode=True) |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
834 if isinstance(payload, bytes): |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
835 temp_file.write(payload) |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
836 file_hash = hashlib.sha256(payload).hexdigest() |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
837 file_path = self.files_path / file_hash |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
838 shutil.move(temp_file.name, file_path) |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
839 file_size = len(payload) |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
840 return FileMetadata(path=file_path, hash=file_hash, size=file_size) |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
841 else: |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
842 log.warning(f"Can't write payload of type {type(payload)}.") |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
843 return None |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
844 except Exception as e: |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
845 raise IOError(f"Failed to save attachment: {e}") |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
846 finally: |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
847 if temp_file is not None and Path(temp_file.name).exists(): |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
848 Path(temp_file.name).unlink() |
95792a1f26c7
component email gateway: attachments handling:
Goffi <goffi@goffi.org>
parents:
4317
diff
changeset
|
849 |
4303 | 850 async def connect_imap(self, from_jid: jid.JID, user_data: UserData) -> None: |
851 """Connect to IMAP service. | |
852 | |
853 [self.on_new_email] will be used as callback on new messages. | |
854 | |
855 @param from_jid: JID of the user associated with given credentials. | |
856 @param credentials: Email credentials. | |
857 """ | |
858 credentials = user_data.credentials | |
859 | |
860 connected = defer.Deferred() | |
861 factory = IMAPClientFactory( | |
862 user_data, | |
4309
b56b1eae7994
component email gateway: add multicasting:
Goffi <goffi@goffi.org>
parents:
4303
diff
changeset
|
863 partial(self.on_new_email, user_data, from_jid.userhostJID()), |
4303 | 864 connected, |
865 ) | |
866 reactor.connectTCP( | |
867 credentials["imap_host"], int(credentials["imap_port"]), factory | |
868 ) | |
869 await connected | |
870 | |
871 async def _on_registration_submit( | |
872 self, | |
873 client: SatXMPPEntity, | |
874 iq_elt: domish.Element, | |
875 submit_form: data_form.Form | None, | |
876 ) -> bool | None: | |
877 """Handle registration submit request. | |
878 | |
879 Submit form is validated, and credentials are stored. | |
880 @param client: client session. | |
881 iq_elt: IQ stanza of the submission request. | |
882 submit_form: submit form. | |
883 @return: True if successful. | |
884 None if the callback is not relevant for this request. | |
885 """ | |
886 if client != self.client: | |
887 return | |
888 assert self.storage is not None | |
889 from_jid = jid.JID(iq_elt["from"]).userhostJID() | |
890 | |
891 if submit_form is None: | |
892 # This is an unregistration request. | |
893 try: | |
894 user_data = self.users_data[from_jid] | |
895 except KeyError: | |
896 pass | |
897 else: | |
898 if user_data.imap_client is not None: | |
899 try: | |
900 await user_data.imap_client.logout() | |
901 except Exception: | |
902 log.exception(f"Can't log out {from_jid} from IMAP server.") | |
903 key = KEY_CREDENTIALS.format(from_jid=from_jid) | |
904 await self.storage.adel(key) | |
905 log.info(f"{from_jid} unregistered from this gateway.") | |
906 return True | |
907 | |
908 self.validate_imap_smtp_form(submit_form) | |
909 credentials = {key: field.value for key, field in submit_form.fields.items()} | |
910 user_data = self.users_data.get(from_jid) | |
911 if user_data is None: | |
912 # The user is not in cache, we cache current credentials. | |
913 user_data = self.users_data[from_jid] = UserData(credentials=credentials) | |
914 else: | |
915 # The user is known, we update credentials. | |
916 user_data.credentials = credentials | |
917 key = KEY_CREDENTIALS.format(from_jid=from_jid) | |
918 try: | |
919 await self.connect_imap(from_jid, user_data) | |
920 except Exception as e: | |
921 log.warning(f"Can't connect to IMAP server for {from_jid}") | |
922 credentials["imap_success"] = False | |
923 await self.storage.aset(key, credentials) | |
924 raise e | |
925 else: | |
926 log.debug(f"Connection successful to IMAP server for {from_jid}") | |
927 credentials["imap_success"] = True | |
928 await self.storage.aset(key, credentials) | |
929 return True | |
930 | |
931 | |
932 @implementer(iwokkel.IDisco) | |
933 class EmailGatewayHandler(XMPPHandler): | |
934 | |
935 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): | |
936 return [] | |
937 | |
938 def getDiscoItems(self, requestor, target, nodeIdentifier=""): | |
939 return [] |