changeset 4398:7ef21e3e5ac9

component email (imap): fetch all recent emails instead of only the last one.
author Goffi <goffi@goffi.org>
date Thu, 11 Sep 2025 21:10:35 +0200
parents 6e3a0ef1c561
children fe09446a09ce
files libervia/backend/plugins/plugin_comp_email_gateway/imap.py
diffstat 1 files changed, 52 insertions(+), 28 deletions(-) [+]
line wrap: on
line diff
--- a/libervia/backend/plugins/plugin_comp_email_gateway/imap.py	Sat Sep 06 16:31:24 2025 +0200
+++ b/libervia/backend/plugins/plugin_comp_email_gateway/imap.py	Thu Sep 11 21:10:35 2025 +0200
@@ -16,6 +16,7 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+from collections.abc import Awaitable
 from email.message import EmailMessage
 from email.parser import BytesParser, Parser
 from email import policy
@@ -84,6 +85,7 @@
             return
         self._idling = True
         self._idle_timer = reactor.callLater(29 * 60, self.on_idle_timeout)
+        log.debug("Starting IDLE mode.")
         await self.sendCommand(
             imap4.Command(
                 b"IDLE",
@@ -101,7 +103,7 @@
         # Send DONE command to exit IDLE mode.
         self.sendLine(b"DONE")
         self._idling = False
-        log.debug("IDLE mode terminated")
+        log.debug("IDLE mode terminated.")
 
     def on_idle_timeout(self):
         """Called when IDLE mode timeout is reached."""
@@ -126,34 +128,53 @@
         @param recent: Number of recent messages.
         """
         log.debug(f"New messages: {exists}, Recent messages: {recent}")
-        log.debug("Retrieving last message.")
+
+        if recent is None:
+            log.debug("No recent messages, skipping fetch.")
+            defer.ensureDeferred(self.idle())
+            return
+
         self.idle_exit()
-        mess_data = await self.fetchMessage("*")
-        for message in mess_data.values():
-            try:
-                content = message["RFC822"]
-            except KeyError:
-                log.warning(f"Can't find content for {message}.")
-                continue
-            else:
-                if isinstance(content, str):
-                    parser = Parser(policy=policy.default)
-                    parser_method = parser.parsestr
-                elif isinstance(content, bytes):
-                    parser = BytesParser(policy=policy.default)
-                    parser_method = parser.parsebytes
-                else:
-                    log.error(f"Invalid content: {content}")
-                    continue
+
+        # We retrieve recent messages.
+        recent_uids = await self.search(imap4.Query(recent=True), uid=True)
+        message_set = imap4.MessageSet()
+        for recent_uid in recent_uids:
+            message_set.add(recent_uid)
+
+        try:
+            mess_data = await self.fetchMessage(message_set, uid=True)
+
+            # Process all fetched messages.
+            log.debug(f"Fetched {len(mess_data)} messages.")
+            for message in mess_data.values():
                 try:
-                    parsed = parser_method(content)
-                except Exception as e:
-                    log.warning(f"Can't parse content of email: {e}")
+                    content = message["RFC822"]
+                except KeyError:
+                    log.warning(f"Can't find content for {message}.")
                     continue
                 else:
-                    assert self.factory is not None
-                    factory = cast(IMAPClientFactory, self.factory)
-                    await factory.on_new_email(parsed)
+                    if isinstance(content, str):
+                        parser = Parser(policy=policy.default)
+                        parser_method = parser.parsestr
+                    elif isinstance(content, bytes):
+                        parser = BytesParser(policy=policy.default)
+                        parser_method = parser.parsebytes
+                    else:
+                        log.error(f"Invalid content: {content}.")
+                        continue
+                    try:
+                        parsed = parser_method(content)
+                    except Exception as e:
+                        log.warning(f"Can't parse content of email: {e}.")
+                        continue
+                    else:
+                        assert self.factory is not None
+                        factory = cast(IMAPClientFactory, self.factory)
+                        await factory.on_new_email(parsed)
+
+        except Exception as e:
+            log.error(f"Error fetching recent messages: {e}")
 
         defer.ensureDeferred(self.idle())
 
@@ -215,13 +236,16 @@
     def __init__(
         self,
         user_data: UserData,
-        on_new_email: Callable[[EmailMessage], None],
+        on_new_email: Callable[[EmailMessage], Awaitable[None]],
         connected: defer.Deferred,
     ) -> None:
         """Initialize the IMAP client factory.
 
-        @param username: The username to use for authentication.
-        @param password: The password to use for authentication.
+        @param user_data: User data containing credentials and other user-specific
+            information.
+        @param on_new_email: Called when a new email is received.
+        @param connected: Deferred that will be fired when the IMAP connection is
+            established.
         """
         credentials = user_data.credentials
         self.user_data = user_data