annotate libervia/backend/plugins/plugin_comp_email_gateway/imap.py @ 4401:ae26233b655f default tip

doc (components): Add message cleaning section to email gateway doc: fix 464
author Goffi <goffi@goffi.org>
date Thu, 11 Sep 2025 21:17:51 +0200
parents 7ef21e3e5ac9
children
Ignore whitespace changes - Everywhere: Within whitespace: At end of lines:
rev   line source
4303
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
1 #!/usr/bin/env python3
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
2
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
3 # Libervia Email Gateway Component
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
4 # Copyright (C) 2009-2024 Jérôme Poisson (goffi@goffi.org)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
5
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
6 # This program is free software: you can redistribute it and/or modify
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
7 # it under the terms of the GNU Affero General Public License as published by
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
8 # the Free Software Foundation, either version 3 of the License, or
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
9 # (at your option) any later version.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
10
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
11 # This program is distributed in the hope that it will be useful,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
14 # GNU Affero General Public License for more details.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
15
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
16 # You should have received a copy of the GNU Affero General Public License
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
18
4398
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
19 from collections.abc import Awaitable
4303
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
20 from email.message import EmailMessage
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
21 from email.parser import BytesParser, Parser
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
22 from email import policy
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
23 from typing import Callable, cast
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
24 from twisted.internet import defer, protocol, reactor
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
25 from twisted.internet.base import DelayedCall
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
26 from twisted.mail import imap4
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
27 from twisted.python.failure import Failure
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
28
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
29 from libervia.backend.core import exceptions
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
30 from libervia.backend.core.i18n import _
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
31 from libervia.backend.core.log import getLogger
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
32 from .models import UserData
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
33
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
34 log = getLogger(__name__)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
35
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
36
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
37 class IMAPClient(imap4.IMAP4Client):
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
38 _idling = False
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
39 _idle_timer: DelayedCall | None = None
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
40
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
41 def __init__(self, connected: defer.Deferred, *args, **kwargs) -> None:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
42 super().__init__(*args, **kwargs)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
43 self._connected = connected
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
44
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
45 def serverGreeting(self, caps: dict) -> None:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
46 """Handle the server greeting and capabilities.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
47
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
48 @param caps: Server capabilities.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
49 """
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
50 defer.ensureDeferred(self.on_server_greeting(caps))
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
51
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
52 async def on_server_greeting(self, caps: dict) -> None:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
53 """Async method called when server greeting is received.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
54
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
55 @param caps: Server capabilities.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
56 """
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
57 self.server_capabilities = caps
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
58 try:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
59 await self.authenticate(self.factory.password.encode())
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
60 except Exception as e:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
61 log.warning(f"Can't authenticate: {e}")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
62 self._connected.errback(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
63 exceptions.PasswordError("Authentication error for IMAP server.")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
64 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
65 return
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
66 log.debug("Authenticated.")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
67 self._connected.callback(None)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
68 if b"IDLE" in caps:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
69 # We use "examine" for read-only access for now, will probably change in the
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
70 # future.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
71 await self.examine(b"INBOX")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
72 log.debug("Activating IDLE mode")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
73 await self.idle()
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
74 else:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
75 log.warning(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
76 f'"IDLE" mode is not supported by your server, this gateways needs a '
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
77 "server supporting this mode."
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
78 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
79 return
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
80
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
81 async def idle(self) -> None:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
82 """Enter the IDLE mode to receive real-time updates from the server."""
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
83 if self._idling:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
84 # We are already in idle state.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
85 return
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
86 self._idling = True
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
87 self._idle_timer = reactor.callLater(29 * 60, self.on_idle_timeout)
4398
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
88 log.debug("Starting IDLE mode.")
4303
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
89 await self.sendCommand(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
90 imap4.Command(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
91 b"IDLE",
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
92 continuation=lambda *a, **kw: log.debug(f"continuation: {a=} {kw=}"),
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
93 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
94 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
95
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
96 def idle_exit(self) -> None:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
97 """Exit the IDLE mode."""
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
98 assert self._idling
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
99 assert self._idle_timer is not None
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
100 if not self._idle_timer.called:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
101 self._idle_timer.cancel()
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
102 self._idle_timer = None
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
103 # Send DONE command to exit IDLE mode.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
104 self.sendLine(b"DONE")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
105 self._idling = False
4398
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
106 log.debug("IDLE mode terminated.")
4303
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
107
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
108 def on_idle_timeout(self):
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
109 """Called when IDLE mode timeout is reached."""
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
110 if self._idling:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
111 # We've reached 29 min of IDLE mode, we restart it as recommended in the
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
112 # specifications.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
113 self.idle_exit()
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
114 defer.ensureDeferred(self.idle())
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
115
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
116 def newMessages(self, exists: int | None, recent: int | None):
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
117 """Called when new messages are received.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
118
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
119 @param exists: Number of existing messages.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
120 @param recent: Number of recent messages.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
121 """
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
122 defer.ensureDeferred(self.on_new_emails(exists, recent))
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
123
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
124 async def on_new_emails(self, exists: int | None, recent: int | None) -> None:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
125 """Async method called when new messages are received.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
126
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
127 @param exists: Number of existing messages.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
128 @param recent: Number of recent messages.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
129 """
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
130 log.debug(f"New messages: {exists}, Recent messages: {recent}")
4398
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
131
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
132 if recent is None:
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
133 log.debug("No recent messages, skipping fetch.")
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
134 defer.ensureDeferred(self.idle())
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
135 return
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
136
4303
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
137 self.idle_exit()
4398
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
138
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
139 # We retrieve recent messages.
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
140 recent_uids = await self.search(imap4.Query(recent=True), uid=True)
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
141 message_set = imap4.MessageSet()
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
142 for recent_uid in recent_uids:
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
143 message_set.add(recent_uid)
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
144
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
145 try:
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
146 mess_data = await self.fetchMessage(message_set, uid=True)
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
147
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
148 # Process all fetched messages.
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
149 log.debug(f"Fetched {len(mess_data)} messages.")
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
150 for message in mess_data.values():
4303
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
151 try:
4398
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
152 content = message["RFC822"]
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
153 except KeyError:
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
154 log.warning(f"Can't find content for {message}.")
4303
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
155 continue
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
156 else:
4398
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
157 if isinstance(content, str):
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
158 parser = Parser(policy=policy.default)
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
159 parser_method = parser.parsestr
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
160 elif isinstance(content, bytes):
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
161 parser = BytesParser(policy=policy.default)
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
162 parser_method = parser.parsebytes
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
163 else:
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
164 log.error(f"Invalid content: {content}.")
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
165 continue
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
166 try:
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
167 parsed = parser_method(content)
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
168 except Exception as e:
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
169 log.warning(f"Can't parse content of email: {e}.")
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
170 continue
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
171 else:
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
172 assert self.factory is not None
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
173 factory = cast(IMAPClientFactory, self.factory)
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
174 await factory.on_new_email(parsed)
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
175
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
176 except Exception as e:
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
177 log.error(f"Error fetching recent messages: {e}")
4303
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
178
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
179 defer.ensureDeferred(self.idle())
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
180
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
181 def connectionLost(self, reason: Failure) -> None:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
182 """Called when the connection is lost.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
183
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
184 @param reason: The reason for the lost connection.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
185 """
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
186 log.debug(f"connectionLost {reason=}")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
187 if not self._connected.called:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
188 self._connected.errback(reason)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
189 super().connectionLost(reason)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
190
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
191 def lineReceived(self, line: bytes) -> None:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
192 """Called when a line is received from the server.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
193
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
194 @param line: The received line.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
195 """
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
196 if self._idling:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
197 if line == b"* OK Still here":
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
198 pass
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
199 elif line == b"+ idling":
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
200 pass
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
201 elif line.startswith(b"* "):
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
202 # Handle unsolicited responses during IDLE
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
203 self._extraInfo([imap4.parseNestedParens(line[2:])])
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
204 else:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
205 log.warning(f"Unexpected line received: {line!r}")
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
206
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
207 return
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
208
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
209 return super().lineReceived(line)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
210
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
211 def sendCommand(self, cmd: imap4.Command) -> defer.Deferred:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
212 """Send a command to the server.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
213
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
214 This method is overriden to stop and restart IDLE mode when a command is received.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
215
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
216 @param cmd: The command to send.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
217 @return: A deferred that fires when the command is sent.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
218 """
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
219 if self._idling and cmd.command != b"IDLE":
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
220 self.idle_exit()
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
221 d = super().sendCommand(cmd)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
222
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
223 def restart_idle_mode(ret):
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
224 defer.ensureDeferred(self.idle())
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
225 return ret
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
226
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
227 d.addCallback(restart_idle_mode)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
228 return d
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
229 else:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
230 return super().sendCommand(cmd)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
231
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
232
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
233 class IMAPClientFactory(protocol.ClientFactory):
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
234 protocol = IMAPClient
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
235
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
236 def __init__(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
237 self,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
238 user_data: UserData,
4398
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
239 on_new_email: Callable[[EmailMessage], Awaitable[None]],
4303
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
240 connected: defer.Deferred,
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
241 ) -> None:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
242 """Initialize the IMAP client factory.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
243
4398
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
244 @param user_data: User data containing credentials and other user-specific
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
245 information.
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
246 @param on_new_email: Called when a new email is received.
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
247 @param connected: Deferred that will be fired when the IMAP connection is
7ef21e3e5ac9 component email (imap): fetch all recent emails instead of only the last one.
Goffi <goffi@goffi.org>
parents: 4386
diff changeset
248 established.
4303
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
249 """
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
250 credentials = user_data.credentials
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
251 self.user_data = user_data
4386
c055042c01e3 component Email gateway: Convert mailing list to pubsub nodes:
Goffi <goffi@goffi.org>
parents: 4303
diff changeset
252 self.username = credentials.imap_username
c055042c01e3 component Email gateway: Convert mailing list to pubsub nodes:
Goffi <goffi@goffi.org>
parents: 4303
diff changeset
253 self.password = credentials.imap_password
4303
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
254 self.on_new_email = on_new_email
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
255 self._connected = connected
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
256
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
257 def buildProtocol(self, addr) -> IMAPClient:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
258 """Build the IMAP client protocol.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
259
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
260 @return: The IMAP client protocol.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
261 """
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
262 assert self.protocol is not None
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
263 assert isinstance(self.protocol, type(IMAPClient))
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
264 protocol_ = self.protocol(self._connected)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
265 protocol_.factory = self
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
266 self.user_data.imap_client = protocol_
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
267 assert isinstance(protocol_, IMAPClient)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
268 protocol_.factory = self
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
269 encoded_username = self.username.encode()
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
270
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
271 protocol_.registerAuthenticator(imap4.PLAINAuthenticator(encoded_username))
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
272 protocol_.registerAuthenticator(imap4.LOGINAuthenticator(encoded_username))
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
273 protocol_.registerAuthenticator(
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
274 imap4.CramMD5ClientAuthenticator(encoded_username)
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
275 )
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
276
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
277 return protocol_
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
278
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
279 def clientConnectionFailed(self, connector, reason: Failure) -> None:
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
280 """Called when the client connection fails.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
281
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
282 @param reason: The reason for the failure.
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
283 """
a7ec325246fb component email-gateway: first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
284 log.warning(f"Connection failed: {reason}")