admin b047160dfb 根据 SMTP_USER 邮箱后缀自动匹配 SMTP 服务商
内置 gmail/qq/163/126/outlook/hotmail/live,
优先级:report-config.json > 自动识别 > 环境变量

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 14:39:33 +08:00

147 lines
5.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 服务器地址和端口优先级:
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()