admin 5895c14240 将 smtp_host/smtp_port 移入 report-config.json,修复 SSL 端口判断
- 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>
2026-02-27 14:33:02 +08:00

127 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
周报邮件发送脚本
SMTP 服务器地址和端口从 report-config.json 读取(不敏感,可提交)。
账号和密码从环境变量读取(敏感,不入库)。
report-config.json 配置项:
smtp_host - SMTP 服务器地址(如 smtphz.qiye.163.com
smtp_port - SMTP 端口SSL用465/994STARTTLS用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()