feat: scan all IMAP folders for unread emails, not just INBOX
This commit is contained in:
@@ -12,7 +12,8 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class Email:
|
||||
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.subject = subject
|
||||
self.sender = sender
|
||||
@@ -21,6 +22,7 @@ class Email:
|
||||
self.date = date
|
||||
self.reply_to = reply_to
|
||||
self.account_email = account_email
|
||||
self.folder = folder
|
||||
|
||||
|
||||
def _decode_str(s: str) -> str:
|
||||
@@ -108,36 +110,63 @@ def _login_and_prepare(account: EmailAccount, timeout: int = 30):
|
||||
def fetch_unseen_emails(account: EmailAccount) -> list[Email]:
|
||||
conn = _login_and_prepare(account)
|
||||
|
||||
_, data = conn.uid("SEARCH", None, "UNSEEN")
|
||||
uids = data[0].split() if data[0] else []
|
||||
logger.info("UNSEEN 数量: %d", len(uids))
|
||||
emails = []
|
||||
|
||||
for uid in uids:
|
||||
try:
|
||||
_, msg_data = conn.uid("FETCH", uid, "RFC822")
|
||||
if msg_data[0] is None:
|
||||
logger.warning("UID %s FETCH 返回空,跳过", uid)
|
||||
# 列出所有文件夹
|
||||
_, folder_data = conn.list()
|
||||
folders = []
|
||||
if folder_data:
|
||||
for item in folder_data:
|
||||
if item is None:
|
||||
continue
|
||||
raw_email = msg_data[0][1]
|
||||
msg = email.message_from_bytes(raw_email)
|
||||
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))
|
||||
|
||||
subject = _decode_str(msg.get("Subject", ""))
|
||||
sender = _decode_address_header(msg.get("From", ""))
|
||||
recipient = _decode_address_header(msg.get("To", ""))
|
||||
reply_to = _decode_address_header(msg.get("Reply-To", ""))
|
||||
date_str = msg.get("Date", "")
|
||||
body = _get_text_from_msg(msg)
|
||||
body_len = len(body)
|
||||
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")
|
||||
uids = data[0].split() if data[0] else []
|
||||
if not uids:
|
||||
continue
|
||||
logger.info(" %s: %d 封未读", folder, len(uids))
|
||||
|
||||
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,
|
||||
body=body, date=date_str, reply_to=reply_to, account_email=account.username))
|
||||
for uid in uids:
|
||||
try:
|
||||
_, msg_data = conn.uid("FETCH", uid, "RFC822")
|
||||
if msg_data[0] is None:
|
||||
logger.warning("UID %s FETCH 返回空,跳过", uid)
|
||||
continue
|
||||
raw_email = msg_data[0][1]
|
||||
msg = email.message_from_bytes(raw_email)
|
||||
|
||||
# FETCH 可能被服务器自动标已读,立即标回未读
|
||||
conn.uid("STORE", uid, "-FLAGS", "\\Seen")
|
||||
subject = _decode_str(msg.get("Subject", ""))
|
||||
sender = _decode_address_header(msg.get("From", ""))
|
||||
recipient = _decode_address_header(msg.get("To", ""))
|
||||
reply_to = _decode_address_header(msg.get("Reply-To", ""))
|
||||
date_str = msg.get("Date", "")
|
||||
body = _get_text_from_msg(msg)
|
||||
body_len = len(body)
|
||||
|
||||
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,
|
||||
body=body, date=date_str, reply_to=reply_to, account_email=account.username,
|
||||
folder=folder))
|
||||
|
||||
# FETCH 可能被服务器自动标已读,立即标回未读
|
||||
conn.uid("STORE", uid, "-FLAGS", "\\Seen")
|
||||
except Exception as e:
|
||||
logger.error(" UID %s FETCH 失败,跳过: %s", uid, e)
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.error("UID %s FETCH 失败,跳过: %s", uid, e)
|
||||
logger.warning("扫描文件夹 %s 失败: %s", folder, e)
|
||||
continue
|
||||
|
||||
try:
|
||||
@@ -149,12 +178,23 @@ def fetch_unseen_emails(account: EmailAccount) -> list[Email]:
|
||||
|
||||
|
||||
@retry()
|
||||
def mark_as_seen(account: EmailAccount, uids: list[bytes]):
|
||||
if not uids:
|
||||
def mark_as_seen(account: EmailAccount, uids_with_folder: list[tuple[bytes, str]]):
|
||||
if not uids_with_folder:
|
||||
return
|
||||
logger.info("标记 %d 封邮件为已读", len(uids))
|
||||
logger.info("标记 %d 封邮件为已读", len(uids_with_folder))
|
||||
conn = _login_and_prepare(account)
|
||||
for uid in uids:
|
||||
conn.uid("STORE", uid, "+FLAGS", "\\Seen")
|
||||
# 按文件夹分组
|
||||
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:
|
||||
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()
|
||||
logger.info("标记完成")
|
||||
|
||||
Reference in New Issue
Block a user