feat: scan all IMAP folders for unread emails, not just INBOX

This commit is contained in:
2026-07-13 00:16:28 +08:00
parent 46bc7c7057
commit 479b855390
2 changed files with 73 additions and 32 deletions

View File

@@ -12,7 +12,8 @@ logger = logging.getLogger(__name__)
class Email: class Email:
def __init__(self, uid: bytes, subject: str, sender: str, recipient: str, def __init__(self, uid: bytes, subject: str, sender: str, recipient: str,
body: str, date: str, reply_to: str = "", account_email: str = ""): body: str, date: str, reply_to: str = "", account_email: str = "",
folder: str = "INBOX"):
self.uid = uid self.uid = uid
self.subject = subject self.subject = subject
self.sender = sender self.sender = sender
@@ -21,6 +22,7 @@ class Email:
self.date = date self.date = date
self.reply_to = reply_to self.reply_to = reply_to
self.account_email = account_email self.account_email = account_email
self.folder = folder
def _decode_str(s: str) -> str: def _decode_str(s: str) -> str:
@@ -108,10 +110,33 @@ def _login_and_prepare(account: EmailAccount, timeout: int = 30):
def fetch_unseen_emails(account: EmailAccount) -> list[Email]: def fetch_unseen_emails(account: EmailAccount) -> list[Email]:
conn = _login_and_prepare(account) conn = _login_and_prepare(account)
# 列出所有文件夹
_, folder_data = conn.list()
folders = []
if folder_data:
for item in folder_data:
if item is None:
continue
parts = item.decode("utf-8", errors="replace").split(' " ')
if len(parts) >= 2:
folder_name = parts[1].strip('"')
folders.append(folder_name)
if not folders:
folders = ["INBOX"]
logger.info("扫描文件夹: %s", ", ".join(folders))
emails = []
for folder in folders:
try:
status, _ = conn.select(folder, readonly=True)
if status != "OK":
logger.warning("无法选择文件夹: %s", folder)
continue
_, data = conn.uid("SEARCH", None, "UNSEEN") _, data = conn.uid("SEARCH", None, "UNSEEN")
uids = data[0].split() if data[0] else [] uids = data[0].split() if data[0] else []
logger.info("UNSEEN 数量: %d", len(uids)) if not uids:
emails = [] continue
logger.info(" %s: %d 封未读", folder, len(uids))
for uid in uids: for uid in uids:
try: try:
@@ -132,13 +157,17 @@ def fetch_unseen_emails(account: EmailAccount) -> list[Email]:
logger.info(" 邮件: [%s] from=%s to=%s reply-to=%s (%d 字符)", subject, sender, recipient, reply_to, body_len) logger.info(" 邮件: [%s] from=%s to=%s reply-to=%s (%d 字符)", subject, sender, recipient, reply_to, body_len)
emails.append(Email(uid=uid, subject=subject, sender=sender, recipient=recipient, emails.append(Email(uid=uid, subject=subject, sender=sender, recipient=recipient,
body=body, date=date_str, reply_to=reply_to, account_email=account.username)) body=body, date=date_str, reply_to=reply_to, account_email=account.username,
folder=folder))
# FETCH 可能被服务器自动标已读,立即标回未读 # FETCH 可能被服务器自动标已读,立即标回未读
conn.uid("STORE", uid, "-FLAGS", "\\Seen") conn.uid("STORE", uid, "-FLAGS", "\\Seen")
except Exception as e: except Exception as e:
logger.error(" UID %s FETCH 失败,跳过: %s", uid, e) logger.error(" UID %s FETCH 失败,跳过: %s", uid, e)
continue continue
except Exception as e:
logger.warning("扫描文件夹 %s 失败: %s", folder, e)
continue
try: try:
conn.logout() conn.logout()
@@ -149,12 +178,23 @@ def fetch_unseen_emails(account: EmailAccount) -> list[Email]:
@retry() @retry()
def mark_as_seen(account: EmailAccount, uids: list[bytes]): def mark_as_seen(account: EmailAccount, uids_with_folder: list[tuple[bytes, str]]):
if not uids: if not uids_with_folder:
return return
logger.info("标记 %d 封邮件为已读", len(uids)) logger.info("标记 %d 封邮件为已读", len(uids_with_folder))
conn = _login_and_prepare(account) conn = _login_and_prepare(account)
# 按文件夹分组
from collections import defaultdict
by_folder: dict[str, list[bytes]] = defaultdict(list)
for uid, folder in uids_with_folder:
by_folder[folder].append(uid)
for folder, uids in by_folder.items():
try:
conn.select(folder, readonly=True)
for uid in uids: for uid in uids:
conn.uid("STORE", uid, "+FLAGS", "\\Seen") conn.uid("STORE", uid, "+FLAGS", "\\Seen")
logger.info(" %s: 标记 %d", folder, len(uids))
except Exception as e:
logger.error(" 标记 %s 失败: %s", folder, e)
conn.logout() conn.logout()
logger.info("标记完成") logger.info("标记完成")

View File

@@ -42,6 +42,7 @@ def ai_process(cfg: Config, acct_idx: int, mail: Email) -> Optional[dict]:
"acct_idx": acct_idx, "acct_idx": acct_idx,
"account_email": mail.account_email, "account_email": mail.account_email,
"uid": mail.uid, "uid": mail.uid,
"folder": mail.folder,
"thinking_msg_id": thinking_msg_id, "thinking_msg_id": thinking_msg_id,
} }
@@ -81,5 +82,5 @@ def tg_send_and_mark(cfg: Config, info: dict):
original_reply_to=info.get("original_reply_to", ""), original_reply_to=info.get("original_reply_to", ""),
account_email=info.get("account_email", "")) account_email=info.get("account_email", ""))
# 只有发送成功才标记已读 # 只有发送成功才标记已读
mark_as_seen(acct, [info["uid"]]) mark_as_seen(acct, [(info["uid"], info.get("folder", "INBOX"))])
logger.info(f" TG 发送成功,已标记已读") logger.info(f" TG 发送成功,已标记已读")