feat: 添加邮件功能第一阶段 - 邮件基础设施

新增功能:
- 创建 email_service.py 邮件服务模块
  - 支持多SMTP配置(主备切换、故障转移)
  - 发送纯文本/HTML邮件
  - 发送带附件邮件(支持ZIP压缩)
  - 异步发送队列(多线程工作池)
  - 每日发送限额控制
  - 发送日志记录和统计

- 数据库表结构
  - smtp_configs: 多SMTP配置表
  - email_settings: 全局邮件设置
  - email_tokens: 邮件验证Token
  - email_logs: 邮件发送日志
  - email_stats: 邮件发送统计

- API接口
  - GET/POST /yuyx/api/email/settings: 全局邮件设置
  - CRUD /yuyx/api/smtp/configs: SMTP配置管理
  - POST /yuyx/api/smtp/configs/<id>/test: 测试SMTP连接
  - POST /yuyx/api/smtp/configs/<id>/primary: 设为主配置
  - GET /yuyx/api/email/stats: 邮件统计
  - GET /yuyx/api/email/logs: 邮件日志
  - POST /yuyx/api/email/logs/cleanup: 清理日志

- 后台管理页面
  - 新增"邮件配置"Tab
  - 全局邮件开关、故障转移开关
  - SMTP配置列表管理
  - 添加/编辑SMTP配置弹窗
  - 邮件发送统计展示
  - 邮件日志查询和清理

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-11 21:38:28 +08:00
parent 364566df99
commit 2f762db337
4 changed files with 2128 additions and 1 deletions

208
app.py
View File

