feat: 邮件配置页面 + 数据库持久化

- 新增 /email 配置页面 + /api/email/config API (GET/POST)
- 新增 /api/email/test-send 测试邮件 + /api/email/reset 重置
- EmailConfig SQLite 表持久化配置,回退到环境变量
- mailer: get_config 优先读 DB,send_report 读取 DB 密码
- 修复 _send_smtp 登录使用环境变量密码的 bug
- 前端新增配置表单 (启用开关/SMTP/TLS/认证/发件人/收件人/时间)
- CSS 暗色主题样式 + 响应式适配
- 导航栏新增「设置」链接
This commit is contained in:
Your Name
2026-07-08 14:36:41 +08:00
parent 3ab73805a3
commit 89dc6b4449
7 changed files with 788 additions and 10 deletions
+86 -1
View File
@@ -700,6 +700,17 @@ def cleanup_old():
return jsonify({"ok": True, "deleted": count})
# ═══════════════════════════════════════════════════════════════════════════
# EmailConfig — 邮件配置持久化 (Web UI 写入, mailer 读取)
# ═══════════════════════════════════════════════════════════════════════════
class EmailConfig(db.Model):
__tablename__ = 'email_config'
id = db.Column(db.Integer, primary_key=True)
key = db.Column(db.String(50), unique=True, nullable=False)
value = db.Column(db.Text, nullable=False, default='')
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# ═══════════════════════════════════════════════════════════════════════════
# Email Routes (延迟导入 mailer, 避免在 mailer 缺失时整个 app 起不来)
# ═══════════════════════════════════════════════════════════════════════════
@@ -756,6 +767,80 @@ def email_preview():
headers={"Content-Disposition": 'inline; filename="mail-preview.txt"'})
@app.route("/email")
def email_config():
"""邮件配置页面。"""
return render_template("email_config.html")
@app.route("/api/email/config")
def email_config_api():
"""获取完整邮件配置 (含密码, 仅本地管理用)。"""
ml = _get_mailer()
if not ml:
return jsonify({"ok": False, "error": "mailer.py 不可用"}), 500
cfg = ml.get_config_full()
errs = ml.config_errors(cfg)
return jsonify({
"ok": True,
"configured": len(errs) == 0,
"config_errors": errs,
**cfg,
})
@app.route("/api/email/config", methods=["POST"])
def email_config_save():
"""保存邮件配置到数据库。"""
data = request.get_json(silent=True) or {}
ml = _get_mailer()
if not ml:
return jsonify({"ok": False, "error": "mailer.py 不可用"}), 500
try:
saved = ml.save_config(data)
except Exception as e:
return jsonify({"ok": False, "error": f"保存失败: {e}"}), 500
cfg = ml.get_config_full()
errs = ml.config_errors(cfg)
return jsonify({
"ok": True,
"saved": saved,
"configured": len(errs) == 0,
"config_errors": errs,
})
@app.route("/api/email/reset", methods=["POST"])
def email_config_reset():
"""清除数据库配置, 回退到环境变量。"""
ml = _get_mailer()
if not ml:
return jsonify({"ok": False, "error": "mailer.py 不可用"}), 500
try:
ml.reset_config()
except Exception as e:
return jsonify({"ok": False, "error": f"重置失败: {e}"}), 500
cfg = ml.get_config_full()
errs = ml.config_errors(cfg)
return jsonify({
"ok": True,
"message": "已清除数据库配置, 回退到环境变量",
"configured": len(errs) == 0,
"config_errors": errs,
})
@app.route("/api/email/test-send", methods=["POST"])
def email_test_send():
"""发送测试邮件 (从 UI 手动触发)。"""
ml = _get_mailer()
if not ml:
return jsonify({"ok": False, "error": "mailer.py 不可用"}), 500
result = ml.send_report()
status = 200 if result["ok"] else 500
return jsonify(result), status
@app.route("/api/email/send", methods=["POST"])
def email_send():
"""
@@ -798,7 +883,7 @@ def _scheduler_loop():
print("[scheduler] 邮件调度线程已启动", flush=True)
while True:
try:
cfg = mailer.get_config()
cfg = mailer.get_config_full()
if not cfg["enabled"]:
# 关闭, 每小时看一眼
time.sleep(3600)