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