内置 gmail/qq/163/126/outlook/hotmail/live, 优先级:report-config.json > 自动识别 > 环境变量 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
147 lines
5.5 KiB
Python
147 lines
5.5 KiB
Python
#!/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()
|