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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user