You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python SMTP邮件脚本随机停止发信故障排查

故障根因分析

监控程序随机停止发信的核心原因是 watchdog事件回调线程未做异常防护,偶发错误直接导致工作线程崩溃,而主线程仍在驻留,造成程序“正常运行”的假象,具体触发场景包括:

  • 文件读取时机错误触发IO异常:on_created事件会在文件刚被创建、还在被日志写入进程占用时立刻触发,此时直接执行文件读取操作会抛出文件占用、权限不足类错误。该类错误触发概率随机:小体积日志写入快时不会触发,大日志写入慢、系统IO繁忙时就会命中,直接打崩回调线程。
  • SMTP连接无容错机制触发网络类异常:现有SMTP逻辑未设置连接超时、未做异常捕获,遇到临时网络抖动、TLS握手失败、Gmail服务端临时拦截(非发送额度超限,多为短时间新建连接过多触发的临时风控)、会话超时等偶发问题时,会直接抛出SMTP相关异常终止线程。且每次发信都新建/销毁SMTP连接,本身就会提升触发服务端临时拦截的概率。
  • 路径拼接逻辑错误触发路径异常:代码中event.src_path本身已经是新文件的绝对路径,额外拼接__file__所在目录会生成非法路径(比如监听目录在D盘、exe在C盘时,会拼出类似C:\app\D:\logs\xxx.log的无效路径),当监听路径配置变化、系统返回路径格式偶发调整时,会触发文件不存在异常打崩线程。
  • 无故障日志导致问题不可追溯:所有逻辑未做错误记录,线程崩溃后无任何提示,用户只能观察到发信停止,无法定位具体触发点。
修复方案

针对上述问题,按以下逻辑调整代码即可解决随机失效问题:

  • 给整个事件回调逻辑加全链路异常捕获,单文件处理失败不影响后续事件处理
  • 修正路径读取逻辑,直接使用event.src_path访问文件,去掉多余的路径拼接
  • 新增文件就绪判断逻辑,等日志文件写入完成、释放句柄后再读取内容,避免文件占用错误
  • 给SMTP连接增加超时配置、有限重试机制,用上下文管理器保证连接无论是否报错都能正常释放
  • 增加本地错误日志记录,后续出现问题可直接通过日志定位原因

修复后的核心参考代码:

import time
import os
import smtplib
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
import traceback

# 配置项
EMAIL_ADDRESS = "你的邮箱"
EMAIL_PASSWORD = "你的应用密码"
WATCH_PATH = "你的日志监听目录"
LOG_PATH = os.path.join(os.path.dirname(__file__), "monitor_error.log")
FILE_WAIT_RETRY = 10  # 最多等10秒等文件写完
SMTP_RETRY = 3  # 发信最多重试3次

def log_error(err_msg):
    """记录错误到本地日志"""
    with open(LOG_PATH, "a", encoding="utf-8") as f:
        f.write(f"[{time.ctime()}] {err_msg}\n{traceback.format_exc()}\n")

class ExampleHandler(FileSystemEventHandler):
    def sendmail(self, email, password, message):
        # 带重试的发信逻辑,用上下文管理器自动释放连接
        for i in range(SMTP_RETRY):
            try:
                with smtplib.SMTP(host="smtp.gmail.com", port=587, timeout=10) as server:
                    server.ehlo()
                    server.starttls()
                    server.login(email, password)
                    server.sendmail(email, email, message)
                return True
            except Exception as e:
                if i == SMTP_RETRY -1:
                    raise e
                time.sleep(2)

    def wait_file_ready(self, file_path):
        """等文件被释放可以读取"""
        for _ in range(FILE_WAIT_RETRY):
            try:
                with open(file_path, "r", encoding="utf-8") as f:
                    f.read(1)
                return True
            except:
                time.sleep(1)
        raise TimeoutError(f"文件等待超时,无法读取: {file_path}")

    def on_created(self, event):
        # 跳过目录创建事件
        if event.is_directory:
            return
        file_path = event.src_path
        try:
            # 等文件写完再读
            self.wait_file_ready(file_path)
            with open(file_path, 'r', encoding="utf-8") as input_file:
                content = input_file.read()
            self.sendmail(EMAIL_ADDRESS, EMAIL_PASSWORD, content)
        except Exception as e:
            # 所有异常都记日志,不抛出去打崩线程
            log_error(f"处理文件{file_path}失败: {str(e)}")

if __name__ == "__main__":
    observer = Observer()
    event_handler = ExampleHandler()
    observer.schedule(event_handler, path=WATCH_PATH, recursive=False)
    observer.start()

    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        observer.stop()
    observer.join()

补充说明:如果使用Gmail SMTP发信,需要确认已开启邮箱两步验证,使用生成的应用专用密码登录,不要使用账号原始密码,避免偶发的登录校验失败问题。

内容的提问来源于stack exchange,提问作者coffee-and-code

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 06:27:24