告别邮件焦虑!Python智能邮件助手,让你的效率翻倍

目录

一、项目核心设计与技术选型

1.1 核心工作流程

1.2 技术栈选型

二、核心模块实现:从邮件解析到飞书推送

2.1 基础模块:邮件接收与解析

2.2 核心模块:智能邮件过滤系统

2.2.1 过滤规则设计

关键实现说明

2.3 通知模块:飞书消息推送

2.3.1 飞书机器人准备

2.3.2 飞书推送代码实现

关键实现说明

2.4 总控模块:任务调度与流程串联

关键实现说明

三、项目其他方法说明

关键实现说明

关键实现说明

关键实现说明

关键实现说明

关键实现说明

关键实现说明

关键实现说明

四、项目优化与部署建议

4.1 功能优化

4.2 部署方案

方案 1:本地后台运行(Windows)

方案 2:云服务器部署(Linux)

五、总结


 

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 核心工作流程

整个助手的逻辑很简单,可概括为 “三步骤循环”:

  1. 定时查收:按设定频率(如每 10 分钟)连接邮箱,获取所有未读邮件。
  2. 规则过滤:根据自定义规则(如发件人、关键词、是否抄送你)判断邮件是否和你相关。
  3. 飞书推送:命中规则的重要邮件,提取关键信息(发件人、标题、摘要)推送到飞书;无关邮件直接标记为已读,不做后续处理。

1.2 技术栈选型

优先选择轻量、成熟的库,避免复杂依赖,确保项目易部署、易维护。

功能模块选用库选择理由
邮件接收与解析poplib + emailPython 标准库,无需额外安装,支持 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 转义字符(如&amp;&),将 HTML 正文转为纯文本。
    • OnlineManager类:封装邮件获取与解析逻辑,通过 POP3_SSL(端口 995)加密连接服务器。
  • 关键流程

    • 去重机制:基于邮件唯一标识(UIDL,即entry_id),通过数据库存储已处理 UID,避免重复解析。
    • 邮件解析
      • 连接服务器后,获取所有邮件的 UIDL 列表,过滤已处理邮件;
      • 解析未处理邮件的主题(解码中文)、接收时间(转为datetime格式化)、发件人(拆分姓名与邮箱);
      • 优先提取纯文本正文,若为 HTML 则调用html_to_text转换。
    • 结果存储:解析后的数据结构化(含主题、正文、发件人等),新邮件 UID 批量存入数据库。
  • 技术细节

    • 编码处理:用decode_header解码邮件头(主题、发件人姓名)的中文,避免乱码;
    • 协议限制:依赖 POP3 协议,仅支持访问收件箱(INBOX);
    • 安全性:使用POP3_SSL加密连接,保护账号密码传输。

2.2 核心模块:智能邮件过滤系统

过滤是本次项目的核心,需要设计一套灵活的规则,让用户可以自定义 “哪些邮件需要推送到飞书”。这里采用配置文件驱动的方式,用户只需修改 YAML 文件,无需修改代码。

2.2.1 过滤规则设计

我们定义 3 类常用过滤规则(可根据需求扩展),满足大部分场景:

  1. 发件人规则:指定需要关注的发件人(如领导、客户的邮箱)。
  2. 关键词规则:邮件标题或内容包含指定关键词(如你的名字、负责的项目名称)。
  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)

关键实现说明

  1. 核心功能:根据规则对邮件进行多维度分类,并按指定顺序返回分类结果。

  2. 分类逻辑(优先级匹配)对每封邮件按以下顺序匹配规则,命中即终止并归类:

    • 发件人匹配:检查邮件发件人是否包含rules['senders']中某类别的模式;
    • 标题关键词匹配:未命中发件人时,检查邮件标题是否含rules['keywords']中某类别的关键词;
    • 正文关键词匹配:前两步未命中时,检查邮件正文是否含rules['mentions']中某类别的关键词;
    • 其他归类:以上均未命中时,归入rules['others']中的类别。
  3. 结果排序与整合

    • classification_order指定的顺序整理分类结果;
    • 未在classification_order中定义的额外类别,追加到结果末尾;
    • 最终返回字典形式的分类结果(键为类别,值为该类别的邮件列表)。
  4. 辅助逻辑

    • 引入日志记录分类过程;
    • defaultdict简化分类结果的初始化与存储。

