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
相关产品推荐
相关产品推荐

