目录

class 卑微码农:
def __init__(self):
self.技能 = ['能读懂十年前祖传代码', '擅长用Ctrl+C/V搭建世界', '信奉"能跑就别动"的玄学']
self.发量 = 100 # 初始发量
self.咖啡因耐受度 = '极限'
def 修Bug(self, bug):
try:
# 试图用玄学解决问题
if bug.严重程度 == '离谱':
print("这一定是环境问题!")
else:
print("让我看看是谁又没写注释...哦,是我自己。")
except Exception as e:
# 如果try块都救不了,那就...
print("重启一下试试?")
self.发量 -= 1 # 每解决一个bug,头发-1
# 实例化一个我
我 = 卑微码农()
引言
每天打开邮箱,几十上百封邮件扑面而来 —— 订阅通知、广告推广、群邮件占了大半,真正和自己相关的工作邮件反而被淹没。手动筛选不仅浪费时间,还可能错过紧急信息。
本文将聚焦海量邮件过滤和飞书实时提醒两大核心需求,用 Python 打造一个轻量化助手。它能 24 小时监控邮箱,自动过滤掉无关邮件,只将涉及你的重要邮件推送到飞书,让你不用频繁查邮箱也能及时掌握关键信息。全程代码可复现,新手也能跟着一步步搭建。
一、项目核心设计与技术选型
在动手前,先明确整个助手的工作流程和技术栈,确保每个模块都围绕 “过滤” 和 “提醒” 这两个核心目标展开。
1.1 核心工作流程
整个助手的逻辑很简单,可概括为 “三步骤循环”:
- 定时查收:按设定频率(如每 10 分钟)连接邮箱,获取所有未读邮件。
- 规则过滤:根据自定义规则(如发件人、关键词、是否抄送你)判断邮件是否和你相关。
- 飞书推送:命中规则的重要邮件,提取关键信息(发件人、标题、摘要)推送到飞书;无关邮件直接标记为已读,不做后续处理。
1.2 技术栈选型
优先选择轻量、成熟的库,避免复杂依赖,确保项目易部署、易维护。
| 功能模块 | 选用库 | 选择理由 |
|---|---|---|
| 邮件接收与解析 | poplib + email | Python 标准库,无需额外安装,支持 pop3 协议(主流邮箱均支持),能解析邮件结构 |
| 规则配置 | yaml | 用 yaml 文件存储过滤规则,用户无需改代码,直接编辑配置即可自定义规则 |
| 飞书消息推送 | requests | 轻量 HTTP 库,通过飞书机器人 Webhook 接口实现消息推送 |
| 定时任务 | schedule | 语法直观,适合设置循环任务(如每 1 分钟查一次邮件),学习成本低 |
环境准备:通过 pip 安装所需第三方库,命令如下:
bash
pip install schedule poplib requests
二、核心模块实现:从邮件解析到飞书推送
2.1 基础模块:邮件接收与解析
要过滤邮件,首先得能正确获取并解析邮件的关键信息(发件人、标题、内容、收件人 / 抄送等)。这一步是后续过滤的基础,必须处理好中文编码、邮件格式等细节问题。
import re
import poplib
import logging.config
from email.parser import BytesParser
from email.header import decode_header
from email.utils import parseaddr, parsedate_to_datetime
from html import unescape
def html_to_text(body):
text = re.sub('<head.*?>.*?</head>', '', body, flags=re.DOTALL)
text = re.sub('<.*?>', ' ', text)
return unescape(text.strip())
class OnlineManager:
def __init__(self, auth):
print("OnlineManager __init__")
self.SERVER = 'mail.XXX.com'
self.PORT = 995
self.USERNAME = auth['user']
self.PASSWORD = auth['pass']
def get_unread_emails(self):
status = check_db_directory()
# 初始化数据库(首次运行时创建)
init_uid_db(status['db_file'])
# 加载已处理UID集合
processed_uids = load_processed_uids(status['db_file']) # 替换原内存集合[5](@ref)
new_uids = set()
# 连接服务器
conn = poplib.POP3_SSL(self.SERVER, self.PORT)
conn.user(self.USERNAME)
conn.pass_(self.PASSWORD)
# 获取UIDL列表(entry_id集合)
response, uidl_list, _ = conn.uidl()
# 遍历邮件
messages = []
for msg_info in uidl_list:
entry_id = msg_info.decode().split()[1] # 提取唯一标识符[4](@ref)
if entry_id in processed_uids:
continue # 跳过已处理邮件
# 获取邮件原始数据
msg_num = msg_info.decode().split()[0]
raw_data = b'\n'.join(conn.retr(msg_num)[1])
# 解析邮件对象
msg = BytesParser().parsebytes(raw_data)
# 提取元数据
subject, encoding = decode_header(msg['Subject'])[0]
if isinstance(subject, bytes):
subject = subject.decode('utf-8', errors='ignore')
received_time = parsedate_to_datetime(msg['Date']) # 转换为datetime对象[2](@ref)
# 解析发件人信息
sender_name, sender_email = parseaddr(msg['From'])
if isinstance(sender_name, bytes):
sender_name = decode_header(sender_name)[0][0].decode()
# 提取正文
body = ""
for part in msg.walk():
content_type = part.get_content_type()
if content_type == 'text/plain':
body = part.get_payload(decode=True).decode('utf-8', errors='ignore')
break
elif content_type == 'text/html':
body = part.get_payload(decode=True).decode('utf-8', errors='ignore')
body = html_to_text(body)
break
# 记录新UID
new_uids.add(entry_id)
# 构建数据结构
messages.append({
"entry_id": entry_id,
"subject": subject,
"body": body,
"received_time": received_time.strftime('%Y-%m-%d %H:%M:%S'),
"sender": sender_email,
"SenderName": sender_name,
"folder": "INBOX" # POP3仅支持收件箱[4,6](@ref)
})
# 批量保存新UID到数据库
save_new_uids(new_uids, status['db_file']) # 使用事务批量插入[6,8](@ref)
conn.quit()
return {
'messages': messages,
'status': status,
}
关键注意点:
-
核心功能:通过 POP3 协议连接邮件服务器,获取并解析未读邮件,实现邮件去重与结构化提取。
-
核心组件
html_to_text函数:用正则移除 HTML 标签(含<head>部分),转换 HTML 转义字符(如&→&),将 HTML 正文转为纯文本。OnlineManager类:封装邮件获取与解析逻辑,通过 POP3_SSL(端口 995)加密连接服务器。
-
关键流程
- 去重机制:基于邮件唯一标识(UIDL,即
entry_id),通过数据库存储已处理 UID,避免重复解析。 - 邮件解析:
- 连接服务器后,获取所有邮件的 UIDL 列表,过滤已处理邮件;
- 解析未处理邮件的主题(解码中文)、接收时间(转为
datetime格式化)、发件人(拆分姓名与邮箱); - 优先提取纯文本正文,若为 HTML 则调用
html_to_text转换。
- 结果存储:解析后的数据结构化(含主题、正文、发件人等),新邮件 UID 批量存入数据库。
- 去重机制:基于邮件唯一标识(UIDL,即
-
技术细节
- 编码处理:用
decode_header解码邮件头(主题、发件人姓名)的中文,避免乱码; - 协议限制:依赖 POP3 协议,仅支持访问收件箱(INBOX);
- 安全性:使用
POP3_SSL加密连接,保护账号密码传输。
- 编码处理:用
2.2 核心模块:智能邮件过滤系统
过滤是本次项目的核心,需要设计一套灵活的规则,让用户可以自定义 “哪些邮件需要推送到飞书”。这里采用配置文件驱动的方式,用户只需修改 YAML 文件,无需修改代码。
2.2.1 过滤规则设计
我们定义 3 类常用过滤规则(可根据需求扩展),满足大部分场景:
- 发件人规则:指定需要关注的发件人(如领导、客户的邮箱)。
- 关键词规则:邮件标题或内容包含指定关键词(如你的名字、负责的项目名称)。
- 提及人规则:你是邮件中被提及的人。
将这些规则写入config.yaml配置文件,格式如下:
# config.yaml 示例
General:
interval: 30 # 轮询间隔(秒)
webhook_url: "https://open.feishu.cn/open-apis/bot/v2/hook/XXXXXX" # 飞书机器人Webhook地址
mailbox: "online" # 邮箱类型,主流的邮箱:online、outlook、foxmail
Proxy:
addr: "proxy.xxx.com:8080"
user: "xxxxxx"
pass: "xxxxxx"
Classification:
senders: # 发件人规则(支持部分匹配)
客户邮件: ["sales@company.com", "someone@"]
内部通知: ["hr@company.com", "anyone@"]
keywords: # 标题关键词规则(不区分大小写)
紧急事项: ["紧急", "重要"]
项目更新: ["项目进度", "deadline"]
报告: ["报告", "周报"]
报销: ["发票", "报销"]
mentions: # 正文中提及关键词规则(不区分大小写)
新任务来啦: ["assigned to you", "已分配给您"]
集成成功: ["build SUCCESS"]
集成失败: ["build FAILURE"]
提到了您: ["@猿大叔~", "@大叔~", "@猿哥", "猿大叔"]
# others: # 其他规则(不区分大小写), 兜底
# 剩余其他: ["", ""]
代码示例:
from collections import defaultdict
import logging.config
def classify_emails(emails, rules, classification_order):
"""执行双重分类[6](@ref)"""
setup_logging()
logger = logging.getLogger(__name__)
categories = defaultdict(list)
list_ordered = []
for msg in emails:
# 发件人匹配
matched = False
for cat, patterns in rules['senders'].items():
if any(p.lower() in msg['sender'].lower() for p in patterns):
categories[cat].append(msg)
matched = True
break
# 标题关键词匹配
if not matched:
subject = msg['subject'].lower()
for cat, keywords in rules['keywords'].items():
if any(kw.lower() in subject for kw in keywords):
categories[cat].append(msg)
matched = True
break
# 正文中提及关键词匹配
if not matched:
body = msg['body'].lower()
for cat, keywords in rules['mentions'].items():
if any(kw.lower() in body for kw in keywords):
categories[cat].append(msg)
matched = True
break
# 其他匹配
if not matched:
for cat, keywords in rules['others'].items():
categories[cat].append(msg)
break
logger.info("classification_order start")
# 按配置文件顺序排序
for cat in classification_order:
logger.info(f"cat === {cat}")
if categories.get(cat):
logger.info(f"categories[{cat}] === {categories[cat]}")
list_ordered.append((cat, categories[cat]))
# 追加未在配置中定义的分类(如有)
extra_cats = [cat for cat in categories if cat not in classification_order]
list_ordered += [(cat, categories[cat]) for cat in extra_cats]
return dict(list_ordered)
关键实现说明
-
核心功能:根据规则对邮件进行多维度分类,并按指定顺序返回分类结果。
-
分类逻辑(优先级匹配)对每封邮件按以下顺序匹配规则,命中即终止并归类:
- 发件人匹配:检查邮件发件人是否包含
rules['senders']中某类别的模式; - 标题关键词匹配:未命中发件人时,检查邮件标题是否含
rules['keywords']中某类别的关键词; - 正文关键词匹配:前两步未命中时,检查邮件正文是否含
rules['mentions']中某类别的关键词; - 其他归类:以上均未命中时,归入
rules['others']中的类别。
- 发件人匹配:检查邮件发件人是否包含
-
结果排序与整合
- 按
classification_order指定的顺序整理分类结果; - 未在
classification_order中定义的额外类别,追加到结果末尾; - 最终返回字典形式的分类结果(键为类别,值为该类别的邮件列表)。
- 按
-
辅助逻辑
- 引入日志记录分类过程;
- 用
defaultdict简化分类结果的初始化与存储。
灵活扩展建议:
- 可添加 “排除规则”(如过滤掉 “广告”“订阅” 等关键词的邮件)。
- 支持正则表达式匹配(如匹配特定格式的订单号、工号)。
- 按邮件优先级(Priority 字段)过滤,只推送高优先级邮件。
2.3 通知模块:飞书消息推送
将重要邮件推送到飞书,需要借助飞书开放平台的 “自定义机器人” 功能。步骤很简单:创建机器人 → 获取 Webhook 地址 → 用 Python 发送 POST 请求。
2.3.1 飞书机器人准备
- 打开飞书,进入需要接收提醒的群聊 → 点击右上角 “设置” → “智能群助手” → “添加机器人” → 选择 “自定义机器人”。
- 给机器人命名(如 “邮件助手”),上传头像(可选),点击 “添加”。
- 复制生成的 “Webhook 地址”(类似
https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxx),保存到yaml文件中的webhook_url。
2.3.2 飞书推送代码实现
代码示例:
def send_feishu_notification(webhook_url, categorized_emails, proxy):
setup_logging()
logger = logging.getLogger(__name__)
# 公共请求配置
addr = proxy['addr']
user = proxy['user']
passwd = proxy['pass']
proxies = {
'https': f'http://{user}:{passwd}@{addr}/'
}
headers = {"Content-Type": "application/json"}
# 构建基础消息模板
base_message = {
"msg_type": "interactive",
"card": {
"elements": [],
"header": {
"title": {
"tag": "plain_text",
"content": "📬 未读邮件分类汇总"
}
}
}
}
# 构建消息内容
for category, emails in categorized_emails.items():
if not emails:
continue
# 分类标题块
category_block = {
"tag": "div",
"text": {
"tag": "lark_md",
"content": f"**📌 {category}**({len(emails)}封)"
}
}
base_message['card']['elements'].append(category_block)
# 邮件条目块
for email in emails:
outlook_url = _generate_outlook_url(email['entry_id'])
email_info = (
f"📩 [[{email['subject']}]({outlook_url})]\n"
f"⏰ 时间:{email['received_time']} | "
f"👤 发件人:{email['sender']}"
)
base_message['card']['elements'].append({
"tag": "div",
"text": {
"tag": "lark_md",
"content": email_info
}
})
# 消息拆分与发送
try:
message_batches = _split_messages(base_message)
success_count = 0
for batch in message_batches:
try:
response = requests.post(
webhook_url,
json=batch,
headers=headers,
proxies=proxies,
timeout=10
)
if response.status_code == 200:
print(f"success_count = {success_count}")
success_count += 1
else:
logger.error(f"发送失败:{response.json()}")
except requests.exceptions.RequestException as e:
logger.error(f"请求异常:{str(e)}")
return success_count == len(message_batches)
except ValueError as e:
logger.error(f"消息拆分错误:{str(e)}")
return False
关键实现说明
-
核心功能:将分类后的未读邮件,通过飞书机器人 Webhook 发送交互式卡片通知,支持代理访问与消息拆分。
-
请求配置
- 代理设置:通过
proxy参数配置 HTTP 代理(含地址、用户名、密码),用于请求转发; - 头部信息:固定
Content-Type: application/json,符合飞书 Webhook 接口要求。
- 代理设置:通过
-
飞书消息构建
- 模板类型:采用飞书
interactive(交互式)卡片,头部标题固定为 “📬 未读邮件分类汇总”; - 内容填充:
- 遍历
categorized_emails,为每个分类添加标题块(含类别名与邮件数量,用 Markdown 加粗); - 为每封邮件生成条目块,包含主题(链接至 Outlook 邮件,调用
_generate_outlook_url生成链接)、接收时间、发件人,使用飞书lark_md格式支持 Markdown。
- 遍历
- 模板类型:采用飞书
-
消息拆分与发送
- 拆分逻辑:调用
_split_messages(未展示实现),推测因飞书消息长度限制,将超长卡片拆分为多个批次; - 批量发送:逐批次通过
requests.post发送,超时 10 秒,统计成功次数,返回 “是否所有批次均发送成功” 的布尔值。
- 拆分逻辑:调用
-
异常与日志
- 捕获请求异常(如网络错误)、消息拆分错误,通过日志记录详情;
- 打印发送成功计数,错误信息输出至日志。
飞书消息优化建议:
- 可添加 “@指定人” 功能,紧急邮件直接 @你(需在飞书机器人设置中开启 “@所有人” 权限)。
- 支持发送邮件附件预览(飞书机器人支持上传文件,可将邮件附件下载后再上传到飞书)。
- 按规则设置不同的消息颜色(如 “紧急” 关键词用红色,普通重要邮件用蓝色)。
2.4 总控模块:任务调度与流程串联
将前面的模块串联起来,实现 “定时查收→过滤→推送→标记已读” 的完整流程,并通过 schedule 库实现 24 小时循环运行。
代码示例:
import schedule
import time
import logging.config
def main_loop():
setup_logging()
logger = logging.getLogger(__name__)
logger.info("__main__ start")
current_work_dir = Path(os.getcwd())
config_dir = current_work_dir.parent / 'configs'
logger.info(f"config_dir = {config_dir}")
if check_ini_files(config_dir):
logger.info("check_ini_files ok")
config = load_config_yaml()
logger.info("load_config ok")
schedule.every(config['general']['interval']).seconds.do(job)
while True:
try:
schedule.run_pending()
time.sleep(5)
except Exception as e:
logger.error(f"Exception e: {e}")
time.sleep(60)
def job():
setup_logging()
logger = logging.getLogger(__name__)
logger.info(f"job start")
cfg_file = load_config_yaml()
unread_emails = []
send_ok = True
if cfg_file['general']['mailbox'] == "online":
online = OnlineManager(cfg_file['proxy'])
status = online.get_unread_emails()
send_ok = status['status']
unread_emails = status['messages']
logger.info(f"unread_emails = {unread_emails}")
logger.info(f"classification_order = {cfg_file['classification_order']}")
categorized = classify_emails(unread_emails, cfg_file['rules'], cfg_file['classification_order'])
logger.info(f"categorized = {categorized}")
if send_ok and any(categorized.values()):
send_feishu_notification(cfg_file['general']['webhook'], categorized, cfg_file['proxy'])
logger.info(f"send_feishu_notification ok")
if __name__ == "__main__":
main_loop()
关键实现说明
-
核心功能:通过定时任务循环执行邮件处理流程,包括获取未读邮件、分类整理并推送飞书通知。
-
主循环(
main_loop)- 初始化日志,检查配置文件(
configs目录下的 ini 文件)是否存在; - 加载 YAML 配置,基于配置中的
interval(间隔秒数),通过schedule库设置定时任务(执行job函数); - 无限循环运行定时任务,每 5 秒检查一次待执行任务;遇异常时记录日志并延迟 60 秒重试。
- 初始化日志,检查配置文件(
-
定时任务(
job)- 加载配置,若配置指定邮箱类型为
online,通过OnlineManager获取未读邮件; - 调用
classify_emails对未读邮件按规则分类(依赖配置中的rules和classification_order); - 若分类结果非空且发送状态正常,调用
send_feishu_notification推送飞书通知,完成后记录日志。
- 加载配置,若配置指定邮箱类型为
-
配置与日志
- 依赖外部配置文件(YAML 格式),包含邮件间隔、飞书 Webhook、分类规则等参数;
- 全程日志记录,追踪程序启动、任务执行、异常等状态。
三、项目其他方法说明
3.1 检查配置文件:
import tkinter as tk
from tkinter import messagebox
import logging.config
def check_ini_files(config_dir="configs"):
setup_logging()
logger = logging.getLogger(__name__)
"""
Verify INI file existence in config directory
Returns: bool - True if INI files exist, False otherwise
"""
config_path = Path(config_dir)
if not config_path.exists():
show_alert(f"Config directory '{config_dir}' not found")
return False
ini_files = list(config_path.glob("*.yaml"))
if not ini_files:
show_alert(f"No YAML files found in {config_path.resolve()}")
return False
logger.info(f"Found {len(ini_files)} YAML configuration files")
return True
def show_alert(message):
"""Display GUI alert popup"""
root = tk.Tk()
root.withdraw() # Hide main window
messagebox.showerror(
"Configuration Error",
f"{message}\n\nPlease create YAML files in configs directory.",
parent=root
)
root.destroy()
关键实现说明
-
核心功能:检查指定配置目录(默认
configs)下是否存在 YAML 配置文件,不存在时通过 GUI 弹窗提示错误,确保程序运行所需配置文件完备。 -
主要函数
check_ini_files:- 验证配置目录(
config_dir)是否存在,不存在则调用show_alert提示; - 查找目录下所有
.yaml文件(注释提及 “INI 文件”,实际检查 YAML,可能为注释误差),无文件时弹窗提示; - 找到文件则记录日志(数量)并返回
True,否则返回False。
- 验证配置目录(
show_alert:用tkinter创建隐藏主窗口,通过messagebox.showerror显示 “配置错误” 弹窗,提示目录 / 文件缺失及解决方案,弹窗关闭后销毁窗口。
-
关键细节
- 依赖
tkinter实现跨平台 GUI 弹窗,无需额外 GUI 库; - 目录路径默认相对当前目录,支持自定义配置目录路径;
- 日志记录配置文件检查结果,便于问题排查。
- 依赖
3.2 检查DB路径:
def check_db_directory():
setup_logging()
logger = logging.getLogger(__name__)
"""检查并创建数据库目录"""
db_dir = Path(".db")
have_db = True
# 检查目录是否存在
if not db_dir.exists():
have_db = False
os.makedirs(db_dir, exist_ok=True) # 自动创建多级目录[3](@ref)
logger.info(f"创建数据库目录: {db_dir.absolute()}")
# 检查数据库文件是否存在
db_file = db_dir / "processed.db"
if not db_file.exists():
have_db = False
return {
'have_db': have_db,
'db_file': db_file,
}
关键实现说明
-
核心功能:检查数据库目录(
.db)及数据库文件(processed.db)是否存在,目录不存在时自动创建,返回目录和文件的状态信息。 -
关键逻辑
- 初始化数据库目录路径为当前工作目录下的
.db文件夹; - 若目录不存在,创建该目录(
os.makedirs(exist_ok=True)确保创建时不报错),并记录日志,标记have_db为False; - 检查目录下是否存在
processed.db文件,不存在则同样标记have_db为False; - 返回字典,包含
have_db(目录和文件是否均存在,均存在为True)和db_file(processed.db的完整路径)。
- 初始化数据库目录路径为当前工作目录下的
-
关键细节
- 用
Path类处理文件路径,兼容性更强; - 仅创建目录,不自动创建数据库文件(
processed.db需由其他逻辑生成); - 日志记录目录创建操作,便于追踪初始化状态。
- 用
3.3 生成对应平台兼容的 Outlook 邮件访问链接
import json
import requests
import logging.config
def _generate_outlook_url(entry_id):
"""全平台兼容的链接生成逻辑"""
import platform
system = platform.system()
_url = f"outlook:///view/{entry_id}"
if system == "Windows":
_url = f"outlook:///view/{entry_id}"
elif system == "Darwin": # macOS
_url = f"ms-outlook://emails/{entry_id}" # macOS专用协议
else: # 移动端/Linux
_url = f"https://outlook.office.com/mail/item/{entry_id}"
# encoded = base64.b64encode(_url.encode()).decode()
# return f"https://applink.feishu.cn/client/redirect?data={encoded}"
return f"{_url}"
关键实现说明
-
核心功能:根据当前操作系统,生成对应平台兼容的 Outlook 邮件访问链接,参数
entry_id为邮件唯一标识。 -
平台适配逻辑
- 调用
platform.system()获取系统类型,针对性生成链接:- Windows:使用本地协议
outlook:///view/{entry_id},直接唤起本地 Outlook 客户端; - macOS(Darwin):使用专用协议
ms-outlook://emails/{entry_id},唤起本地 Outlook; - 其他系统(Linux / 移动端):生成 Outlook 网页版链接
https://outlook.office.com/mail/item/{entry_id}。
- Windows:使用本地协议
- 调用
-
关键细节
- 依赖
platform模块实现跨平台判断; - 注释中保留了飞书链接跳转逻辑(Base64 编码 URL),当前未启用;
- 链接生成直接依赖
entry_id,确保能定位到具体邮件。
- 依赖
3.4 将超过大小限制(默认 20KB)的飞书交互式卡片消息,拆分为多个符合大小要求的消息批次。
from typing import Dict, List
def _split_messages(original_msg: Dict, max_size: int = 20 * 1024) -> List[Dict]:
setup_logging()
logger = logging.getLogger(__name__)
"""拆分超过大小限制的消息为多个批次"""
batches = []
current_batch = {
"msg_type": "interactive",
"card": {
"elements": [],
"header": original_msg["card"]["header"]
}
}
def get_batch_size(batch):
return len(json.dumps(batch, separators=(",", ":")).encode("utf-8"))
for element in original_msg["card"]["elements"]:
# 预计算添加后的消息大小
temp_batch = current_batch.copy()
temp_batch["card"]["elements"].append(element)
if get_batch_size(temp_batch) > max_size:
if not current_batch["card"]["elements"]:
logger.warning("单个消息元素超过20KB限制")
batches.append(current_batch)
current_batch = {
"msg_type": "interactive",
"card": {
"elements": [element],
"header": original_msg["card"]["header"]
}
}
else:
current_batch = temp_batch
if current_batch["card"]["elements"]:
batches.append(current_batch)
return batches
关键实现说明
-
核心功能:将超过大小限制(默认 20KB)的飞书交互式卡片消息,拆分为多个符合大小要求的消息批次,确保消息能正常发送(规避平台消息大小限制)。
-
关键逻辑
- 初始化当前批次:复制原消息的
header(保持卡片标题一致性),初始化空elements(内容元素); - 遍历原消息的
elements(卡片内容块),对每个元素:- 尝试添加到 “临时批次”,计算添加后的大小;
- 若超限制,将 “当前批次” 加入结果列表,重新初始化 “当前批次” 并放入该元素;
- 未超限制则更新 “当前批次” 为 “临时批次”;
- 处理剩余元素:遍历结束后,将未完成的 “当前批次” 加入结果列表。
- 初始化当前批次:复制原消息的
-
大小计算方式通过
json.dumps用紧凑格式(separators=(",", ":")去除空格)序列化消息,转utf-8字节后计算长度,模拟实际传输时的消息大小。 -
边界处理若单个元素超过 20KB,无法拆分,会记录警告日志,仍将其作为单独批次(避免消息丢失);所有批次均保留原消息
header,确保拆分后卡片风格统一。
3.5 提取通用配置、代理配置、邮件分类规则及分类顺序,返回结构化配置字典供其他模块使用。
import logging.config
def load_config_yaml():
setup_logging()
logger = logging.getLogger(__name__)
current_work_dir = Path(os.getcwd())
config_file = current_work_dir.parent / 'configs' / 'config.yaml'
with open(config_file, 'r', encoding='utf-8') as stream:
try:
config_data = yaml.safe_load(stream)
except yaml.YAMLError as exc:
logger.error(f"YAML解析错误: {exc}")
raise ValueError(f"YAML解析错误: {exc}")
# 提取分类规则
classification = config_data.get('Classification', {})
rules = {
'senders': classification.get('senders', {}),
'keywords': classification.get('keywords', {}),
'mentions': classification.get('mentions', {}),
'others': classification.get('others', {})
}
# 保留原始顺序(需注意YAML默认不保留顺序)
classification_order = []
for category in ['senders', 'keywords', 'mentions', 'others']:
if category in classification:
classification_order.extend(classification[category].keys())
return {
'general': {
'interval': config_data['General'].get('interval', 60),
'webhook': config_data['General'].get('webhook_url'),
'mailbox': config_data['General'].get('mailbox'),
'maildir': config_data['General'].get('maildir')
},
'proxy': {
'addr': config_data['Proxy'].get('addr'),
'user': config_data['Proxy'].get('user'),
'pass': config_data['Proxy'].get('pass')
},
'rules': rules,
'classification_order': classification_order
}
关键实现说明
-
核心功能:加载并解析项目配置目录下的
config.yaml文件,提取通用配置、代理配置、邮件分类规则及分类顺序,返回结构化配置字典供其他模块使用。 -
主要逻辑
- 配置路径:固定从当前工作目录的父目录(
../configs/)读取config.yaml文件; - 安全解析:用
yaml.safe_load加载 YAML 内容(避免恶意代码执行),解析失败时记录错误日志并抛出ValueError; - 规则提取:从
Classification字段中提取senders(发件人规则)、keywords(标题关键词规则)、mentions(正文关键词规则)、others(其他规则),组成rules字典; - 顺序整理:按
senders→keywords→mentions→others的固定顺序,提取各规则下的类别名,生成classification_order(分类结果排序依据)。
- 配置路径:固定从当前工作目录的父目录(
-
返回结构输出字典包含 4 个核心部分:
general:通用配置(邮件检查间隔、飞书 Webhook、邮箱类型等);proxy:代理配置(地址、用户名、密码);rules:邮件分类规则;classification_order:分类结果的展示顺序。
-
关键细节
- 依赖项目目录结构(配置文件需放在上级
configs目录); - 解析错误会主动抛异常,强制上层处理配置问题;
- 注释提示 YAML 默认不保留字段顺序,需注意配置文件中类别顺序可能影响最终分类顺序。
- 依赖项目目录结构(配置文件需放在上级
3.6 通过 SQLite 数据库管理已处理邮件的唯一标识(UID/entry_id),实现邮件去重(避免重复处理)。
import sqlite3
from contextlib import closing
import logging.config
def init_uid_db(db_path='.db/processed.db'):
"""初始化数据库表结构"""
with closing(sqlite3.connect(db_path)) as conn:
conn.execute('''CREATE TABLE IF NOT EXISTS processed_uids (
entry_id TEXT PRIMARY KEY
) WITHOUT ROWID''')
conn.commit()
def load_processed_uids(db_path='.db/processed.db'):
"""加载已处理UID集合"""
try:
with closing(sqlite3.connect(db_path)) as conn:
cur = conn.execute('SELECT entry_id FROM processed_uids')
return {row[0] for row in cur.fetchall()}
except sqlite3.Error:
return set()
def save_new_uids(new_uids, db_path='.db/processed.db'):
setup_logging()
logger = logging.getLogger(__name__)
"""批量保存新UID"""
if not new_uids:
return
try:
with closing(sqlite3.connect(db_path)) as conn:
conn.execute("BEGIN TRANSACTION") # 开启事务提升性能[7](@ref)
conn.executemany('''INSERT OR IGNORE INTO processed_uids VALUES (?)''',
[(uid,) for uid in new_uids])
conn.commit()
except sqlite3.Error as e:
conn.rollback()
logger.error(f"UID保存失败: {e}")
关键实现说明
-
核心功能:通过 SQLite 数据库管理已处理邮件的唯一标识(UID/entry_id),实现邮件去重(避免重复处理),包含数据库初始化、已处理 UID 加载、新 UID 保存三个核心操作。
-
主要函数
init_uid_db:初始化数据库表结构,创建processed_uids表(仅含entry_id字段,设为主键),并通过WITHOUT ROWID优化(因主键已唯一,无需额外行 ID),确保表结构存在(CREATE TABLE IF NOT EXISTS)。load_processed_uids:查询表中所有entry_id,返回包含已处理 UID 的集合;遇数据库错误时返回空集合,避免程序中断。save_new_uids:批量保存新 UID,通过事务(BEGIN TRANSACTION)提升批量插入性能,用INSERT OR IGNORE避免重复插入(利用主键唯一性约束);出错时回滚事务并记录日志。
-
关键细节
- 用
contextlib.closing自动管理数据库连接关闭,避免资源泄露; - 依赖 SQLite 轻量特性,无需独立服务,适合本地存储场景;
- 通过主键约束和插入策略确保
entry_id唯一性,核心是去重逻辑的持久化实现。
- 用
3.7 初始化Log模块:
import yaml
import os
import logging.config
from pathlib import Path
def setup_logging():
current_work_dir = Path(os.getcwd())
yaml_file = current_work_dir.parent / 'configs' / 'logging.yaml'
with open(yaml_file, 'r', encoding='utf-8') as f:
config = yaml.safe_load(f) # 安全加载YAML[5](@ref)
logging.config.dictConfig(config) # 应用配置字典[8](@ref)
关键实现说明
-
核心功能:加载项目配置目录下的
logging.yaml文件,初始化 Python 的logging模块,统一配置日志的输出格式、级别、存储路径等(如控制台输出、文件记录)。 -
关键逻辑
- 拼接日志配置文件路径:从当前工作目录的父目录(
../configs/)中读取logging.yaml; - 安全加载配置:用
yaml.safe_load读取 YAML 文件(避免恶意代码执行),获取日志配置字典; - 应用日志配置:通过
logging.config.dictConfig将配置字典应用到logging模块,覆盖默认日志设置。
- 拼接日志配置文件路径:从当前工作目录的父目录(
-
关键细节
- 依赖
Path类处理文件路径,跨平台兼容性更强; - 配置文件路径固定,需项目目录结构满足 “上级目录有 configs 文件夹及 logging.yaml”;
- 日志的具体行为(如日志级别、输出位置)由
logging.yaml的内容决定,本函数仅负责加载和应用配置。
- 依赖
四、项目优化与部署建议
4.1 功能优化
- 日志分级:将日志分为 INFO(普通信息)、WARNING(警告)、ERROR(错误)等级,方便快速定位问题。
- 异常重试:网络波动时,邮件接收或飞书推送可能失败,可添加重试机制(如使用
tenacity库)。 - 规则热更新:支持在不重启程序的情况下,修改
config.yaml后自动加载新规则。 - 多邮箱支持:扩展代码,同时监控多个邮箱(如工作邮箱、个人邮箱)。
4.2 部署方案
要实现真正的 “24 小时全天候运行”,需要将程序部署到服务器或本地后台进程:
方案 1:本地后台运行(Windows)
- 新建一个批处理文件
start_assistant.bat,内容如下:bat
@echo off python "C:\path\to\your\project\main.py" pause - 右键点击批处理文件,选择 “创建快捷方式”。
- 右键点击快捷方式,选择 “属性”→“快捷方式”→“目标”,在末尾添加
> output.log 2>&1(将输出重定向到日志文件)。 - 将快捷方式拖到 “启动” 文件夹(Win+R 输入
shell:startup打开),实现开机自启。
方案 2:云服务器部署(Linux)
- 将项目文件上传到云服务器(如阿里云、腾讯云)。
- 使用
nohup命令让程序在后台运行:bash
nohup python3 main.py > assistant.log 2>&1 & - 查看运行状态:
bash
ps aux | grep main.py # 查看进程是否在运行 tail -f assistant.log # 实时查看日志 - 设置开机自启:通过
systemd创建服务,确保服务器重启后程序自动运行。
五、总结
本文聚焦 “海量邮件过滤” 和 “飞书实时提醒” 两大核心需求,用 Python 实现了一个轻量化的邮件助手。通过 POP 协议接收邮件,自定义 YAML 规则过滤无关信息,再借助飞书机器人推送重要内容,彻底解决了 “邮件太多找不到重点” 的问题。
整个项目的代码都围绕 “实用” 和 “易用” 展开,没有复杂的框架依赖,新手也能跟着配置和修改。你可以根据自己的工作场景,灵活调整过滤规则,甚至扩展更多功能(如微信推送、邮件自动回复)。
1387

被折叠的 条评论
为什么被折叠?



