#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
定时检查新邮件并自动分类脚本
支持 IMAP 邮箱,根据发件人/主题关键词移动邮件到指定文件夹
"""
import imaplib
import email
from email.header import decode_header
import time
import logging
import os
import sys
from datetime import datetime, timedelta
from typing import List, Dict, Tuple, Optional
# ================== 配置区 ==================
# 邮箱配置(请修改为您的邮箱信息)
EMAIL_CONFIG = {
"imap_server": "imap.gmail.com", # imap.gmail.com / imap.qq.com
"port": 993,
"use_ssl": True,
"username": "your_email@gmail.com", # 邮箱地址
"password": "your_app_password" # 建议使用应用专用密码
}
# 检查间隔(秒)
CHECK_INTERVAL = 300 # 5分钟
# 分类规则:规则列表,按顺序匹配,匹配第一个则移动
# folder: 目标文件夹(IMAP 文件夹名,如 INBOX.分类名 或 直接使用中文)
# 如果目标文件夹不存在,脚本会自动创建
CLASSIFY_RULES = [
{
"name": "工作邮件",
"folder": "工作",
"conditions": {
"from_keywords": ["@company.com", "boss@", "hr@"], # 发件人包含这些词
"subject_keywords": ["会议", "报告", "项目", "deadline", "urgent"]
}
},
{
"name": "订阅/通知",
"folder": "订阅",
"conditions": {
"from_keywords": ["noreply@", "notification@", "weekly@", "newsletter@"],
"subject_keywords": ["订阅", "周报", "月报", "newsletter", "unsubscribe"]
}
},
{
"name": "个人邮件",
"folder": "个人",
"conditions": {
"from_keywords": ["@gmail.com", "@qq.com", "friend@", "family@"],
"subject_keywords": ["聚会", "旅行", "照片", "hello"]
}
},
# 可以添加更多规则
]
# 日志配置
LOG_FILE = "email_classifier.log"
# ==========================================
# 全局变量
logger = logging.getLogger(__name__)
processed_uid_set = set() # 已处理邮件的 UID 集合,防止重复处理
def setup_logging():
"""配置日志输出到文件和控制台"""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(LOG_FILE, encoding='utf-8'),
logging.StreamHandler(sys.stdout)
]
)
def decode_mime_string(s: str) -> str:
"""解码 MIME 编码的字符串(如 =?UTF-8?B?xxx?=)"""
if not s:
return ""
try:
decoded_parts = decode_header(s)
result = []
for part, charset in decoded_parts:
if isinstance(part, bytes):
if charset:
result.append(part.decode(charset, errors='replace'))
else:
result.append(part.decode('utf-8', errors='replace'))
else:
result.append(str(part))
return ''.join(result)
except Exception:
return str(s)
def get_text_from_email(msg) -> str:
"""从邮件对象中提取纯文本内容(用于调试/日志)"""
if msg.is_multipart():
for part in msg.walk():
if part.get_content_type() == "text/plain":
try:
return part.get_payload(decode=True).decode('utf-8', errors='replace')
except:
continue
else:
try:
return msg.get_payload(decode=True).decode('utf-8', errors='replace')
except:
return ""
return ""
def check_conditions(msg, rules: dict) -> bool:
"""
检查邮件是否匹配规则
规则说明:
- from_keywords: 发件人地址包含任一关键词(大小写不敏感)
- subject_keywords: 主题包含任一关键词(大小写不敏感)
当同一类条件(如发件人)有多词时,满足任一即可;
不同条件之间是 AND 关系(发件人 AND 主题)。
"""
# 获取发件人
from_addr = ""
from_header = msg.get("From", "")
if from_header:
from_addr = decode_mime_string(from_header).lower()
# 获取主题
subject = ""
subject_header = msg.get("Subject", "")
if subject_header:
subject = decode_mime_string(subject_header).lower()
conditions = rules.get("conditions", {})
from_kw = conditions.get("from_keywords", [])
subject_kw = conditions.get("subject_keywords", [])
# 如果没有任何条件,则不匹配
if not from_kw and not subject_kw:
return False
# 检查发件人关键词
from_match = True
if from_kw:
from_match = any(kw.lower() in from_addr for kw in from_kw)
# 检查主题关键词
subject_match = True
if subject_kw:
subject_match = any(kw.lower() in subject for kw in subject_kw)
# 两种条件同时满足(AND)
return from_match and subject_match
def classify_and_move(mail: imaplib.IMAP4_SSL, email_uid: bytes) -> Optional[str]:
"""
对单封邮件执行分类并移动
返回移动到的文件夹名称,如果未分类返回 None
"""
# 获取邮件数据
typ, data = mail.uid('fetch', email_uid, '(BODY.PEEK[HEADER])')
if typ != 'OK':
logger.warning(f"无法获取邮件 UID {email_uid} 的头部")
return None
msg = email.message_from_bytes(data[0][1])
subject = decode_mime_string(msg.get("Subject", ""))
from_addr = decode_mime_string(msg.get("From", ""))
logger.info(f"检查邮件: 来自 [{from_addr}] 主题 [{subject}]")
# 遍历规则,匹配第一个即停止
for rule in CLASSIFY_RULES:
if check_conditions(msg, rule):
target_folder = rule["folder"]
logger.info(f" -> 归类为 [{rule['name']}],移动到文件夹 [{target_folder}]")
# 创建目标文件夹(如果不存在)
try:
# 尝试选择目标文件夹(判断是否存在)
status, _ = mail.select(f'"{target_folder}"')
if status != 'OK':
# 创建文件夹
status, _ = mail.create(f'"{target_folder}"')
if status == 'OK':
logger.info(f" 创建目标文件夹 [{target_folder}] 成功")
else:
logger.error(f" 创建目标文件夹 [{target_folder}] 失败")
return None
except Exception as e:
logger.error(f" 操作文件夹时出错: {e}")
return None
# 移动邮件:复制到目标文件夹,然后删除原邮件
try:
# 复制到目标文件夹
typ, _ = mail.uid('copy', email_uid, target_folder)
if typ != 'OK':
logger.error(f" 复制邮件到 [{target_folder}] 失败")
return None
# 标记原邮件为已删除
typ, _ = mail.uid('store', email_uid, '+FLAGS', '\\Deleted')
if typ != 'OK':
logger.warning(f" 标记删除原邮件失败,可能已移动")
else:
# 立即清除(expunge)当前文件夹中的已删除邮件
# 注意:expunge 会影响所有标记删除的邮件,而非仅此一封
# 但我们后续会调用 expunge,这里可以省略
pass
logger.info(f" 成功移动到 [{target_folder}]")
return target_folder
except Exception as e:
logger.error(f" 移动邮件时出错: {e}")
return None
logger.info(f" -> 未匹配任何规则,留在收件箱")
return None
def process_new_emails(mail: imaplib.IMAP4_SSL):
"""检查并处理新邮件"""
global processed_uid_set
try:
# 选择收件箱(只读模式先检查,但移动需要写入,所以直接选择可写)
status, messages = mail.select('INBOX', readonly=False)
if status != 'OK':
logger.error("无法选择 INBOX")
return
# 搜索今天收到的邮件(更高效,也可改为 'UNSEEN' 但会标记已读)
# 使用 SINCE 获取最近一天的邮件,避免无限扫描旧邮件
today = datetime.now().strftime("%d-%b-%Y")
typ, data = mail.uid('search', None, f'(SINCE {today})')
if typ != 'OK':
logger.error("搜索邮件失败")
return
uids = data[0].split()
if not uids:
logger.info("没有新邮件")
return
# 去重:跳过已处理的 UID
new_uids = [uid for uid in uids if uid not in processed_uid_set]
if not new_uids:
logger.info(f"所有邮件均已处理过(收件箱共 {len(uids)} 封,已处理 {len(processed_uid_set)} 封)")
return
logger.info(f"发现 {len(new_uids)} 封待处理邮件")
# 处理每一封新邮件
moved_count = 0
for uid in new_uids:
result = classify_and_move(mail, uid)
if result:
moved_count += 1
# 记录已处理(无论是否移动)
processed_uid_set.add(uid)
# 清理已删除的邮件(expunge),必须在同一个 select 后执行
if moved_count > 0:
mail.expunge()
logger.info(f"已完成清理,共移动 {moved_count} 封邮件")
# 限制 processed_uid_set 大小,避免内存无限增长(保留最近 5000 个)
if len(processed_uid_set) > 5000:
# 简单删除较早的一半
old_uids = list(processed_uid_set)[:2500]
for uid in old_uids:
processed_uid_set.discard(uid)
logger.info(f"清理 UID 缓存,当前大小 {len(processed_uid_set)}")
except Exception as e:
logger.error(f"处理邮件时发生异常: {e}", exc_info=True)
def main_loop():
"""主循环:定时连接并检查"""
logger.info("=== 邮件分类脚本启动 ===")
logger.info(f"IMAP 服务器: {EMAIL_CONFIG['imap_server']}")
logger.info(f"检查间隔: {CHECK_INTERVAL} 秒")
logger.info(f"分类规则数: {len(CLASSIFY_RULES)}")
logger.info("等待首次执行...")
# 连接邮箱
mail = None
while True:
try:
if mail is None:
logger.info("正在连接邮箱...")
if EMAIL_CONFIG["use_ssl"]:
mail = imaplib.IMAP4_SSL(EMAIL_CONFIG["imap_server"], EMAIL_CONFIG["port"])
else:
mail = imaplib.IMAP4(EMAIL_CONFIG["imap_server"], EMAIL_CONFIG["port"])
mail.login(EMAIL_CONFIG["username"], EMAIL_CONFIG["password"])
logger.info("邮箱连接成功")
# 执行一次检查
process_new_emails(mail)
# 等待下一次执行
logger.info(f"等待 {CHECK_INTERVAL} 秒后再次检查...")
time.sleep(CHECK_INTERVAL)
except imaplib.IMAP4.abort as e:
logger.error(f"IMAP 连接中断: {e},将尝试重新连接...")
mail = None
time.sleep(30) # 等待30秒后重连
except KeyboardInterrupt:
logger.info("用户中断,退出程序")
if mail:
try:
mail.logout()
except:
pass
sys.exit(0)
except Exception as e:
logger.error(f"主循环异常: {e}", exc_info=True)
logger.info("等待 60 秒后重试...")
time.sleep(60)
if __name__ == "__main__":
setup_logging()
main_loop()
使用说明
安装依赖
脚本仅使用 Python 标准库(imaplib、email 等),无需额外安装,确保 Python 版本 ≥ 3.6 即可。

修改配置
- 邮箱配置:修改
EMAIL_CONFIG中的服务器地址、端口、用户名和密码(建议使用应用专用密码)。 - 检查间隔:调整
CHECK_INTERVAL变量(单位:秒),默认 300 秒(5分钟)。 - 分类规则:编辑
CLASSIFY_RULES列表,每个规则包含:name:规则描述(仅用于日志)folder:目标文件夹(IMAP 文件夹名,支持中文)conditions:匹配条件,支持from_keywords(发件人关键词)和subject_keywords(主题关键词)
运行脚本
python3 email_classifier.py
脚本会在后台持续运行,每 5 分钟检查一次收件箱,日志将输出到控制台和 email_classifier.log 文件。
注意事项
- 密码安全:建议为邮箱开启 IMAP 并生成“应用专用密码”,避免使用主密码。
- 文件夹自动创建:如果目标文件夹不存在,脚本会自动创建。
- 已处理缓存:脚本会记录已处理的邮件 UID,避免重复移动,重启脚本会清空缓存,但已移动的邮件不会重复处理(因为已不在收件箱)。
- 停止脚本:按
Ctrl+C可安全退出。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播