@@ -3463,7 +3463,7 @@ def checkpoint_resume(task_id):
return jsonify({'success': False, 'message': str(e)}), 500
@app.route('/yuyx/api/checkpoint/<task_id>/abandon', methods=['POST'])
@admin_required
@admin_required
def checkpoint_abandon(task_id):
try:
if checkpoint_mgr.abandon_task(task_id):
@@ -3472,6 +3472,198 @@ def checkpoint_abandon(task_id):
except Exception as e:
return jsonify({'success': False, 'message': str(e)}), 500
# ==================== 邮件服务API ====================
import email_service
@app.route('/yuyx/api/email/settings', methods=['GET'])
@admin_required
def get_email_settings_api():
"""获取全局邮件设置"""
try:
settings = email_service.get_email_settings()
return jsonify(settings)
except Exception as e:
logger.error(f"获取邮件设置失败: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/yuyx/api/email/settings', methods=['POST'])
@admin_required
def update_email_settings_api():
"""更新全局邮件设置"""
try:
data = request.json
enabled = data.get('enabled', False)
failover_enabled = data.get('failover_enabled', True)
email_service.update_email_settings(enabled, failover_enabled)
return jsonify({'success': True})
except Exception as e:
logger.error(f"更新邮件设置失败: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/yuyx/api/smtp/configs', methods=['GET'])
@admin_required
def get_smtp_configs_api():
"""获取所有SMTP配置列表"""
try:
configs = email_service.get_smtp_configs(include_password=False)
return jsonify(configs)
except Exception as e:
logger.error(f"获取SMTP配置失败: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/yuyx/api/smtp/configs', methods=['POST'])
@admin_required
def create_smtp_config_api():
"""创建SMTP配置"""
try:
data = request.json
# 验证必填字段
if not data.get('host'):
return jsonify({'error': 'SMTP服务器地址不能为空'}), 400
if not data.get('username'):
return jsonify({'error': 'SMTP用户名不能为空'}), 400
config_id = email_service.create_smtp_config(data)
return jsonify({'success': True, 'id': config_id})
except Exception as e:
logger.error(f"创建SMTP配置失败: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/yuyx/api/smtp/configs/<int:config_id>', methods=['GET'])
@admin_required
def get_smtp_config_api(config_id):
"""获取单个SMTP配置详情"""
try:
config = email_service.get_smtp_config(config_id, include_password=False)
if not config:
return jsonify({'error': '配置不存在'}), 404
return jsonify(config)
except Exception as e:
logger.error(f"获取SMTP配置失败: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/yuyx/api/smtp/configs/<int:config_id>', methods=['PUT'])
@admin_required
def update_smtp_config_api(config_id):
"""更新SMTP配置"""
try:
data = request.json
if email_service.update_smtp_config(config_id, data):
return jsonify({'success': True})
return jsonify({'error': '更新失败'}), 400
except Exception as e:
logger.error(f"更新SMTP配置<EFBFBD><EFBFBD>败: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/yuyx/api/smtp/configs/<int:config_id>', methods=['DELETE'])
@admin_required
def delete_smtp_config_api(config_id):
"""删除SMTP配置"""
try:
if email_service.delete_smtp_config(config_id):
return jsonify({'success': True})
return jsonify({'error': '删除失败'}), 400
except Exception as e:
logger.error(f"删除SMTP配置失败: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/yuyx/api/smtp/configs/<int:config_id>/test', methods=['POST'])
@admin_required
def test_smtp_config_api(config_id):
"""测试SMTP配置"""
try:
data = request.json
test_email = data.get('email', '')
if not test_email:
return jsonify({'error': '请提供测试邮箱'}), 400
# 简单验证邮箱格式
import re
if not re.match(r'^[^\s@]+@[^\s@]+\.[^\s@]+$', test_email):
return jsonify({'error': '邮箱格式不正确'}), 400
result = email_service.test_smtp_config(config_id, test_email)
return jsonify(result)
except Exception as e:
logger.error(f"测试SMTP配置失败: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/yuyx/api/smtp/configs/<int:config_id>/primary', methods=['POST'])
@admin_required
def set_primary_smtp_config_api(config_id):
"""设置主SMTP配置"""
try:
if email_service.set_primary_smtp_config(config_id):
return jsonify({'success': True})
return jsonify({'error': '设置失败'}), 400
except Exception as e:
logger.error(f"设置主SMTP配置失败: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/yuyx/api/email/stats', methods=['GET'])
@admin_required
def get_email_stats_api():
"""获取邮件发送统计"""
try:
stats = email_service.get_email_stats()
return jsonify(stats)
except Exception as e:
logger.error(f"获取邮件统计失败: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/yuyx/api/email/logs', methods=['GET'])
@admin_required
def get_email_logs_api():
"""获取邮件发送日志"""
try:
page = request.args.get('page', 1, type=int)
page_size = request.args.get('page_size', 20, type=int)
email_type = request.args.get('type', None)
status = request.args.get('status', None)
# 限制page_size范围
page_size = min(max(page_size, 10), 100)
result = email_service.get_email_logs(page, page_size, email_type, status)
return jsonify(result)
except Exception as e:
logger.error(f"获取邮件日志失败: {e}")
return jsonify({'error': str(e)}), 500
@app.route('/yuyx/api/email/logs/cleanup', methods=['POST'])
@admin_required
def cleanup_email_logs_api():
"""清理过期邮件日志"""
try:
data = request.json or {}
days = data.get('days', 30)
# 限制days范围
days = min(max(days, 7), 365)
deleted = email_service.cleanup_email_logs(days)
return jsonify({'success': True, 'deleted': deleted})
except Exception as e:
logger.error(f"清理邮件日志失败: {e}")
return jsonify({'error': str(e)}), 500
# 初始化浏览器池(在后台线程中预热,不阻塞启动)
# ==================== 用户定时任务API ====================
@@ -3815,6 +4007,13 @@ def cleanup_on_exit():
except:
pass
# 3.5 关闭邮件队列
print("- 关闭邮件队列...")
try:
email_service.shutdown_email_queue()
except:
pass
# 4. 关闭数据库连接池
print("- 关闭数据库连接池...")
try:
@@ -3850,6 +4049,13 @@ if __name__ == '__main__':
checkpoint_mgr = get_checkpoint_manager()
print("✓ 任务断点管理器已初始化")
# 初始化邮件服务
try:
email_service.init_email_service()
print("✓ 邮件服务已初始化")
except Exception as e:
print(f"警告: 邮件服务初始化失败: {e}")
# 启动内存清理调度器
start_cleanup_scheduler()