灵活扩展建议

  • 可添加 “排除规则”(如过滤掉 “广告”“订阅” 等关键词的邮件)。
  • 支持正则表达式匹配(如匹配特定格式的订单号、工号)。
  • 按邮件优先级(Priority 字段)过滤,只推送高优先级邮件。

2.3 通知模块:飞书消息推送

将重要邮件推送到飞书,需要借助飞书开放平台的 “自定义机器人” 功能。步骤很简单:创建机器人 → 获取 Webhook 地址 → 用 Python 发送 POST 请求。

2.3.1 飞书机器人准备

  1. 打开飞书,进入需要接收提醒的群聊 → 点击右上角 “设置” → “智能群助手” → “添加机器人” → 选择 “自定义机器人”。
  2. 给机器人命名(如 “邮件助手”),上传头像(可选),点击 “添加”。
  3. 复制生成的 “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

    关键实现说明

    1. 核心功能:将分类后的未读邮件,通过飞书机器人 Webhook 发送交互式卡片通知,支持代理访问与消息拆分。

    2. 请求配置

      • 代理设置:通过proxy参数配置 HTTP 代理(含地址、用户名、密码),用于请求转发;
      • 头部信息:固定Content-Type: application/json,符合飞书 Webhook 接口要求。
    3. 飞书消息构建

      • 模板类型:采用飞书interactive(交互式)卡片,头部标题固定为 “📬 未读邮件分类汇总”;
      • 内容填充:
        1. 遍历categorized_emails,为每个分类添加标题块(含类别名与邮件数量,用 Markdown 加粗);
        2. 为每封邮件生成条目块,包含主题(链接至 Outlook 邮件,调用_generate_outlook_url生成链接)、接收时间、发件人,使用飞书lark_md格式支持 Markdown。
    4. 消息拆分与发送

      • 拆分逻辑:调用_split_messages(未展示实现),推测因飞书消息长度限制,将超长卡片拆分为多个批次;
      • 批量发送:逐批次通过requests.post发送,超时 10 秒,统计成功次数,返回 “是否所有批次均发送成功” 的布尔值。
    5. 异常与日志

      • 捕获请求异常(如网络错误)、消息拆分错误,通过日志记录详情;
      • 打印发送成功计数,错误信息输出至日志。

    飞书消息优化建议

    • 可添加 “@指定人” 功能,紧急邮件直接 @你(需在飞书机器人设置中开启 “@所有人” 权限)。
    • 支持发送邮件附件预览(飞书机器人支持上传文件,可将邮件附件下载后再上传到飞书)。
    • 按规则设置不同的消息颜色(如 “紧急” 关键词用红色,普通重要邮件用蓝色)。

    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()

    关键实现说明

    1. 核心功能:通过定时任务循环执行邮件处理流程,包括获取未读邮件、分类整理并推送飞书通知。

    2. 主循环(main_loop

      • 初始化日志,检查配置文件(configs目录下的 ini 文件)是否存在;
      • 加载 YAML 配置,基于配置中的interval(间隔秒数),通过schedule库设置定时任务(执行job函数);
      • 无限循环运行定时任务,每 5 秒检查一次待执行任务;遇异常时记录日志并延迟 60 秒重试。
    3. 定时任务(job

      • 加载配置,若配置指定邮箱类型为online,通过OnlineManager获取未读邮件;
      • 调用classify_emails对未读邮件按规则分类(依赖配置中的rulesclassification_order);
      • 若分类结果非空且发送状态正常,调用send_feishu_notification推送飞书通知,完成后记录日志。
    4. 配置与日志

      • 依赖外部配置文件(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()

    关键实现说明

    1. 核心功能:检查指定配置目录(默认configs)下是否存在 YAML 配置文件,不存在时通过 GUI 弹窗提示错误,确保程序运行所需配置文件完备。

    2. 主要函数

      • check_ini_files
        1. 验证配置目录(config_dir)是否存在,不存在则调用show_alert提示;
        2. 查找目录下所有.yaml文件(注释提及 “INI 文件”,实际检查 YAML,可能为注释误差),无文件时弹窗提示;
        3. 找到文件则记录日志(数量)并返回True,否则返回False
      • show_alert:用tkinter创建隐藏主窗口,通过messagebox.showerror显示 “配置错误” 弹窗,提示目录 / 文件缺失及解决方案,弹窗关闭后销毁窗口。
    3. 关键细节

      • 依赖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,
        }

    关键实现说明

    1. 核心功能:检查数据库目录(.db)及数据库文件(processed.db)是否存在,目录不存在时自动创建,返回目录和文件的状态信息。

    2. 关键逻辑

      • 初始化数据库目录路径为当前工作目录下的 .db 文件夹;
      • 若目录不存在,创建该目录(os.makedirs(exist_ok=True) 确保创建时不报错),并记录日志,标记 have_db 为 False
      • 检查目录下是否存在 processed.db 文件,不存在则同样标记 have_db 为 False
      • 返回字典,包含 have_db(目录和文件是否均存在,均存在为 True)和 db_fileprocessed.db 的完整路径)。
    3. 关键细节

      • 用 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}"

    关键实现说明

    1. 核心功能:根据当前操作系统,生成对应平台兼容的 Outlook 邮件访问链接,参数entry_id为邮件唯一标识。

    2. 平台适配逻辑

      • 调用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}
    3. 关键细节

      • 依赖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

    关键实现说明

    1. 核心功能:将超过大小限制(默认 20KB)的飞书交互式卡片消息,拆分为多个符合大小要求的消息批次,确保消息能正常发送(规避平台消息大小限制)。

    2. 关键逻辑

      • 初始化当前批次:复制原消息的header(保持卡片标题一致性),初始化空elements(内容元素);
      • 遍历原消息的elements(卡片内容块),对每个元素:
        1. 尝试添加到 “临时批次”,计算添加后的大小;
        2. 若超限制,将 “当前批次” 加入结果列表,重新初始化 “当前批次” 并放入该元素;
        3. 未超限制则更新 “当前批次” 为 “临时批次”;
      • 处理剩余元素:遍历结束后,将未完成的 “当前批次” 加入结果列表。
    3. 大小计算方式通过json.dumps用紧凑格式(separators=(",", ":")去除空格)序列化消息,转utf-8字节后计算长度,模拟实际传输时的消息大小。

    4. 边界处理若单个元素超过 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
        }

    关键实现说明

    1. 核心功能:加载并解析项目配置目录下的config.yaml文件,提取通用配置、代理配置、邮件分类规则及分类顺序,返回结构化配置字典供其他模块使用。

    2. 主要逻辑

      • 配置路径:固定从当前工作目录的父目录(../configs/)读取config.yaml文件;
      • 安全解析:用yaml.safe_load加载 YAML 内容(避免恶意代码执行),解析失败时记录错误日志并抛出ValueError
      • 规则提取:从Classification字段中提取senders(发件人规则)、keywords(标题关键词规则)、mentions(正文关键词规则)、others(其他规则),组成rules字典;
      • 顺序整理:按senders→keywords→mentions→others的固定顺序,提取各规则下的类别名,生成classification_order(分类结果排序依据)。
    3. 返回结构输出字典包含 4 个核心部分:

      • general:通用配置(邮件检查间隔、飞书 Webhook、邮箱类型等);
      • proxy:代理配置(地址、用户名、密码);
      • rules:邮件分类规则;
      • classification_order:分类结果的展示顺序。
    4. 关键细节

      • 依赖项目目录结构(配置文件需放在上级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}")

    关键实现说明

    1. 核心功能:通过 SQLite 数据库管理已处理邮件的唯一标识(UID/entry_id),实现邮件去重(避免重复处理),包含数据库初始化、已处理 UID 加载、新 UID 保存三个核心操作。

    2. 主要函数

      • 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避免重复插入(利用主键唯一性约束);出错时回滚事务并记录日志。
    3. 关键细节

      • 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)
    
    

    关键实现说明

    1. 核心功能:加载项目配置目录下的logging.yaml文件,初始化 Python 的logging模块,统一配置日志的输出格式、级别、存储路径等(如控制台输出、文件记录)。

    2. 关键逻辑

      • 拼接日志配置文件路径:从当前工作目录的父目录(../configs/)中读取logging.yaml
      • 安全加载配置:用yaml.safe_load读取 YAML 文件(避免恶意代码执行),获取日志配置字典;
      • 应用日志配置:通过logging.config.dictConfig将配置字典应用到logging模块,覆盖默认日志设置。
    3. 关键细节

      • 依赖Path类处理文件路径,跨平台兼容性更强;
      • 配置文件路径固定,需项目目录结构满足 “上级目录有 configs 文件夹及 logging.yaml”;
      • 日志的具体行为(如日志级别、输出位置)由logging.yaml的内容决定,本函数仅负责加载和应用配置。

    四、项目优化与部署建议

    4.1 功能优化

    • 日志分级:将日志分为 INFO(普通信息)、WARNING(警告)、ERROR(错误)等级,方便快速定位问题。
    • 异常重试:网络波动时,邮件接收或飞书推送可能失败,可添加重试机制(如使用tenacity库)。
    • 规则热更新:支持在不重启程序的情况下,修改config.yaml后自动加载新规则。
    • 多邮箱支持:扩展代码,同时监控多个邮箱(如工作邮箱、个人邮箱)。

    4.2 部署方案

    要实现真正的 “24 小时全天候运行”,需要将程序部署到服务器或本地后台进程:

    方案 1:本地后台运行(Windows)

    1. 新建一个批处理文件start_assistant.bat,内容如下:

      bat

      @echo off
      python "C:\path\to\your\project\main.py"
      pause
      
    2. 右键点击批处理文件,选择 “创建快捷方式”。
    3. 右键点击快捷方式,选择 “属性”→“快捷方式”→“目标”,在末尾添加 > output.log 2>&1(将输出重定向到日志文件)。
    4. 将快捷方式拖到 “启动” 文件夹(Win+R 输入shell:startup打开),实现开机自启。

    方案 2:云服务器部署(Linux)

    1. 将项目文件上传到云服务器(如阿里云、腾讯云)。
    2. 使用nohup命令让程序在后台运行:

      bash

      nohup python3 main.py > assistant.log 2>&1 &
      
    3. 查看运行状态:

      bash

      ps aux | grep main.py  # 查看进程是否在运行
      tail -f assistant.log  # 实时查看日志
      
    4. 设置开机自启:通过systemd创建服务,确保服务器重启后程序自动运行。

    五、总结

    本文聚焦 “海量邮件过滤” 和 “飞书实时提醒” 两大核心需求,用 Python 实现了一个轻量化的邮件助手。通过 POP 协议接收邮件,自定义 YAML 规则过滤无关信息,再借助飞书机器人推送重要内容,彻底解决了 “邮件太多找不到重点” 的问题。

    整个项目的代码都围绕 “实用” 和 “易用” 展开,没有复杂的框架依赖,新手也能跟着配置和修改。你可以根据自己的工作场景,灵活调整过滤规则,甚至扩展更多功能(如微信推送、邮件自动回复)。

     

    评论
    成就一亿技术人!
    拼手气红包6.0元
    还能输入1000个字符
     
    红包 添加红包
    表情包 插入表情
     条评论被折叠 查看
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值