- report-config.json 新增 smtp_host、smtp_port 字段(不敏感,可提交)
- 环境变量仅保留 SMTP_USER 和 SMTP_PASSWORD
- 修复 port 994 被误判为 STARTTLS 的问题,SSL 端口统一为 {465, 994}
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
127 lines
4.5 KiB
Python
127 lines
4.5 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
周报邮件发送脚本
|
||
|
||
SMTP 服务器地址和端口从 report-config.json 读取(不敏感,可提交)。
|
||
账号和密码从环境变量读取(敏感,不入库)。
|
||
|
||
report-config.json 配置项:
|
||
smtp_host - SMTP 服务器地址(如 smtphz.qiye.163.com)
|
||
smtp_port - SMTP 端口(SSL用465/994,STARTTLS用587)
|
||
|
||
必须设置的环境变量:
|
||
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
|
||
|
||
|
||
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:
|
||
host = config.get("smtp_host") or os.environ.get("SMTP_HOST")
|
||
port = config.get("smtp_port") or os.environ.get("SMTP_PORT", "465")
|
||
user = os.environ.get("SMTP_USER")
|
||
password = os.environ.get("SMTP_PASSWORD")
|
||
|
||
if not host:
|
||
print("错误:SMTP 服务器地址未配置,请在 report-config.json 中添加 smtp_host", 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()
|