component email (imap): fetch all recent emails instead of only the last one.

This commit is contained in:
Goffi 2025-09-11 21:10:35 +02:00
commit 412db6a216

View file

@ -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 @@ class IMAPClient(imap4.IMAP4Client):
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 @@ class IMAPClient(imap4.IMAP4Client):
# 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 @@ class IMAPClient(imap4.IMAP4Client):
@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 @@ class IMAPClientFactory(protocol.ClientFactory):
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