#!/usr/bin/env python3 """ 周报邮件发送脚本 SMTP 服务器地址和端口优先级: 1. report-config.json 中的 smtp_host / smtp_port(最高优先级) 2. 根据 SMTP_USER 邮箱后缀自动匹配内置服务商 3. 环境变量 SMTP_HOST / SMTP_PORT(兜底) 账号和密码从环境变量读取(敏感,不入库): SMTP_USER - 发件人邮箱 SMTP_PASSWORD - SMTP 密码或授权码 """ import argparse import json import os import smtplib import sys from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText # 内置服务商:邮箱后缀 -> (host, port) SMTP_PROVIDERS = { "gmail.com": ("smtp.gmail.com", 587), "qq.com": ("smtp.qq.com", 465), "163.com": ("smtp.163.com", 465), "126.com": ("smtp.126.com", 465), "outlook.com": ("smtp.office365.com", 587), "hotmail.com": ("smtp.office365.com", 587), "live.com": ("smtp.office365.com", 587), } def load_config(config_path: str) -> dict: try: with open(config_path, "r", encoding="utf-8") as f: return json.load(f) except FileNotFoundError: print(f"错误:找不到配置文件 {config_path}", file=sys.stderr) print("请在项目根目录创建 report-config.json,格式:", file=sys.stderr) print('{"recipients": ["email@example.com"], "project_name": "项目名"}', file=sys.stderr) sys.exit(1) except json.JSONDecodeError as e: print(f"错误:report-config.json 格式错误 - {e}", file=sys.stderr) sys.exit(1) def get_smtp_config(config: dict) -> tuple: user = os.environ.get("SMTP_USER") password = os.environ.get("SMTP_PASSWORD") # 根据邮箱后缀自动匹配服务商默认值 auto_host, auto_port = None, None if user and "@" in user: suffix = user.split("@", 1)[1].lower() if suffix in SMTP_PROVIDERS: auto_host, auto_port = SMTP_PROVIDERS[suffix] print(f"自动识别服务商:{suffix} -> {auto_host}:{auto_port}", file=sys.stderr) # 优先级:config > 自动识别 > 环境变量 host = config.get("smtp_host") or auto_host or os.environ.get("SMTP_HOST") port = config.get("smtp_port") or auto_port or os.environ.get("SMTP_PORT", "465") if not host: print("错误:无法确定 SMTP 服务器地址", file=sys.stderr) print("请在 report-config.json 中添加 smtp_host,或使用内置服务商(gmail/qq/163/outlook)", file=sys.stderr) sys.exit(1) missing = [k for k, v in {"SMTP_USER": user, "SMTP_PASSWORD": password}.items() if not v] if missing: print(f"错误:缺少以下环境变量:{', '.join(missing)}", file=sys.stderr) print("请在 shell 中设置(不要写入任何文件):", file=sys.stderr) for k in missing: print(f" export {k}=your_value", file=sys.stderr) sys.exit(1) try: port = int(port) except ValueError: print(f"错误:smtp_port 必须是数字,当前值为 '{port}'", file=sys.stderr) sys.exit(1) return host, port, user, password def send_email(recipients: list, subject: str, body_html: str, sender: str, config: dict): host, port, user, password = get_smtp_config(config) msg = MIMEMultipart("alternative") msg["Subject"] = subject msg["From"] = sender or user msg["To"] = ", ".join(recipients) msg.attach(MIMEText(body_html, "html", "utf-8")) # 465 和 994 均为 SSL 直连端口,587 使用 STARTTLS ssl_ports = {465, 994} try: if port in ssl_ports: with smtplib.SMTP_SSL(host, port, timeout=30) as smtp: smtp.login(user, password) smtp.sendmail(user, recipients, msg.as_string()) else: with smtplib.SMTP(host, port, timeout=30) as smtp: smtp.ehlo() smtp.starttls() smtp.login(user, password) smtp.sendmail(user, recipients, msg.as_string()) except smtplib.SMTPAuthenticationError: print("错误:SMTP 认证失败,请检查 SMTP_USER 和 SMTP_PASSWORD", file=sys.stderr) sys.exit(1) except smtplib.SMTPConnectError: print(f"错误:无法连接到 {host}:{port},请检查 SMTP_HOST 和 SMTP_PORT", file=sys.stderr) sys.exit(1) except Exception as e: print(f"错误:发送失败 - {e}", file=sys.stderr) sys.exit(1) print(f"邮件已发送至:{', '.join(recipients)}") def main(): parser = argparse.ArgumentParser(description="发送周报邮件") parser.add_argument("--config", required=True, help="report-config.json 路径") parser.add_argument("--subject", required=True, help="邮件主题") parser.add_argument("--body-file", required=True, help="HTML 正文文件路径") args = parser.parse_args() config = load_config(args.config) recipients = config.get("recipients", []) if not recipients: print("错误:report-config.json 中 recipients 为空", file=sys.stderr) sys.exit(1) try: with open(args.body_file, "r", encoding="utf-8") as f: body_html = f.read() except FileNotFoundError: print(f"错误:找不到邮件正文文件 {args.body_file}", file=sys.stderr) sys.exit(1) send_email(recipients, args.subject, body_html, sender=config.get("sender", ""), config=config) if __name__ == "__main__": main()