commit aeffc4f8b87c65d8fe28b1502626231d271d8522 Author: huangzhenpc Date: Tue Feb 25 19:50:00 2025 +0800 初始化提交,包含完整的邮件系统代码 diff --git a/.env b/.env new file mode 100644 index 0000000..6609999 --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +FLASK_ENV=production +SECRET_KEY=your_random_secure_key_here +MAIL_DOMAINS=example.com,mail.example.com +DATABASE_URL=sqlite:///email_system.db \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c506221 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# virtualenv +venv/ +ENV/ + +# 日志和数据文件 +logs/ +*.log +email_data/ +db/ +*.db + +# 本地设置 +.env.local + +# IDE相关 +.idea/ +.vscode/ +*.swp +*.swo + +# 系统文件 +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/DEPLOYMENT_GUIDE.md b/DEPLOYMENT_GUIDE.md new file mode 100644 index 0000000..e3cfd69 --- /dev/null +++ b/DEPLOYMENT_GUIDE.md @@ -0,0 +1,283 @@ +# 邮箱系统部署指南 + +本文档提供在生产环境中部署邮箱系统的详细步骤。 + +## 系统要求 + +- Python 3.7+ +- 公网IP地址 +- 已配置MX记录的域名 +- 开放的25端口(SMTP)和5000端口(API) + +## 1. 服务器准备 + +### 1.1 安装基础软件包 + +```bash +# Ubuntu/Debian +sudo apt update +sudo apt install -y python3 python3-pip python3-venv git ufw + +# CentOS/RHEL +sudo yum update -y +sudo yum install -y python3 python3-pip git +``` + +### 1.2 配置防火墙 + +```bash +# Ubuntu/Debian (ufw) +sudo ufw allow 22/tcp # SSH +sudo ufw allow 25/tcp # SMTP +sudo ufw allow 5000/tcp # API +sudo ufw enable + +# CentOS/RHEL (firewalld) +sudo firewall-cmd --permanent --add-port=22/tcp +sudo firewall-cmd --permanent --add-port=25/tcp +sudo firewall-cmd --permanent --add-port=5000/tcp +sudo firewall-cmd --reload +``` + +## 2. 部署系统 + +### 2.1 获取代码 + +```bash +git clone /opt/email-system +cd /opt/email-system +``` + +### 2.2 创建虚拟环境并安装依赖 + +```bash +python3 -m venv venv +source venv/bin/activate +pip install --upgrade pip +pip install -r requirements.txt +``` + +### 2.3 配置系统 + +创建环境变量文件: + +```bash +cat > .env << EOF +FLASK_ENV=production +SECRET_KEY=$(python -c 'import secrets; print(secrets.token_hex(16))') +MAIL_DOMAINS=yourdomain.com,anotherdomain.com +DATABASE_URL=sqlite:///db/email_system.db +EOF +``` + +修改`local_settings.py`以适应生产环境: + +```bash +cat > local_settings.py << EOF +""" +生产环境配置文件 +""" +# SMTP服务器配置 +SMTP_HOST = '0.0.0.0' # 监听所有网络接口 +SMTP_PORT = 25 # 使用标准SMTP端口 + +# 日志配置 +LOG_LEVEL = 'INFO' +EOF +``` + +### 2.4 初始化数据库 + +```bash +mkdir -p db email_data logs +python -c "from app.models import init_db; init_db()" +``` + +## 3. 启动服务 + +### 3.1 直接启动(测试用) + +```bash +python run.py --host 0.0.0.0 --port 5000 --smtp-port 25 +``` + +### 3.2 使用Systemd服务(推荐用于生产) + +创建服务文件: + +```bash +sudo cat > /etc/systemd/system/email-system.service << EOF +[Unit] +Description=Email System +After=network.target + +[Service] +User=root +WorkingDirectory=/opt/email-system +ExecStart=/opt/email-system/venv/bin/python run.py --host 0.0.0.0 --port 5000 --smtp-port 25 +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +EOF +``` + +启动服务: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable email-system +sudo systemctl start email-system +``` + +## 4. 验证部署 + +### 4.1 检查服务状态 + +```bash +sudo systemctl status email-system +netstat -tulpn | grep -E ':(25|5000)' +curl http://localhost:5000/api/status +``` + +### 4.2 检查日志 + +```bash +tail -f logs/email_system.log +``` + +## 5. 故障排除 + +### 5.1 服务无法启动 + +- 检查日志文件: `cat logs/email_system.log` +- 确认端口未被占用: `netstat -tulpn | grep -E ':(25|5000)'` +- 检查Python依赖: `pip install -r requirements.txt` + +### 5.2 无法接收外部邮件 + +- 验证MX记录配置: `dig MX yourdomain.com` +- 检查25端口是否开放: `telnet your-server-ip 25` +- 确认SMTP服务已启动: `ps aux | grep smtp` +- 检查防火墙规则: `sudo ufw status` 或 `sudo firewall-cmd --list-all` + +### 5.3 常见ISP限制 + +许多ISP会阻止25端口的出站流量。如果遇到此问题: +- 联系ISP要求解除25端口限制 +- 使用备用端口(如587)并在防火墙中转发到25端口 +- 考虑使用云服务提供商(如AWS、GCP、阿里云等)的VM实例 + +## 6. 性能优化 + +### 6.1 数据库优化 + +对于大规模部署,考虑从SQLite迁移到PostgreSQL: + +```bash +# 安装PostgreSQL +sudo apt install -y postgresql postgresql-contrib +# 创建数据库和用户 +sudo -u postgres psql -c "CREATE USER email_system WITH PASSWORD 'your_password';" +sudo -u postgres psql -c "CREATE DATABASE email_system_db OWNER email_system;" +``` + +然后更新`.env`文件中的`DATABASE_URL`: + +``` +DATABASE_URL=postgresql://email_system:your_password@localhost/email_system_db +``` + +### 6.2 使用反向代理 + +对于生产环境,建议使用Nginx作为反向代理: + +```bash +# 安装Nginx +sudo apt install -y nginx + +# 配置Nginx +sudo cat > /etc/nginx/sites-available/email-system << EOF +server { + listen 80; + server_name api.yourdomain.com; + + location / { + proxy_pass http://localhost:5000; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + } +} +EOF + +sudo ln -s /etc/nginx/sites-available/email-system /etc/nginx/sites-enabled/ +sudo nginx -t +sudo systemctl restart nginx +``` + +## 7. 安全建议 + +- 启用HTTPS (使用Let's Encrypt) +- 限制API访问 (IP白名单、API密钥等) +- 定期备份数据库 +- 监控系统资源使用情况 +- 实现基本的邮件过滤功能 + +## 8. 备份与恢复 + +### 8.1 备份 + +```bash +# 创建备份脚本 +cat > backup.sh << EOF +#!/bin/bash +TIMESTAMP=\$(date +%Y%m%d_%H%M%S) +BACKUP_DIR=/opt/email-system/backups/\$TIMESTAMP + +mkdir -p \$BACKUP_DIR + +# 停止服务 +systemctl stop email-system + +# 备份数据库 +cp db/email_system.db \$BACKUP_DIR/ + +# 备份邮件数据 +cp -r email_data \$BACKUP_DIR/ + +# 备份配置 +cp .env \$BACKUP_DIR/ +cp local_settings.py \$BACKUP_DIR/ + +# 重启服务 +systemctl start email-system + +echo "备份完成: \$BACKUP_DIR" +EOF + +chmod +x backup.sh +``` + +### 8.2 恢复 + +```bash +# 恢复数据 +BACKUP_DIR=/opt/email-system/backups/20220101_120000 + +# 停止服务 +systemctl stop email-system + +# 恢复数据库 +cp $BACKUP_DIR/email_system.db db/ + +# 恢复邮件数据 +cp -r $BACKUP_DIR/email_data ./ + +# 恢复配置 +cp $BACKUP_DIR/.env ./ +cp $BACKUP_DIR/local_settings.py ./ + +# 重启服务 +systemctl start email-system +``` \ No newline at end of file diff --git a/DNS_CONFIGURATION.md b/DNS_CONFIGURATION.md new file mode 100644 index 0000000..e33b000 --- /dev/null +++ b/DNS_CONFIGURATION.md @@ -0,0 +1,181 @@ +# 邮箱系统DNS配置指南 + +为使邮箱系统能够接收来自外部的邮件,必须正确配置DNS记录。本文档详细说明了配置过程。 + +## 必要的DNS记录 + +### MX记录(必须) + +MX(Mail Exchange)记录指定接收域名邮件的服务器。您需要为每个要接收邮件的域名添加MX记录。 + +| 记录类型 | 主机记录 | 优先级 | 记录值 | +|---------|---------|-------|--------| +| MX | @ | 10 | mail.yourdomain.com | + +* `@`表示针对域名本身(如`yourdomain.com`) +* `优先级`数字越小表示优先级越高,如果有多个MX记录,邮件将首先尝试发送到优先级最高的服务器 +* `记录值`应指向您的邮件服务器的主机名 + +### A记录(必须) + +A记录将主机名映射到IPv4地址。您需要创建一个A记录,将MX记录中使用的主机名指向您的服务器IP。 + +| 记录类型 | 主机记录 | 记录值 | +|---------|---------|-------| +| A | mail | 203.0.113.1 | + +* `主机记录`应为MX记录中使用的主机名的前缀(如果MX记录值为`mail.yourdomain.com`,则主机记录为`mail`) +* `记录值`是您服务器的公网IP地址 + +### SPF记录(推荐) + +SPF(Sender Policy Framework)记录用于防止邮件欺诈,指定哪些服务器被授权发送来自您域名的邮件。 + +| 记录类型 | 主机记录 | 记录值 | +|---------|---------|-------| +| TXT | @ | v=spf1 ip4:203.0.113.1 -all | + +* `v=spf1`表示这是SPF记录的版本1 +* `ip4:203.0.113.1`授权您服务器的IP地址发送邮件 +* `-all`表示严格模式,任何不匹配的发件人都会被拒绝 + +### PTR记录(重要) + +PTR(反向DNS)记录用于将IP地址映射回主机名,许多邮件服务器会检查这个记录来验证发件人。 + +**注意:** PTR记录通常由您的ISP或云服务提供商配置,需要联系他们进行设置。 + +请求他们将您的IP地址(如`203.0.113.1`)的PTR记录设置为您的邮件服务器域名(如`mail.yourdomain.com`)。 + +## 配置步骤 + +### 1. 登录DNS管理面板 + +使用您的域名注册商提供的管理面板或DNS服务提供商的控制台登录。 + +常见的DNS管理面板包括: +- Cloudflare +- GoDaddy +- NameCheap +- 阿里云 +- 腾讯云 + +### 2. 添加MX记录 + +1. 找到DNS记录管理界面 +2. 选择添加新记录 +3. 记录类型选择"MX" +4. 主机记录填写"@" +5. 优先级填写"10" +6. 记录值填写指向您服务器的域名,如"mail.yourdomain.com" +7. TTL可以保持默认值,或设置为3600(1小时) +8. 保存记录 + +### 3. 添加A记录 + +1. 再次选择添加新记录 +2. 记录类型选择"A" +3. 主机记录填写"mail"(或您在MX记录中使用的子域名) +4. 记录值填写您服务器的公网IP地址 +5. TTL可以保持默认值,或设置为3600 +6. 保存记录 + +### 4. 添加SPF记录 + +1. 再次选择添加新记录 +2. 记录类型选择"TXT" +3. 主机记录填写"@" +4. 记录值填写`v=spf1 ip4:YOUR_SERVER_IP -all`,替换YOUR_SERVER_IP为您的服务器IP +5. TTL可以保持默认值,或设置为3600 +6. 保存记录 + +## 验证配置 + +### 1. 验证MX记录 + +使用dig命令查询MX记录: + +```bash +dig MX yourdomain.com +``` + +预期输出应包含您配置的MX记录: + +``` +yourdomain.com. 3600 IN MX 10 mail.yourdomain.com. +``` + +### 2. 验证A记录 + +```bash +dig A mail.yourdomain.com +``` + +预期输出应显示您服务器的IP: + +``` +mail.yourdomain.com. 3600 IN A 203.0.113.1 +``` + +### 3. 验证SPF记录 + +```bash +dig TXT yourdomain.com +``` + +预期输出应包含您的SPF记录: + +``` +yourdomain.com. 3600 IN TXT "v=spf1 ip4:203.0.113.1 -all" +``` + +### 4. 验证PTR记录 + +```bash +dig -x YOUR_SERVER_IP +``` + +预期输出应将您的IP解析为您的服务器域名: + +``` +1.113.0.203.in-addr.arpa. 3600 IN PTR mail.yourdomain.com. +``` + +## 故障排除 + +### 记录未生效 + +DNS更改可能需要24-48小时在全球范围内完全生效。如果刚刚更改了记录,可能需要等待一段时间才能看到效果。 + +可以使用不同的DNS查询工具检查传播状态: +- https://www.whatsmydns.net/ +- https://dnschecker.org/ + +### 邮件被退回 + +如果邮件被接收方拒绝,可能的原因包括: + +1. **没有正确的PTR记录**: 许多服务器(如Gmail)要求有匹配的PTR记录 +2. **SPF记录配置错误**: 检查SPF记录语法是否正确 +3. **服务器IP被列入黑名单**: 使用 https://mxtoolbox.com/blacklists.aspx 检查 + +### 验证综合邮件配置 + +使用MX Toolbox等工具进行综合检查: +https://mxtoolbox.com/SuperTool.aspx + +## 多域名配置 + +要为系统中的每个域名都接收邮件,需要为每个域名单独配置MX记录,但都指向相同的邮件服务器。例如: + +对于domain1.com: +``` +domain1.com. MX 10 mail.yourdomain.com. +``` + +对于domain2.com: +``` +domain2.com. MX 10 mail.yourdomain.com. +``` + +这样所有配置的域名收到的邮件都会被发送到同一个服务器进行处理。 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..27cf459 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,43 @@ +FROM python:3.9-alpine + +# 设置工作目录 +WORKDIR /app + +# 安装系统依赖 +RUN apk add --no-cache \ + gcc \ + musl-dev \ + python3-dev \ + libffi-dev \ + openssl-dev \ + bash + +# 复制依赖文件并安装 +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# 复制应用程序代码 +COPY . . + +# 创建必要的目录 +RUN mkdir -p logs email_data db + +# 设置环境变量 +ENV FLASK_ENV=production \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +# 设置非root用户 +RUN adduser -D emailuser +RUN chown -R emailuser:emailuser /app +USER root + +# 暴露端口 +EXPOSE 5000 25 + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:5000/api/status || exit 1 + +# 启动命令 +CMD ["sh", "-c", "python run.py --host 0.0.0.0 --port 5000 --smtp-port 25"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b2b247c --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# 邮件系统 + +这是一个基于Python的完整邮件系统解决方案,提供SMTP服务和API接口管理。 + +## 功能特点 + +- SMTP服务器接收和发送邮件 +- 邮箱管理API接口 +- 配置灵活,支持多域名 +- 支持Docker部署 +- 包含监控和性能测试工具 + +## 目录结构 + +- `app/`: 应用主代码 + - `api/`: API接口实现 + - `models/`: 数据模型 + - `services/`: 业务逻辑服务 + - `templates/`: 模板文件 +- `config.py`: 配置文件 +- `run.py`: 主程序入口 +- `monitor_email_system.py`: 监控工具 +- `performance_test.py`: 性能测试工具 +- `deploy_production.sh`: 生产环境部署脚本 + +## 部署方法 + +详细部署步骤请参考 `DEPLOYMENT_GUIDE.md`。 + +## 许可证 + +此项目采用MIT许可证。 \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..6197538 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,74 @@ +import os +import logging +from flask import Flask +from flask_cors import CORS + +# 修改相对导入为绝对导入 +import sys +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from config import active_config + + +def setup_logging(app): + """设置日志""" + log_level = getattr(logging, active_config.LOG_LEVEL.upper(), logging.INFO) + + # 确保日志目录存在 + log_dir = os.path.dirname(active_config.LOG_FILE) + if not os.path.exists(log_dir): + os.makedirs(log_dir) + + # 配置日志 + logging.basicConfig( + level=log_level, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(active_config.LOG_FILE), + logging.StreamHandler() + ] + ) + + app.logger.setLevel(log_level) + return app + + +def create_app(config=None): + """创建并配置Flask应用""" + app = Flask(__name__) + + # 加载配置 + app.config.from_object(active_config) + + # 如果提供了自定义配置,加载它 + if config: + app.config.from_object(config) + + # 允许跨域请求 + CORS(app) + + # 设置日志 + app = setup_logging(app) + + # 确保存储邮件的目录存在 + os.makedirs(active_config.MAIL_STORAGE_PATH, exist_ok=True) + + # 初始化数据库 + from .models import init_db + init_db() + + # 注册蓝图 + from .api import api_bp + app.register_blueprint(api_bp) + + # 首页路由 + @app.route("/") + def index(): + return { + "name": "Email System", + "version": "1.0.0", + "status": "running" + } + + app.logger.info('应用初始化完成') + + return app \ No newline at end of file diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 0000000..8da90d4 --- /dev/null +++ b/app/api/__init__.py @@ -0,0 +1,23 @@ +# API模块初始化文件 +from flask import Blueprint +import logging + +# 创建API蓝图 +api_bp = Blueprint('api', __name__, url_prefix='/api') + +# 注册默认路由 +@api_bp.route('/') +def index(): + return { + 'name': 'Email System API', + 'version': '1.0.0', + 'status': 'running' + } + +# 导入并合并所有API路由 +# 为避免可能的文件读取问题,改为从routes.py模块中导入所有路由定义 +try: + from .routes import * +except Exception as e: + logging.error(f"导入API路由时出错: {str(e)}") + raise \ No newline at end of file diff --git a/app/api/domain_routes.py b/app/api/domain_routes.py new file mode 100644 index 0000000..a05687c Binary files /dev/null and b/app/api/domain_routes.py differ diff --git a/app/api/email_routes.py b/app/api/email_routes.py new file mode 100644 index 0000000..91682e1 --- /dev/null +++ b/app/api/email_routes.py @@ -0,0 +1,198 @@ +from flask import request, jsonify, current_app, send_file +from io import BytesIO +import time + +from . import api_bp +from ..models import get_session, Email, Mailbox + +# 获取邮箱的所有邮件 +@api_bp.route('/mailboxes//emails', methods=['GET']) +def get_mailbox_emails(mailbox_id): + """获取指定邮箱的所有邮件""" + try: + page = int(request.args.get('page', 1)) + limit = int(request.args.get('limit', 50)) + unread_only = request.args.get('unread_only', 'false').lower() == 'true' + offset = (page - 1) * limit + + db = get_session() + try: + # 检查邮箱是否存在 + mailbox = db.query(Mailbox).filter_by(id=mailbox_id).first() + if not mailbox: + return jsonify({'error': '邮箱不存在'}), 404 + + # 查询邮件 + query = db.query(Email).filter(Email.mailbox_id == mailbox_id) + + if unread_only: + query = query.filter(Email.read == False) + + # 获取总数 + total = query.count() + + # 分页获取邮件 + emails = query.order_by(Email.received_at.desc()) \ + .limit(limit) \ + .offset(offset) \ + .all() + + # 返回结果 + result = { + 'total': total, + 'page': page, + 'limit': limit, + 'emails': [email.to_dict() for email in emails] + } + + return jsonify(result), 200 + finally: + db.close() + except Exception as e: + current_app.logger.error(f"获取邮件列表出错: {str(e)}") + return jsonify({'error': '获取邮件列表失败', 'details': str(e)}), 500 + +# 获取特定邮件详情 +@api_bp.route('/emails/', methods=['GET']) +def get_email(email_id): + """获取特定邮件的详细信息""" + try: + mark_as_read = request.args.get('mark_as_read', 'true').lower() == 'true' + + db = get_session() + try: + email = db.query(Email).filter_by(id=email_id).first() + + if not email: + return jsonify({'error': '邮件不存在'}), 404 + + # 标记为已读 + if mark_as_read and not email.read: + email.read = True + db.commit() + + # 构建详细响应 + result = email.to_dict() + result['body_text'] = email.body_text + result['body_html'] = email.body_html + + # 获取附件信息 + attachments = [] + for attachment in email.attachments: + attachments.append({ + 'id': attachment.id, + 'filename': attachment.filename, + 'content_type': attachment.content_type, + 'size': attachment.size + }) + result['attachments'] = attachments + + return jsonify(result), 200 + finally: + db.close() + except Exception as e: + current_app.logger.error(f"获取邮件详情出错: {str(e)}") + return jsonify({'error': '获取邮件详情失败', 'details': str(e)}), 500 + +# 删除邮件 +@api_bp.route('/emails/', methods=['DELETE']) +def delete_email(email_id): + """删除特定邮件""" + try: + db = get_session() + try: + email = db.query(Email).filter_by(id=email_id).first() + + if not email: + return jsonify({'error': '邮件不存在'}), 404 + + db.delete(email) + db.commit() + + return jsonify({'message': '邮件已删除'}), 200 + except Exception as e: + db.rollback() + raise + finally: + db.close() + except Exception as e: + current_app.logger.error(f"删除邮件出错: {str(e)}") + return jsonify({'error': '删除邮件失败', 'details': str(e)}), 500 + +# 下载附件 +@api_bp.route('/attachments/', methods=['GET']) +def download_attachment(attachment_id): + """下载特定的附件""" + try: + from ..models import Attachment + + db = get_session() + try: + attachment = db.query(Attachment).filter_by(id=attachment_id).first() + + if not attachment: + return jsonify({'error': '附件不存在'}), 404 + + # 获取附件内容 + content = attachment.get_content() + if not content: + return jsonify({'error': '附件内容不可用'}), 404 + + # 创建内存文件对象 + file_obj = BytesIO(content) + + # 返回文件下载响应 + return send_file( + file_obj, + mimetype=attachment.content_type, + as_attachment=True, + download_name=attachment.filename + ) + finally: + db.close() + except Exception as e: + current_app.logger.error(f"下载附件出错: {str(e)}") + return jsonify({'error': '下载附件失败', 'details': str(e)}), 500 + +# 获取最新邮件 (轮询API) +@api_bp.route('/mailboxes//poll', methods=['GET']) +def poll_new_emails(mailbox_id): + """轮询指定邮箱的新邮件""" + try: + # 获取上次检查时间 + last_check = request.args.get('last_check') + if last_check: + try: + last_check_time = float(last_check) + except ValueError: + return jsonify({'error': '无效的last_check参数'}), 400 + else: + last_check_time = time.time() - 300 # 默认检查最近5分钟 + + db = get_session() + try: + # 检查邮箱是否存在 + mailbox = db.query(Mailbox).filter_by(id=mailbox_id).first() + if not mailbox: + return jsonify({'error': '邮箱不存在'}), 404 + + # 查询新邮件 + new_emails = db.query(Email).filter( + Email.mailbox_id == mailbox_id, + Email.received_at >= time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(last_check_time)) + ).order_by(Email.received_at.desc()).all() + + # 返回结果 + result = { + 'mailbox_id': mailbox_id, + 'count': len(new_emails), + 'emails': [email.to_dict() for email in new_emails], + 'timestamp': time.time() + } + + return jsonify(result), 200 + finally: + db.close() + except Exception as e: + current_app.logger.error(f"轮询新邮件出错: {str(e)}") + return jsonify({'error': '轮询新邮件失败', 'details': str(e)}), 500 \ No newline at end of file diff --git a/app/api/mailbox_routes.py b/app/api/mailbox_routes.py new file mode 100644 index 0000000..84b416d --- /dev/null +++ b/app/api/mailbox_routes.py @@ -0,0 +1,206 @@ +from flask import request, jsonify, current_app +from sqlalchemy.exc import IntegrityError +import random +import string + +from . import api_bp +from ..models import get_session, Domain, Mailbox + +# 获取所有邮箱 +@api_bp.route('/mailboxes', methods=['GET']) +def get_mailboxes(): + """获取所有邮箱列表""" + try: + page = int(request.args.get('page', 1)) + limit = int(request.args.get('limit', 50)) + offset = (page - 1) * limit + + db = get_session() + try: + # 查询总数 + total = db.query(Mailbox).count() + + # 获取分页数据 + mailboxes = db.query(Mailbox).order_by(Mailbox.created_at.desc()) \ + .limit(limit) \ + .offset(offset) \ + .all() + + # 转换为字典列表 + result = { + 'total': total, + 'page': page, + 'limit': limit, + 'mailboxes': [mailbox.to_dict() for mailbox in mailboxes] + } + + return jsonify(result), 200 + finally: + db.close() + except Exception as e: + current_app.logger.error(f"获取邮箱列表出错: {str(e)}") + return jsonify({'error': '获取邮箱列表失败', 'details': str(e)}), 500 + +# 创建邮箱 +@api_bp.route('/mailboxes', methods=['POST']) +def create_mailbox(): + """创建新邮箱""" + try: + data = request.json + + # 验证必要参数 + if not data or 'domain_id' not in data: + return jsonify({'error': '缺少必要参数'}), 400 + + db = get_session() + try: + # 查询域名是否存在 + domain = db.query(Domain).filter_by(id=data['domain_id'], active=True).first() + if not domain: + return jsonify({'error': '指定的域名不存在或未激活'}), 404 + + # 生成或使用给定地址 + if 'address' not in data or not data['address']: + # 生成随机地址 + address = ''.join(random.choices(string.ascii_lowercase + string.digits, k=10)) + else: + address = data['address'] + + # 创建邮箱 + mailbox = Mailbox( + address=address, + domain_id=domain.id, + description=data.get('description', ''), + active=True + ) + + db.add(mailbox) + db.commit() + + return jsonify({ + 'message': '邮箱创建成功', + 'mailbox': mailbox.to_dict() + }), 201 + except IntegrityError: + db.rollback() + return jsonify({'error': '邮箱地址已存在'}), 409 + except Exception as e: + db.rollback() + raise + finally: + db.close() + except Exception as e: + current_app.logger.error(f"创建邮箱出错: {str(e)}") + return jsonify({'error': '创建邮箱失败', 'details': str(e)}), 500 + +# 批量创建邮箱 +@api_bp.route('/mailboxes/batch', methods=['POST']) +def batch_create_mailboxes(): + """批量创建邮箱""" + try: + data = request.json + + # 验证必要参数 + if not data or 'domain_id' not in data or 'count' not in data: + return jsonify({'error': '缺少必要参数'}), 400 + + domain_id = data['domain_id'] + count = min(int(data['count']), 100) # 限制最大数量为100 + prefix = data.get('prefix', '') + + db = get_session() + try: + # 查询域名是否存在 + domain = db.query(Domain).filter_by(id=domain_id, active=True).first() + if not domain: + return jsonify({'error': '指定的域名不存在或未激活'}), 404 + + created_mailboxes = [] + + # 批量创建 + for _ in range(count): + # 生成随机地址 + if prefix: + address = f"{prefix}{random.randint(1000, 9999)}" + else: + address = ''.join(random.choices(string.ascii_lowercase + string.digits, k=10)) + + # 尝试创建,如果地址已存在则重试 + retries = 0 + while retries < 3: # 最多尝试3次 + try: + mailbox = Mailbox( + address=address, + domain_id=domain.id, + active=True + ) + + db.add(mailbox) + db.flush() # 验证但不提交 + created_mailboxes.append(mailbox) + break + except IntegrityError: + db.rollback() + # 地址已存在,重新生成 + address = ''.join(random.choices(string.ascii_lowercase + string.digits, k=10)) + retries += 1 + + # 提交所有更改 + db.commit() + + return jsonify({ + 'message': f'成功创建 {len(created_mailboxes)} 个邮箱', + 'mailboxes': [mailbox.to_dict() for mailbox in created_mailboxes] + }), 201 + except Exception as e: + db.rollback() + raise + finally: + db.close() + except Exception as e: + current_app.logger.error(f"批量创建邮箱出错: {str(e)}") + return jsonify({'error': '批量创建邮箱失败', 'details': str(e)}), 500 + +# 获取特定邮箱 +@api_bp.route('/mailboxes/', methods=['GET']) +def get_mailbox(mailbox_id): + """获取指定ID的邮箱信息""" + try: + db = get_session() + try: + mailbox = db.query(Mailbox).filter_by(id=mailbox_id).first() + + if not mailbox: + return jsonify({'error': '邮箱不存在'}), 404 + + return jsonify(mailbox.to_dict()), 200 + finally: + db.close() + except Exception as e: + current_app.logger.error(f"获取邮箱详情出错: {str(e)}") + return jsonify({'error': '获取邮箱详情失败', 'details': str(e)}), 500 + +# 删除邮箱 +@api_bp.route('/mailboxes/', methods=['DELETE']) +def delete_mailbox(mailbox_id): + """删除指定ID的邮箱""" + try: + db = get_session() + try: + mailbox = db.query(Mailbox).filter_by(id=mailbox_id).first() + + if not mailbox: + return jsonify({'error': '邮箱不存在'}), 404 + + db.delete(mailbox) + db.commit() + + return jsonify({'message': '邮箱已删除'}), 200 + except Exception as e: + db.rollback() + raise + finally: + db.close() + except Exception as e: + current_app.logger.error(f"删除邮箱出错: {str(e)}") + return jsonify({'error': '删除邮箱失败', 'details': str(e)}), 500 \ No newline at end of file diff --git a/app/api/routes.py b/app/api/routes.py new file mode 100644 index 0000000..a0eabab --- /dev/null +++ b/app/api/routes.py @@ -0,0 +1,341 @@ +from flask import Blueprint, request, jsonify, current_app +import json +from datetime import datetime, timedelta +import os +import time +import psutil +import sys +import platform +from sqlalchemy import func +from ..models import get_session, Domain, Mailbox, Email +from ..services import get_smtp_server, get_email_processor + +api_bp = Blueprint('api', __name__, url_prefix='/api') + + +@api_bp.route('/domains', methods=['GET']) +def get_domains(): + """获取所有可用域名""" + db = get_session() + try: + domains = db.query(Domain).filter_by(active=True).all() + return jsonify({ + 'success': True, + 'domains': [domain.to_dict() for domain in domains] + }) + except Exception as e: + current_app.logger.exception(f"获取域名失败: {str(e)}") + return jsonify({'success': False, 'error': '获取域名失败'}), 500 + finally: + db.close() + + +@api_bp.route('/domains', methods=['POST']) +def create_domain(): + """创建新域名""" + data = request.json + if not data or 'name' not in data: + return jsonify({'success': False, 'error': '缺少必要字段'}), 400 + + db = get_session() + try: + # 检查域名是否已存在 + domain_exists = db.query(Domain).filter_by(name=data['name']).first() + if domain_exists: + return jsonify({'success': False, 'error': '域名已存在'}), 400 + + # 创建新域名 + domain = Domain( + name=data['name'], + description=data.get('description', ''), + active=data.get('active', True) + ) + db.add(domain) + db.commit() + + return jsonify({ + 'success': True, + 'message': '域名创建成功', + 'domain': domain.to_dict() + }) + except Exception as e: + db.rollback() + current_app.logger.exception(f"创建域名失败: {str(e)}") + return jsonify({'success': False, 'error': '创建域名失败'}), 500 + finally: + db.close() + + +@api_bp.route('/mailboxes', methods=['GET']) +def get_mailboxes(): + """获取所有邮箱""" + db = get_session() + try: + mailboxes = db.query(Mailbox).all() + return jsonify({ + 'success': True, + 'mailboxes': [mailbox.to_dict() for mailbox in mailboxes] + }) + except Exception as e: + current_app.logger.exception(f"获取邮箱失败: {str(e)}") + return jsonify({'success': False, 'error': '获取邮箱失败'}), 500 + finally: + db.close() + + +@api_bp.route('/mailboxes', methods=['POST']) +def create_mailbox(): + """创建新邮箱""" + data = request.json + if not data or 'address' not in data or 'domain_id' not in data: + return jsonify({'success': False, 'error': '缺少必要字段'}), 400 + + db = get_session() + try: + # 检查域名是否存在 + domain = db.query(Domain).filter_by(id=data['domain_id'], active=True).first() + if not domain: + return jsonify({'success': False, 'error': '域名不存在或未激活'}), 400 + + # 检查邮箱是否已存在 + mailbox_exists = db.query(Mailbox).filter_by( + address=data['address'], domain_id=data['domain_id']).first() + if mailbox_exists: + return jsonify({'success': False, 'error': '邮箱已存在'}), 400 + + # 创建新邮箱 + mailbox = Mailbox( + address=data['address'], + domain_id=data['domain_id'], + description=data.get('description', ''), + active=data.get('active', True) + ) + db.add(mailbox) + db.commit() + + return jsonify({ + 'success': True, + 'message': '邮箱创建成功', + 'mailbox': mailbox.to_dict() + }) + except Exception as e: + db.rollback() + current_app.logger.exception(f"创建邮箱失败: {str(e)}") + return jsonify({'success': False, 'error': '创建邮箱失败'}), 500 + finally: + db.close() + + +@api_bp.route('/mailboxes/batch', methods=['POST']) +def batch_create_mailboxes(): + """批量创建邮箱""" + data = request.json + if not data or 'domain_id' not in data or 'usernames' not in data or not isinstance(data['usernames'], list): + return jsonify({'success': False, 'error': '缺少必要字段或格式不正确'}), 400 + + domain_id = data['domain_id'] + usernames = data['usernames'] + description = data.get('description', '') + + db = get_session() + try: + # 检查域名是否存在 + domain = db.query(Domain).filter_by(id=domain_id, active=True).first() + if not domain: + return jsonify({'success': False, 'error': '域名不存在或未激活'}), 400 + + created_mailboxes = [] + existed_mailboxes = [] + + for username in usernames: + # 检查邮箱是否已存在 + mailbox_exists = db.query(Mailbox).filter_by( + username=username, domain_id=domain_id).first() + if mailbox_exists: + existed_mailboxes.append(username) + continue + + # 创建新邮箱 + mailbox = Mailbox( + username=username, + domain_id=domain_id, + description=description, + active=True + ) + db.add(mailbox) + created_mailboxes.append(username) + + db.commit() + + return jsonify({ + 'success': True, + 'message': f'成功创建 {len(created_mailboxes)} 个邮箱,{len(existed_mailboxes)} 个已存在', + 'created': created_mailboxes, + 'existed': existed_mailboxes + }) + except Exception as e: + db.rollback() + current_app.logger.exception(f"批量创建邮箱失败: {str(e)}") + return jsonify({'success': False, 'error': '批量创建邮箱失败'}), 500 + finally: + db.close() + + +@api_bp.route('/mailboxes/', methods=['GET']) +def get_mailbox(mailbox_id): + """获取特定邮箱的信息""" + db = get_session() + try: + mailbox = db.query(Mailbox).filter_by(id=mailbox_id).first() + if not mailbox: + return jsonify({'success': False, 'error': '邮箱不存在'}), 404 + + # 更新最后访问时间 + mailbox.last_accessed = datetime.utcnow() + db.commit() + + return jsonify({ + 'success': True, + 'mailbox': mailbox.to_dict() + }) + except Exception as e: + current_app.logger.exception(f"获取邮箱信息失败: {str(e)}") + return jsonify({'success': False, 'error': '获取邮箱信息失败'}), 500 + finally: + db.close() + + +@api_bp.route('/mailboxes//emails', methods=['GET']) +def get_emails(mailbox_id): + """获取邮箱中的所有邮件""" + db = get_session() + try: + mailbox = db.query(Mailbox).filter_by(id=mailbox_id).first() + if not mailbox: + return jsonify({'success': False, 'error': '邮箱不存在'}), 404 + + # 更新最后访问时间 + mailbox.last_accessed = datetime.utcnow() + db.commit() + + emails = db.query(Email).filter_by(mailbox_id=mailbox_id).order_by(Email.received_at.desc()).all() + + return jsonify({ + 'success': True, + 'emails': [email.to_dict() for email in emails] + }) + except Exception as e: + current_app.logger.exception(f"获取邮件失败: {str(e)}") + return jsonify({'success': False, 'error': '获取邮件失败'}), 500 + finally: + db.close() + + +@api_bp.route('/emails/', methods=['GET']) +def get_email(email_id): + """获取特定邮件的详细内容""" + db = get_session() + try: + email = db.query(Email).filter_by(id=email_id).first() + if not email: + return jsonify({'success': False, 'error': '邮件不存在'}), 404 + + # 标记为已读 + if not email.read: + email.read = True + db.commit() + + return jsonify({ + 'success': True, + 'email': email.to_dict() + }) + except Exception as e: + current_app.logger.exception(f"获取邮件详情失败: {str(e)}") + return jsonify({'success': False, 'error': '获取邮件详情失败'}), 500 + finally: + db.close() + + +@api_bp.route('/emails//verification', methods=['GET']) +def get_verification_info(email_id): + """获取邮件中的验证信息(链接和验证码)""" + db = get_session() + try: + email = db.query(Email).filter_by(id=email_id).first() + if not email: + return jsonify({'success': False, 'error': '邮件不存在'}), 404 + + verification_links = json.loads(email.verification_links) if email.verification_links else [] + verification_codes = json.loads(email.verification_codes) if email.verification_codes else [] + + return jsonify({ + 'success': True, + 'email_id': email_id, + 'verification_links': verification_links, + 'verification_codes': verification_codes + }) + except Exception as e: + current_app.logger.exception(f"获取验证信息失败: {str(e)}") + return jsonify({'success': False, 'error': '获取验证信息失败'}), 500 + finally: + db.close() + + +@api_bp.route('/status', methods=['GET']) +def system_status(): + """获取系统状态""" + session = get_session() + + # 获取基本统计信息 + domain_count = session.query(func.count(Domain.id)).scalar() + mailbox_count = session.query(func.count(Mailbox.id)).scalar() + email_count = session.query(func.count(Email.id)).scalar() + + # 获取最近24小时的邮件数量 + recent_emails = session.query(func.count(Email.id)).filter( + Email.received_at > datetime.now() - timedelta(hours=24) + ).scalar() + + # 获取系统资源信息 + cpu_percent = psutil.cpu_percent(interval=0.5) + memory = psutil.virtual_memory() + disk = psutil.disk_usage('/') + + # 获取服务状态 + smtp_server = get_smtp_server() + email_processor = get_email_processor() + + smtp_status = "running" if smtp_server and smtp_server.controller else "stopped" + processor_status = "running" if email_processor and email_processor.is_running else "stopped" + + # 构建响应 + status = { + "system": { + "uptime": round(time.time() - psutil.boot_time()), + "time": datetime.now().isoformat(), + "platform": platform.platform(), + "python_version": sys.version + }, + "resources": { + "cpu_percent": cpu_percent, + "memory_percent": memory.percent, + "memory_used": memory.used, + "memory_total": memory.total, + "disk_percent": disk.percent, + "disk_used": disk.used, + "disk_total": disk.total + }, + "application": { + "domain_count": domain_count, + "mailbox_count": mailbox_count, + "email_count": email_count, + "recent_emails_24h": recent_emails, + "storage_path": os.path.abspath("email_data"), + "services": { + "smtp_server": smtp_status, + "email_processor": processor_status + } + } + } + + return jsonify(status) \ No newline at end of file diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..6d8980e --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1,46 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker, scoped_session +import os +import sys + +# 修改相对导入为绝对导入 +sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +from config import active_config + +# 创建数据库引擎 +engine = create_engine(active_config.SQLALCHEMY_DATABASE_URI) + +# 创建会话工厂 +session_factory = sessionmaker(bind=engine) +Session = scoped_session(session_factory) + +# 创建模型基类 +Base = declarative_base() + +# 获取数据库会话 +def get_session(): + """获取数据库会话""" + return Session() + +# 初始化数据库 +def init_db(): + """初始化数据库,创建所有表""" + # 导入所有模型以确保它们被注册 + from .domain import Domain + from .mailbox import Mailbox + from .email import Email + from .attachment import Attachment + + # 创建表 + Base.metadata.create_all(engine) + + return get_session() + +# 导出模型类 +from .domain import Domain +from .mailbox import Mailbox +from .email import Email +from .attachment import Attachment + +__all__ = ['Base', 'get_session', 'init_db', 'Domain', 'Mailbox', 'Email', 'Attachment'] \ No newline at end of file diff --git a/app/models/attachment.py b/app/models/attachment.py new file mode 100644 index 0000000..9c5cf89 --- /dev/null +++ b/app/models/attachment.py @@ -0,0 +1,71 @@ +from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, LargeBinary +from sqlalchemy.orm import relationship +from datetime import datetime +import os + +from . import Base + +class Attachment(Base): + """附件模型""" + __tablename__ = 'attachments' + + id = Column(Integer, primary_key=True) + email_id = Column(Integer, ForeignKey('emails.id'), nullable=False, index=True) + filename = Column(String(255), nullable=False) + content_type = Column(String(100), nullable=True) + size = Column(Integer, nullable=False, default=0) + storage_path = Column(String(500), nullable=True) # 用于文件系统存储 + content = Column(LargeBinary, nullable=True) # 用于小型附件的直接存储 + created_at = Column(DateTime, default=datetime.utcnow) + + # 关联关系 + email = relationship("Email", back_populates="attachments") + + @property + def is_stored_in_fs(self): + """判断附件是否存储在文件系统中""" + return bool(self.storage_path and not self.content) + + def save_to_filesystem(self, content, base_path): + """将附件保存到文件系统""" + # 确保目录存在 + os.makedirs(base_path, exist_ok=True) + + # 创建文件路径 + file_path = os.path.join( + base_path, + f"{self.email_id}_{self.id}_{self.filename}" + ) + + # 写入文件 + with open(file_path, 'wb') as f: + f.write(content) + + # 更新对象属性 + self.storage_path = file_path + self.size = len(content) + self.content = None # 清空内存中的内容 + + return file_path + + def get_content(self, attachments_dir=None): + """获取附件内容,无论是从数据库还是文件系统""" + if self.content: + return self.content + + if self.storage_path and os.path.exists(self.storage_path): + with open(self.storage_path, 'rb') as f: + return f.read() + + return None + + def to_dict(self): + """转换为字典,用于API响应""" + return { + "id": self.id, + "email_id": self.email_id, + "filename": self.filename, + "content_type": self.content_type, + "size": self.size, + "created_at": self.created_at.isoformat() if self.created_at else None + } \ No newline at end of file diff --git a/app/models/domain.py b/app/models/domain.py new file mode 100644 index 0000000..b962603 --- /dev/null +++ b/app/models/domain.py @@ -0,0 +1,35 @@ +from sqlalchemy import Column, Integer, String, Boolean, DateTime +from sqlalchemy.orm import relationship +from datetime import datetime + +from . import Base + + +class Domain(Base): + """邮件域名模型""" + __tablename__ = 'domains' + + id = Column(Integer, primary_key=True) + name = Column(String(255), unique=True, nullable=False, index=True) + description = Column(String(500), nullable=True) + active = Column(Boolean, default=True) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + # 关系 + mailboxes = relationship("Mailbox", back_populates="domain", cascade="all, delete-orphan") + + def __repr__(self): + return f"" + + def to_dict(self): + """转换为字典,用于API响应""" + return { + "id": self.id, + "name": self.name, + "description": self.description, + "active": self.active, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None, + "mailbox_count": len(self.mailboxes) if self.mailboxes else 0 + } \ No newline at end of file diff --git a/app/models/email.py b/app/models/email.py new file mode 100644 index 0000000..163a566 --- /dev/null +++ b/app/models/email.py @@ -0,0 +1,98 @@ +import os +import json +from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Boolean, JSON +from sqlalchemy.orm import relationship +from datetime import datetime +import re +import sys + +from . import Base +import config +active_config = config.active_config + + +class Email(Base): + """电子邮件模型""" + __tablename__ = 'emails' + + id = Column(Integer, primary_key=True) + mailbox_id = Column(Integer, ForeignKey('mailboxes.id'), nullable=False, index=True) + sender = Column(String(255), nullable=False) + recipients = Column(String(1000), nullable=False) + subject = Column(String(500), nullable=True) + body_text = Column(Text, nullable=True) + body_html = Column(Text, nullable=True) + received_at = Column(DateTime, default=datetime.utcnow) + read = Column(Boolean, default=False) + headers = Column(JSON, nullable=True) + + # 提取的验证码和链接 + verification_code = Column(String(100), nullable=True) + verification_link = Column(String(1000), nullable=True) + + # 关联关系 + mailbox = relationship("Mailbox", back_populates="emails") + attachments = relationship("Attachment", back_populates="email", cascade="all, delete-orphan") + + def save_raw_email(self, raw_content): + """保存原始邮件内容到文件""" + storage_path = active_config.MAIL_STORAGE_PATH + mailbox_dir = os.path.join(storage_path, str(self.mailbox_id)) + os.makedirs(mailbox_dir, exist_ok=True) + + # 保存原始邮件内容 + file_path = os.path.join(mailbox_dir, f"{self.id}.eml") + with open(file_path, 'wb') as f: + f.write(raw_content) + + def extract_verification_data(self): + """ + 尝试从邮件内容中提取验证码和验证链接 + 这个方法会在邮件保存时自动调用 + """ + # 合并文本和HTML内容用于搜索 + content = f"{self.subject} {self.body_text or ''}" + + # 提取可能的验证码(4-8位数字或字母组合) + code_patterns = [ + r'\b[A-Z0-9]{4,8}\b', # 大写字母和数字 + r'验证码[::]\s*([A-Z0-9]{4,8})', # 中文格式 + r'验证码是[::]\s*([A-Z0-9]{4,8})', # 中文格式2 + r'code[::]\s*([A-Z0-9]{4,8})', # 英文格式 + ] + + for pattern in code_patterns: + matches = re.findall(pattern, content, re.IGNORECASE) + if matches: + self.verification_code = matches[0] + break + + # 提取验证链接 + link_patterns = [ + r'https?://\S+(?:verify|confirm|activate)\S+', + r'https?://\S+(?:token|auth|account)\S+', + ] + + for pattern in link_patterns: + matches = re.findall(pattern, content, re.IGNORECASE) + if matches: + self.verification_link = matches[0] + break + + def __repr__(self): + return f"" + + def to_dict(self): + """转换为字典,用于API响应""" + return { + "id": self.id, + "mailbox_id": self.mailbox_id, + "sender": self.sender, + "recipients": self.recipients, + "subject": self.subject, + "received_at": self.received_at.isoformat() if self.received_at else None, + "read": self.read, + "verification_code": self.verification_code, + "verification_link": self.verification_link, + "has_attachments": len(self.attachments) > 0 if self.attachments else False + } \ No newline at end of file diff --git a/app/models/mailbox.py b/app/models/mailbox.py new file mode 100644 index 0000000..9d577c1 --- /dev/null +++ b/app/models/mailbox.py @@ -0,0 +1,50 @@ +from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey +from sqlalchemy.orm import relationship +from datetime import datetime +import secrets + +from . import Base + + +class Mailbox(Base): + """邮箱模型""" + __tablename__ = 'mailboxes' + + id = Column(Integer, primary_key=True) + address = Column(String(255), unique=True, nullable=False, index=True) + domain_id = Column(Integer, ForeignKey('domains.id'), nullable=False) + password_hash = Column(String(255), nullable=True) + description = Column(String(500), nullable=True) + active = Column(Boolean, default=True) + api_key = Column(String(64), unique=True, default=lambda: secrets.token_hex(16)) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + last_accessed = Column(DateTime, nullable=True) + + # 关系 + domain = relationship("Domain", back_populates="mailboxes") + emails = relationship("Email", back_populates="mailbox", cascade="all, delete-orphan") + + @property + def full_address(self): + """获取完整邮箱地址 (包含域名)""" + return f"{self.address}@{self.domain.name}" + + def __repr__(self): + return f"" + + def to_dict(self): + """转换为字典,用于API响应""" + return { + "id": self.id, + "address": self.address, + "domain_id": self.domain_id, + "domain_name": self.domain.name if self.domain else None, + "full_address": self.full_address, + "description": self.description, + "active": self.active, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None, + "last_accessed": self.last_accessed.isoformat() if self.last_accessed else None, + "email_count": len(self.emails) if self.emails else 0 + } \ No newline at end of file diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000..91004a1 --- /dev/null +++ b/app/services/__init__.py @@ -0,0 +1,44 @@ +# 服务层初始化文件 +# 这里将导入所有服务模块以便于统一调用 + +from .smtp_server import SMTPServer +from .email_processor import EmailProcessor +from .mail_store import MailStore + +# 全局服务实例 +_smtp_server = None +_email_processor = None +_mail_store = None + +def register_smtp_server(instance): + """注册SMTP服务器实例""" + global _smtp_server + _smtp_server = instance + +def register_email_processor(instance): + """注册邮件处理器实例""" + global _email_processor + _email_processor = instance + +def register_mail_store(instance): + """注册邮件存储实例""" + global _mail_store + _mail_store = instance + +def get_smtp_server(): + """获取SMTP服务器实例""" + return _smtp_server + +def get_email_processor(): + """获取邮件处理器实例""" + return _email_processor + +def get_mail_store(): + """获取邮件存储实例""" + return _mail_store + +__all__ = [ + 'SMTPServer', 'EmailProcessor', 'MailStore', + 'register_smtp_server', 'register_email_processor', 'register_mail_store', + 'get_smtp_server', 'get_email_processor', 'get_mail_store' +] \ No newline at end of file diff --git a/app/services/email_processor.py b/app/services/email_processor.py new file mode 100644 index 0000000..de80cca --- /dev/null +++ b/app/services/email_processor.py @@ -0,0 +1,123 @@ +import logging +import re +import threading +import time +from queue import Queue + +logger = logging.getLogger(__name__) + +class EmailProcessor: + """邮件处理器,负责处理邮件并提取验证信息""" + + def __init__(self, mail_store): + """ + 初始化邮件处理器 + + 参数: + mail_store: 邮件存储服务实例 + """ + self.mail_store = mail_store + self.processing_queue = Queue() + self.is_running = False + self.worker_thread = None + + def start(self): + """启动邮件处理器""" + if self.is_running: + logger.warning("邮件处理器已在运行") + return False + + self.is_running = True + self.worker_thread = threading.Thread( + target=self._processing_worker, + daemon=True + ) + self.worker_thread.start() + logger.info("邮件处理器已启动") + return True + + def stop(self): + """停止邮件处理器""" + if not self.is_running: + logger.warning("邮件处理器未在运行") + return False + + self.is_running = False + if self.worker_thread: + self.worker_thread.join(timeout=5.0) + self.worker_thread = None + + logger.info("邮件处理器已停止") + return True + + def queue_email_for_processing(self, email_id): + """将邮件添加到处理队列""" + self.processing_queue.put(email_id) + return True + + def _processing_worker(self): + """处理队列中的邮件的工作线程""" + while self.is_running: + try: + # 获取队列中的邮件,最多等待1秒 + try: + email_id = self.processing_queue.get(timeout=1.0) + except: + continue + + # 处理邮件 + self._process_email(email_id) + + # 标记任务完成 + self.processing_queue.task_done() + + except Exception as e: + logger.error(f"处理邮件时出错: {str(e)}") + + def _process_email(self, email_id): + """处理单个邮件,提取验证码和链接""" + # 从邮件存储获取邮件 + email_data = self.mail_store.get_email_by_id(email_id, mark_as_read=False) + if not email_data: + logger.warning(f"找不到ID为 {email_id} 的邮件") + return False + + # 提取验证码和链接已经在Email模型的extract_verification_data方法中实现 + # 这里可以添加更复杂的提取逻辑或后处理 + + logger.info(f"邮件 {email_id} 处理完成") + return True + + @staticmethod + def extract_verification_code(content): + """从内容中提取验证码""" + code_patterns = [ + r'\b[A-Z0-9]{4,8}\b', # 基本验证码格式 + r'验证码[::]\s*([A-Z0-9]{4,8})', + r'验证码是[::]\s*([A-Z0-9]{4,8})', + r'code[::]\s*([A-Z0-9]{4,8})', + r'码[::]\s*(\d{4,8})' # 纯数字验证码 + ] + + for pattern in code_patterns: + matches = re.findall(pattern, content, re.IGNORECASE) + if matches: + return matches[0] + + return None + + @staticmethod + def extract_verification_link(content): + """从内容中提取验证链接""" + link_patterns = [ + r'(https?://\S+(?:verify|confirm|activate)\S+)', + r'(https?://\S+(?:token|auth|account)\S+)', + r'href\s*=\s*["\']([^"\']+(?:verify|confirm|activate)[^"\']*)["\']' + ] + + for pattern in link_patterns: + matches = re.findall(pattern, content, re.IGNORECASE) + if matches: + return matches[0] + + return None \ No newline at end of file diff --git a/app/services/mail_store.py b/app/services/mail_store.py new file mode 100644 index 0000000..366a253 --- /dev/null +++ b/app/services/mail_store.py @@ -0,0 +1,263 @@ +import logging +import os +import email +from email.policy import default +from sqlalchemy.orm import Session +from datetime import datetime +import re + +from ..models.domain import Domain +from ..models.mailbox import Mailbox +from ..models.email import Email +from ..models.attachment import Attachment + +logger = logging.getLogger(__name__) + +class MailStore: + """邮件存储服务,负责保存和检索邮件""" + + def __init__(self, db_session_factory, storage_path=None): + """ + 初始化邮件存储服务 + + 参数: + db_session_factory: 数据库会话工厂函数 + storage_path: 附件存储路径 + """ + self.db_session_factory = db_session_factory + self.storage_path = storage_path or os.path.join(os.getcwd(), 'email_data') + + # 确保存储目录存在 + if not os.path.exists(self.storage_path): + os.makedirs(self.storage_path) + + async def save_email(self, sender, recipient, message, raw_data): + """ + 保存一封电子邮件 + + 参数: + sender: 发件人地址 + recipient: 收件人地址 + message: 解析后的邮件对象 + raw_data: 原始邮件数据 + + 返回: + 成功返回邮件ID,失败返回None + """ + # 从收件人地址中提取用户名和域名 + try: + address, domain_name = recipient.split('@', 1) + except ValueError: + logger.warning(f"无效的收件人地址格式: {recipient}") + return None + + # 获取数据库会话 + db = self.db_session_factory() + + try: + # 检查域名是否存在且活跃 + domain = db.query(Domain).filter_by(name=domain_name, active=True).first() + if not domain: + logger.warning(f"不支持的域名: {domain_name}") + return None + + # 查找或创建邮箱 + mailbox = db.query(Mailbox).filter_by(address=address, domain_id=domain.id).first() + if not mailbox: + # 自动创建新邮箱 + mailbox = Mailbox( + address=address, + domain_id=domain.id, + active=True + ) + db.add(mailbox) + db.flush() # 获取ID但不提交 + logger.info(f"已为 {recipient} 自动创建邮箱") + + # 提取邮件内容 + subject = message.get('subject', '') + + # 获取文本和HTML内容 + body_text = None + body_html = None + attachments_data = [] + + if message.is_multipart(): + for part in message.walk(): + content_type = part.get_content_type() + content_disposition = part.get_content_disposition() + + # 处理文本内容 + if content_disposition is None or content_disposition == 'inline': + if content_type == 'text/plain' and not body_text: + body_text = part.get_content() + elif content_type == 'text/html' and not body_html: + body_html = part.get_content() + + # 处理附件 + elif content_disposition == 'attachment': + filename = part.get_filename() + if filename: + content = part.get_payload(decode=True) + if content: + attachments_data.append({ + 'filename': filename, + 'content_type': content_type, + 'data': content, + 'size': len(content) + }) + else: + # 非多部分邮件 + content_type = message.get_content_type() + if content_type == 'text/plain': + body_text = message.get_content() + elif content_type == 'text/html': + body_html = message.get_content() + + # 创建邮件记录 + email_obj = Email( + mailbox_id=mailbox.id, + sender=sender, + recipients=recipient, + subject=subject, + body_text=body_text, + body_html=body_html, + headers={k: v for k, v in message.items()} + ) + + # 保存邮件 + db.add(email_obj) + db.flush() # 获取ID但不提交 + + # 提取验证信息 + email_obj.extract_verification_data() + + # 保存附件 + for attachment_data in attachments_data: + attachment = Attachment( + email_id=email_obj.id, + filename=attachment_data['filename'], + content_type=attachment_data['content_type'], + size=attachment_data['size'] + ) + + db.add(attachment) + db.flush() + + # 决定存储位置 + if attachment_data['size'] > 1024 * 1024: # 大于1MB的存储到文件系统 + attachments_dir = os.path.join(self.storage_path, 'attachments') + attachment.save_to_filesystem(attachment_data['data'], attachments_dir) + else: + # 小附件直接存储在数据库 + attachment.content = attachment_data['data'] + + # 提交所有更改 + db.commit() + logger.info(f"邮件已成功保存: {sender} -> {recipient}, ID: {email_obj.id}") + return email_obj.id + + except Exception as e: + db.rollback() + logger.error(f"保存邮件时出错: {str(e)}") + return None + finally: + db.close() + + def get_emails_for_mailbox(self, mailbox_id, limit=50, offset=0, unread_only=False): + """获取指定邮箱的邮件列表""" + db = self.db_session_factory() + try: + query = db.query(Email).filter(Email.mailbox_id == mailbox_id) + + if unread_only: + query = query.filter(Email.read == False) + + total = query.count() + + emails = query.order_by(Email.received_at.desc()) \ + .limit(limit) \ + .offset(offset) \ + .all() + + return { + 'total': total, + 'items': [email.to_dict() for email in emails] + } + except Exception as e: + logger.error(f"获取邮件列表时出错: {str(e)}") + return {'total': 0, 'items': []} + finally: + db.close() + + def get_email_by_id(self, email_id, mark_as_read=True): + """获取指定ID的邮件详情""" + db = self.db_session_factory() + try: + email = db.query(Email).filter(Email.id == email_id).first() + + if not email: + return None + + if mark_as_read and not email.read: + email.read = True + email.last_read = datetime.utcnow() + db.commit() + + # 获取附件信息 + attachments = [attachment.to_dict() for attachment in email.attachments] + + # 构建完整响应 + result = email.to_dict() + result['body_text'] = email.body_text + result['body_html'] = email.body_html + result['attachments'] = attachments + + return result + except Exception as e: + db.rollback() + logger.error(f"获取邮件详情时出错: {str(e)}") + return None + finally: + db.close() + + def delete_email(self, email_id): + """删除指定ID的邮件""" + db = self.db_session_factory() + try: + email = db.query(Email).filter(Email.id == email_id).first() + + if not email: + return False + + db.delete(email) + db.commit() + return True + except Exception as e: + db.rollback() + logger.error(f"删除邮件时出错: {str(e)}") + return False + finally: + db.close() + + def get_attachment_content(self, attachment_id): + """获取附件内容""" + db = self.db_session_factory() + try: + attachment = db.query(Attachment).filter(Attachment.id == attachment_id).first() + + if not attachment: + return None + + content = attachment.get_content() + + return { + 'content': content, + 'filename': attachment.filename, + 'content_type': attachment.content_type + } + except Exception as e: + logger.error(f"获取附件内容时出错: {str(e)}") + return None + finally: + db.close() \ No newline at end of file diff --git a/app/services/smtp_server.py b/app/services/smtp_server.py new file mode 100644 index 0000000..32443df --- /dev/null +++ b/app/services/smtp_server.py @@ -0,0 +1,136 @@ +import asyncio +import logging +import email +import platform +from email.policy import default +from aiosmtpd.controller import Controller +from aiosmtpd.smtp import SMTP as SMTPProtocol +from aiosmtpd.handlers import Message +import os +import sys +import threading + +from ..models.domain import Domain +from ..models.mailbox import Mailbox + +logger = logging.getLogger(__name__) + +# 检测是否Windows环境 +IS_WINDOWS = platform.system().lower() == 'windows' + +class EmailHandler(Message): + """处理接收的电子邮件""" + + def __init__(self, mail_store): + super().__init__() + self.mail_store = mail_store + + def handle_message(self, message): + """处理邮件消息,这是Message类的抽象方法,必须实现""" + # 这个方法在异步DATA处理完成后被调用,但我们的邮件处理逻辑已经在handle_DATA中实现 + # 所以这里只是一个空实现 + return + + async def handle_DATA(self, server, session, envelope): + """处理接收到的邮件数据""" + try: + # 获取收件人和发件人 + peer = session.peer + mail_from = envelope.mail_from + rcpt_tos = envelope.rcpt_tos + + # 获取原始邮件内容 + data = envelope.content + mail = email.message_from_bytes(data, policy=default) + + # 保存邮件到存储服务 + for rcpt in rcpt_tos: + result = await self.mail_store.save_email(mail_from, rcpt, mail, data) + + # 记录日志 + if result: + logger.info(f"邮件已保存: {mail_from} -> {rcpt}, 主题: {mail.get('Subject')}") + else: + logger.warning(f"邮件未保存: {mail_from} -> {rcpt}, 可能是无效地址") + + return '250 Message accepted for delivery' + except Exception as e: + logger.error(f"处理邮件时出错: {str(e)}") + return '451 Requested action aborted: error in processing' + + +# 为Windows环境自定义SMTP控制器 +if IS_WINDOWS: + class WindowsSafeController(Controller): + """Windows环境安全的Controller,跳过连接测试""" + def _trigger_server(self): + """Windows环境下跳过SMTP服务器自检连接测试""" + # 在Windows环境下,我们跳过自检连接测试 + logger.info("Windows环境: 跳过SMTP服务器连接自检") + return + + +class SMTPServer: + """SMTP服务器实现""" + + def __init__(self, host='0.0.0.0', port=25, mail_store=None): + self.host = host + self.port = port + self.mail_store = mail_store + self.controller = None + self.server_thread = None + + def start(self): + """启动SMTP服务器""" + if self.controller: + logger.warning("SMTP服务器已经在运行") + return + + try: + handler = EmailHandler(self.mail_store) + + # 根据环境选择适当的Controller + if IS_WINDOWS: + # Windows环境使用自定义Controller + logger.info(f"Windows环境: 使用自定义Controller启动SMTP服务器 {self.host}:{self.port}") + self.controller = WindowsSafeController( + handler, + hostname=self.host, + port=self.port + ) + else: + # 非Windows环境使用标准Controller + self.controller = Controller( + handler, + hostname=self.host, + port=self.port + ) + + # 在单独的线程中启动服务器 + self.server_thread = threading.Thread( + target=self.controller.start, + daemon=True + ) + self.server_thread.start() + + logger.info(f"SMTP服务器已启动在 {self.host}:{self.port}") + return True + except Exception as e: + logger.error(f"启动SMTP服务器失败: {str(e)}") + return False + + def stop(self): + """停止SMTP服务器""" + if not self.controller: + logger.warning("SMTP服务器没有运行") + return + + try: + self.controller.stop() + self.controller = None + self.server_thread = None + logger.info("SMTP服务器已停止") + return True + except Exception as e: + logger.error(f"停止SMTP服务器失败: {str(e)}") + return False \ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..e69de29 diff --git a/basic_test.py b/basic_test.py new file mode 100644 index 0000000..4d8a516 --- /dev/null +++ b/basic_test.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import requests +import traceback +import time +import sys + +# API配置 +API_BASE_URL = "http://localhost:5000/api" + +def print_separator(title): + """打印分隔线""" + print("\n" + "=" * 80) + print(title.center(80)) + print("=" * 80 + "\n") + +def create_domain(name, description=""): + """创建域名""" + print(f"创建域名: {name}") + try: + data = { + "name": name, + "description": description + } + response = requests.post(f"{API_BASE_URL}/domains", json=data) + print(f"响应状态码: {response.status_code}") + + if response.status_code in (200, 201): + result = response.json() + print("响应内容:") + print(result) + return result.get("domain") + else: + print(f"创建域名失败: {response.text}") + return None + except Exception as e: + print(f"创建域名时出错: {str(e)}") + traceback.print_exc() + return None + +def create_mailbox(domain_id, address): + """创建邮箱""" + print(f"创建邮箱: {address} (域名ID: {domain_id})") + try: + data = { + "domain_id": domain_id, + "address": address + } + response = requests.post(f"{API_BASE_URL}/mailboxes", json=data) + print(f"响应状态码: {response.status_code}") + + if response.status_code in (200, 201): + result = response.json() + print("响应内容:") + print(result) + return result.get("mailbox") + else: + print(f"创建邮箱失败: {response.text}") + return None + except Exception as e: + print(f"创建邮箱时出错: {str(e)}") + traceback.print_exc() + return None + +def get_all_domains(): + """获取所有域名""" + print("获取所有域名") + try: + response = requests.get(f"{API_BASE_URL}/domains") + print(f"响应状态码: {response.status_code}") + + if response.status_code == 200: + result = response.json() + domains = result.get("domains", []) + print(f"找到 {len(domains)} 个域名:") + for domain in domains: + print(f" - {domain.get('name')} (ID: {domain.get('id')})") + return domains + else: + print(f"获取域名失败: {response.text}") + return [] + except Exception as e: + print(f"获取域名时出错: {str(e)}") + traceback.print_exc() + return [] + +def get_all_mailboxes(): + """获取所有邮箱""" + print("获取所有邮箱") + try: + response = requests.get(f"{API_BASE_URL}/mailboxes") + print(f"响应状态码: {response.status_code}") + + if response.status_code == 200: + result = response.json() + mailboxes = result.get("mailboxes", []) + print(f"找到 {len(mailboxes)} 个邮箱:") + for mailbox in mailboxes: + print(f" - {mailbox.get('full_address')} (ID: {mailbox.get('id')})") + return mailboxes + else: + print(f"获取邮箱失败: {response.text}") + return [] + except Exception as e: + print(f"获取邮箱时出错: {str(e)}") + traceback.print_exc() + return [] + +def main(): + try: + print_separator("基础功能测试") + + # 测试获取所有域名 + print_separator("测试获取域名") + domains = get_all_domains() + + # 测试创建新域名 + print_separator("测试创建域名") + test_domain = create_domain("test-domain.com", "测试域名") + + if test_domain: + print(f"成功创建域名: {test_domain.get('name')} (ID: {test_domain.get('id')})") + + # 测试获取所有邮箱 + print_separator("测试获取邮箱") + mailboxes = get_all_mailboxes() + + # 测试创建新邮箱 + print_separator("测试创建邮箱") + test_mailbox = create_mailbox(test_domain.get('id'), "testuser") + + if test_mailbox: + print(f"成功创建邮箱: {test_mailbox.get('full_address')} (ID: {test_mailbox.get('id')})") + else: + print("创建邮箱失败") + else: + print("创建域名失败") + + print_separator("测试完成") + return 0 + + except Exception as e: + print(f"测试过程中发生错误: {str(e)}") + traceback.print_exc() + return 1 + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/check_db.py b/check_db.py new file mode 100644 index 0000000..258db43 --- /dev/null +++ b/check_db.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from app.models import get_session, Domain, Mailbox, Email + +def check_database(): + """直接从数据库获取邮件信息""" + session = get_session() + + try: + # 获取所有域名 + print("获取所有域名...") + domains = session.query(Domain).all() + print(f"找到 {len(domains)} 个域名:") + for domain in domains: + print(f" - {domain.name} (ID: {domain.id})") + + # 获取所有邮箱 + print("\n获取所有邮箱...") + mailboxes = session.query(Mailbox).all() + print(f"找到 {len(mailboxes)} 个邮箱:") + for mailbox in mailboxes: + print(f" - {mailbox.full_address} (ID: {mailbox.id})") + + # 获取所有邮件 + print("\n获取所有邮件...") + emails = session.query(Email).all() + print(f"找到 {len(emails)} 封邮件:") + + for i, email in enumerate(emails, 1): + print(f"\n--- 邮件 {i} ---") + print(f"ID: {email.id}") + print(f"邮箱ID: {email.mailbox_id}") + + # 获取邮箱信息 + mailbox = session.query(Mailbox).filter_by(id=email.mailbox_id).first() + mailbox_address = mailbox.full_address if mailbox else "未知" + + print(f"收件人: {mailbox_address}") + print(f"发件人: {email.sender}") + print(f"主题: {email.subject}") + print(f"接收时间: {email.received_at}") + print(f"是否已读: {email.read}") + + # 打印邮件内容 + print("\n邮件文本内容摘要:") + if email.body_text: + print(email.body_text[:200] + "..." if len(email.body_text) > 200 else email.body_text) + else: + print("<无文本内容>") + + print("\n邮件HTML内容摘要:") + if email.body_html: + print(email.body_html[:200] + "..." if len(email.body_html) > 200 else email.body_html) + else: + print("<无HTML内容>") + + # 提取验证码 + if hasattr(email, 'verification_code') and email.verification_code: + print(f"\n验证码: {email.verification_code}") + + print("-" * 50) + + finally: + session.close() + +if __name__ == "__main__": + check_database() \ No newline at end of file diff --git a/check_deployment_readiness.py b/check_deployment_readiness.py new file mode 100644 index 0000000..69cd1c7 --- /dev/null +++ b/check_deployment_readiness.py @@ -0,0 +1,355 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +邮箱系统部署准备检查脚本 +用于验证服务器环境是否满足系统运行的要求 +""" + +import os +import sys +import platform +import socket +import subprocess +import importlib +import shutil +import json +from datetime import datetime + +# 颜色输出 +class Colors: + HEADER = '\033[95m' + BLUE = '\033[94m' + GREEN = '\033[92m' + YELLOW = '\033[93m' + RED = '\033[91m' + ENDC = '\033[0m' + BOLD = '\033[1m' + UNDERLINE = '\033[4m' + +# 检查结果 +class CheckResult: + PASS = "PASS" + WARN = "WARN" + FAIL = "FAIL" + INFO = "INFO" + +def print_header(title): + """打印检查标题""" + print(f"\n{Colors.HEADER}{Colors.BOLD}{'=' * 80}{Colors.ENDC}") + print(f"{Colors.HEADER}{Colors.BOLD}{title.center(80)}{Colors.ENDC}") + print(f"{Colors.HEADER}{Colors.BOLD}{'=' * 80}{Colors.ENDC}\n") + +def print_result(check_name, result, message, details=None): + """打印检查结果""" + if result == CheckResult.PASS: + status = f"{Colors.GREEN}[{result}]{Colors.ENDC}" + elif result == CheckResult.WARN: + status = f"{Colors.YELLOW}[{result}]{Colors.ENDC}" + elif result == CheckResult.FAIL: + status = f"{Colors.RED}[{result}]{Colors.ENDC}" + else: + status = f"{Colors.BLUE}[{result}]{Colors.ENDC}" + + print(f"{status} {check_name}: {message}") + + if details: + print(f" {Colors.BLUE}Details:{Colors.ENDC} {details}") + +def check_python_version(): + """检查Python版本""" + required_version = (3, 7) + current_version = sys.version_info + + if current_version >= required_version: + result = CheckResult.PASS + message = f"Python版本 {sys.version.split()[0]} 满足要求 (最低要求: {required_version[0]}.{required_version[1]})" + else: + result = CheckResult.FAIL + message = f"Python版本 {sys.version.split()[0]} 低于要求的最低版本 {required_version[0]}.{required_version[1]}" + + print_result("Python版本", result, message) + return result == CheckResult.PASS + +def check_os(): + """检查操作系统""" + system = platform.system() + release = platform.release() + + result = CheckResult.INFO + message = f"操作系统: {system} {release}" + + if system.lower() == "windows": + details = "Windows环境需要特殊配置,请确保已正确设置local_settings.py" + else: + details = "Linux环境适合生产部署" + + print_result("操作系统", result, message, details) + return True + +def check_port_availability(host='0.0.0.0', ports=[25, 5000]): + """检查端口是否可用""" + all_available = True + + for port in ports: + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.bind((host, port)) + sock.close() + result = CheckResult.PASS + message = f"端口 {port} 可用" + details = None + except socket.error as e: + all_available = False + result = CheckResult.FAIL + message = f"端口 {port} 已被占用或无权限访问" + details = f"错误: {str(e)}" + + # 对于Windows环境和低端口,提供额外提示 + if platform.system().lower() == "windows" and port < 1024: + details += "\n Windows环境下需要管理员权限才能使用低端口,或者使用大于1024的端口" + + print_result(f"端口 {port}", result, message, details) + + return all_available + +def check_dependencies(): + """检查依赖库是否已安装""" + required_packages = [ + "flask", "python-dotenv", "aiosmtpd", "sqlalchemy", + "pydantic", "requests", "pytz", "email-validator", + "psutil" + ] + + missing_packages = [] + + for package in required_packages: + try: + importlib.import_module(package.replace("-", "_")) + result = CheckResult.PASS + message = f"已安装" + except ImportError: + missing_packages.append(package) + result = CheckResult.FAIL + message = f"未安装" + + print_result(f"依赖库 {package}", result, message) + + if missing_packages: + print(f"\n{Colors.YELLOW}缺少以下依赖库,可以使用以下命令安装:{Colors.ENDC}") + print(f"pip install {' '.join(missing_packages)}") + + return len(missing_packages) == 0 + +def check_network(): + """检查网络连接""" + try: + # 尝试连接到一个外部DNS服务器 + socket.create_connection(("8.8.8.8", 53), timeout=5) + result = CheckResult.PASS + message = "网络连接正常" + except socket.error: + result = CheckResult.WARN + message = "无法连接到互联网,可能会影响部分功能" + + print_result("网络连接", result, message) + return result == CheckResult.PASS + +def check_disk_space(): + """检查磁盘空间""" + # 获取当前脚本所在目录的可用空间 + if platform.system().lower() == "windows": + try: + import ctypes + free_bytes = ctypes.c_ulonglong(0) + ctypes.windll.kernel32.GetDiskFreeSpaceExW( + ctypes.c_wchar_p(os.getcwd()), + None, None, + ctypes.pointer(free_bytes) + ) + free_space = free_bytes.value / (1024 * 1024 * 1024) # GB + except: + free_space = None + else: + try: + st = os.statvfs(os.getcwd()) + free_space = (st.f_bavail * st.f_frsize) / (1024 * 1024 * 1024) # GB + except: + free_space = None + + if free_space is not None: + if free_space > 5: + result = CheckResult.PASS + message = f"磁盘空间充足: {free_space:.2f} GB可用" + elif free_space > 1: + result = CheckResult.WARN + message = f"磁盘空间有限: {free_space:.2f} GB可用" + else: + result = CheckResult.FAIL + message = f"磁盘空间不足: {free_space:.2f} GB可用" + else: + result = CheckResult.INFO + message = "无法检查磁盘空间" + + print_result("磁盘空间", result, message) + return result != CheckResult.FAIL + +def check_permissions(): + """检查文件和目录权限""" + # 检查当前目录是否可写 + try: + test_file = os.path.join(os.getcwd(), ".permission_test") + with open(test_file, "w") as f: + f.write("test") + os.remove(test_file) + result = CheckResult.PASS + message = "当前目录可写" + except: + result = CheckResult.FAIL + message = "当前目录不可写,可能影响系统运行" + + print_result("文件权限", result, message) + return result == CheckResult.PASS + +def check_firewall(): + """检查防火墙状态""" + result = CheckResult.INFO + + if platform.system().lower() == "linux": + # 检查常见的Linux防火墙 + if shutil.which("ufw"): + try: + output = subprocess.check_output(["sudo", "ufw", "status"], universal_newlines=True) + if "inactive" in output.lower(): + message = "UFW防火墙未启用" + else: + message = "UFW防火墙已启用,请确保已开放25和5000端口" + except: + message = "无法检查UFW防火墙状态" + elif shutil.which("firewall-cmd"): + try: + subprocess.check_call(["firewall-cmd", "--state"], stdout=subprocess.DEVNULL) + message = "Firewalld防火墙已启用,请确保已开放25和5000端口" + except: + message = "Firewalld防火墙未启用或无法检查状态" + else: + message = "未检测到常见防火墙,请手动确认防火墙状态" + elif platform.system().lower() == "windows": + message = "Windows环境下请确保Windows防火墙已允许Python和相关端口" + else: + message = "未知操作系统,请手动检查防火墙状态" + + print_result("防火墙状态", result, message) + return True + +def check_smtp_tools(): + """检查是否存在常用SMTP测试工具""" + tools = ["telnet", "nc", "curl"] + available_tools = [] + + for tool in tools: + if shutil.which(tool): + available_tools.append(tool) + + if available_tools: + result = CheckResult.PASS + message = f"可用的网络工具: {', '.join(available_tools)}" + else: + result = CheckResult.WARN + message = "未找到常用网络工具,可能影响故障排查" + + print_result("网络工具", result, message) + return True + +def generate_report(checks): + """生成检查报告""" + report = { + "timestamp": datetime.now().isoformat(), + "system": platform.system(), + "platform": platform.platform(), + "python_version": sys.version, + "checks": checks, + "summary": { + "pass": sum(1 for c in checks.values() if c["result"] == CheckResult.PASS), + "warn": sum(1 for c in checks.values() if c["result"] == CheckResult.WARN), + "fail": sum(1 for c in checks.values() if c["result"] == CheckResult.FAIL), + "total": len(checks) + } + } + + with open("deployment_check_report.json", "w") as f: + json.dump(report, f, indent=2) + + print(f"\n{Colors.BLUE}检查报告已保存到 deployment_check_report.json{Colors.ENDC}") + +def main(): + """主函数""" + print_header("邮箱系统部署环境检查") + print(f"检查时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + print(f"检查目录: {os.getcwd()}") + + checks = {} + + # 基础环境检查 + print_header("1. 基础环境检查") + checks["python_version"] = {"result": CheckResult.PASS if check_python_version() else CheckResult.FAIL} + checks["os"] = {"result": CheckResult.INFO} + check_os() + + # 网络检查 + print_header("2. 网络检查") + checks["network"] = {"result": CheckResult.PASS if check_network() else CheckResult.WARN} + checks["ports"] = {"result": CheckResult.PASS if check_port_availability() else CheckResult.FAIL} + checks["firewall"] = {"result": CheckResult.INFO} + check_firewall() + + # 依赖检查 + print_header("3. 依赖检查") + checks["dependencies"] = {"result": CheckResult.PASS if check_dependencies() else CheckResult.FAIL} + + # 系统资源检查 + print_header("4. 系统资源检查") + checks["disk_space"] = {"result": CheckResult.PASS if check_disk_space() else CheckResult.FAIL} + checks["permissions"] = {"result": CheckResult.PASS if check_permissions() else CheckResult.FAIL} + + # 工具检查 + print_header("5. 工具检查") + checks["smtp_tools"] = {"result": CheckResult.PASS if check_smtp_tools() else CheckResult.WARN} + + # 总结 + print_header("检查结果摘要") + passes = sum(1 for c in checks.values() if c["result"] == CheckResult.PASS) + warns = sum(1 for c in checks.values() if c["result"] == CheckResult.WARN) + fails = sum(1 for c in checks.values() if c["result"] == CheckResult.FAIL) + infos = sum(1 for c in checks.values() if c["result"] == CheckResult.INFO) + + print(f"{Colors.GREEN}通过: {passes}{Colors.ENDC}") + print(f"{Colors.YELLOW}警告: {warns}{Colors.ENDC}") + print(f"{Colors.RED}失败: {fails}{Colors.ENDC}") + print(f"{Colors.BLUE}信息: {infos}{Colors.ENDC}") + print(f"总检查项: {len(checks)}") + + if fails > 0: + print(f"\n{Colors.RED}系统存在{fails}项关键问题,需要解决后再部署{Colors.ENDC}") + readiness = "不适合部署" + elif warns > 0: + print(f"\n{Colors.YELLOW}系统存在{warns}项警告,建议处理后再部署{Colors.ENDC}") + readiness = "需谨慎部署" + else: + print(f"\n{Colors.GREEN}系统检查通过,可以进行部署{Colors.ENDC}") + readiness = "可以部署" + + print(f"\n{Colors.BOLD}部署就绪状态: {readiness}{Colors.ENDC}") + + # 生成报告 + generate_report(checks) + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\n检查被用户中断") + except Exception as e: + print(f"\n{Colors.RED}检查过程中出错: {str(e)}{Colors.ENDC}") + import traceback + traceback.print_exc() \ No newline at end of file diff --git a/check_emails.py b/check_emails.py new file mode 100644 index 0000000..f70e820 --- /dev/null +++ b/check_emails.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import requests +import json +import os + +# 基础URL +BASE_URL = "http://localhost:5000/api" + +def check_emails(): + """检查系统中的邮件""" + try: + # 获取域名列表 + print("获取域名列表...") + domains_response = requests.get(f"{BASE_URL}/domains") + domains_data = domains_response.json() + + # 从响应中提取domains列表 + domains = domains_data.get('domains', []) + print(f"系统中的域名: {len(domains)}") + for domain in domains: + print(f"- {domain.get('name', '未知')} (ID: {domain.get('id', '未知')})") + + # 获取邮箱列表 + print("\n获取邮箱列表...") + mailboxes_response = requests.get(f"{BASE_URL}/mailboxes") + mailboxes_data = mailboxes_response.json() + + # 从响应中提取mailboxes列表 + mailboxes = mailboxes_data.get('mailboxes', []) + print(f"系统中的邮箱: {len(mailboxes)}") + + if len(mailboxes) == 0: + print("还没有创建邮箱,创建一个新邮箱...") + + # 获取第一个域名的ID + if domains: + domain_id = domains[0].get('id') + print(f"使用域名ID: {domain_id}") + + # 创建一个新邮箱 + create_data = { + "domain_id": domain_id, + "address": "user", + "password": "password123", + "description": "测试用户" + } + + create_response = requests.post(f"{BASE_URL}/mailboxes", json=create_data) + print(f"创建邮箱响应状态: {create_response.status_code}") + print(f"创建邮箱响应内容: {create_response.text}") + + # 再次获取邮箱列表 + mailboxes_response = requests.get(f"{BASE_URL}/mailboxes") + mailboxes_data = mailboxes_response.json() + mailboxes = mailboxes_data.get('mailboxes', []) + + # 遍历邮箱列表 + for mailbox in mailboxes: + print(f"- {mailbox.get('address', '未知')}@{mailbox.get('domain_name', '?')} (ID: {mailbox.get('id', '未知')})") + + # 获取该邮箱的邮件 + mailbox_id = mailbox.get('id') + if mailbox_id: + print(f" 获取邮箱ID={mailbox_id}的邮件...") + emails_response = requests.get(f"{BASE_URL}/mailboxes/{mailbox_id}/emails") + emails_data = emails_response.json() + + # 从响应中提取emails列表 + emails = emails_data.get('emails', []) + print(f" 邮件数量: {len(emails)}") + + for email in emails: + print(f" - 主题: {email.get('subject', '无主题')}") + print(f" 从: {email.get('from_addr', '未知')}") + print(f" 发送时间: {email.get('created_at', '未知')}") + print(f" ID: {email.get('id', '未知')}") + + if not emails: + print(" 无邮件") + + # 检查存储目录 + email_data_dir = os.path.join(os.getcwd(), 'email_data') + if os.path.exists(email_data_dir): + print(f"\n检查存储目录: {email_data_dir}") + file_count = 0 + for root, dirs, files in os.walk(email_data_dir): + for file in files: + if file.endswith('.eml'): + print(f"- 发现邮件文件: {os.path.join(root, file)}") + file_count += 1 + + if file_count == 0: + print("存储目录中没有找到.eml文件") + else: + print(f"\n存储目录不存在: {email_data_dir}") + + except Exception as e: + print(f"检查失败: {str(e)}") + +if __name__ == "__main__": + check_emails() \ No newline at end of file diff --git a/check_server.py b/check_server.py new file mode 100644 index 0000000..e05c95a --- /dev/null +++ b/check_server.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import socket +import requests +import sys +import traceback + +def check_port_open(host, port): + """检查指定端口是否开放""" + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + s.connect((host, port)) + s.shutdown(socket.SHUT_RDWR) + return True + except Exception as e: + return False + finally: + s.close() + +def check_api_status(): + """检查API服务器状态""" + try: + response = requests.get("http://localhost:5000/api/status") + return response.status_code == 200, response.text + except Exception as e: + return False, str(e) + +def main(): + print("检查服务器状态...") + + # 检查HTTP服务器端口 + http_port_open = check_port_open("localhost", 5000) + print(f"HTTP服务器端口 (5000) 状态: {'开放' if http_port_open else '关闭'}") + + # 检查SMTP服务器端口 + smtp_port_open = check_port_open("localhost", 3825) + print(f"SMTP服务器端口 (3825) 状态: {'开放' if smtp_port_open else '关闭'}") + + # 检查API状态 + if http_port_open: + api_ok, api_response = check_api_status() + print(f"API状态: {'正常' if api_ok else '异常'}") + print(f"API响应: {api_response}") + + # 总结状态 + if http_port_open and smtp_port_open: + print("\n服务器状态: 正常运行") + return 0 + else: + print("\n服务器状态: 异常") + print("请确保服务器已启动并且端口未被占用。") + print("启动命令: python run.py --port 5000 --smtp-port 3825 --debug") + return 1 + +if __name__ == "__main__": + try: + sys.exit(main()) + except Exception as e: + print(f"检查过程发生错误: {str(e)}") + traceback.print_exc() + sys.exit(1) \ No newline at end of file diff --git a/check_specific_mailbox.py b/check_specific_mailbox.py new file mode 100644 index 0000000..f43da2d --- /dev/null +++ b/check_specific_mailbox.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import requests +import json +import os +import html +import re + +# 基础URL +BASE_URL = "http://localhost:5000/api" + +def check_mailbox_emails(mailbox_id): + """检查特定邮箱的邮件""" + try: + # 获取邮箱信息 + mailbox_response = requests.get(f"{BASE_URL}/mailboxes/{mailbox_id}") + mailbox_data = mailbox_response.json() + + if not mailbox_data.get('success', False): + print(f"获取邮箱信息失败: {mailbox_data.get('error', '未知错误')}") + return + + mailbox = mailbox_data.get('mailbox', {}) + print(f"邮箱信息: {mailbox.get('full_address')} (ID: {mailbox.get('id')})") + + # 获取该邮箱的邮件 + emails_response = requests.get(f"{BASE_URL}/mailboxes/{mailbox_id}/emails") + emails_data = emails_response.json() + + if not emails_data.get('success', False): + print(f"获取邮件失败: {emails_data.get('error', '未知错误')}") + return + + # 从响应中提取emails列表 + emails = emails_data.get('emails', []) + print(f"邮件数量: {len(emails)}") + + # 遍历显示邮件内容 + for i, email in enumerate(emails, 1): + print(f"\n--- 邮件 {i} ---") + print(f"ID: {email.get('id', '未知')}") + print(f"主题: {email.get('subject', '无主题')}") + print(f"发件人: {email.get('from_addr', '未知')}") + print(f"接收时间: {email.get('created_at', '未知')}") + + # 获取邮件详情 + email_id = email.get('id') + if email_id: + # 打印完整的邮件信息,包括所有字段 + print("\n邮件详细信息:") + for key, value in email.items(): + print(f" {key}: {value}") + + # 尝试提取验证码直接从邮件主题或内容 + email_subject = email.get('subject', '') + email_body_text = email.get('body_text', '') + email_body_html = email.get('body_html', '') + + print("\n邮件正文:") + if email_body_text: + print(f"--- 文本内容 ---\n{email_body_text[:200]}...") + + if email_body_html: + print(f"--- HTML内容 ---\n{email_body_html[:200]}...") + + # 尝试从原始内容中提取验证码 + verification_code = None + # 从HTML内容中查找 + if email_body_html: + code_match = re.search(r'letter-spacing: 5px[^>]*>([^<]+)<', email_body_html) + if code_match: + verification_code = code_match.group(1).strip() + + # 从文本内容中查找6位数字 + if not verification_code and email_body_text: + code_match = re.search(r'\b(\d{6})\b', email_body_text) + if code_match: + verification_code = code_match.group(1) + + if verification_code: + print(f"\n提取到的验证码: {verification_code}") + else: + print("\n未能提取到验证码") + + print("-------------------") + + if not emails: + print("此邮箱没有邮件") + + except Exception as e: + print(f"查询失败: {str(e)}") + +def main(): + # 邮箱ID - testaa@nosqli.com 的ID应该是2 + mailbox_id = 2 + + print(f"检查邮箱ID {mailbox_id} 的邮件...") + check_mailbox_emails(mailbox_id) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..cb6d0c6 --- /dev/null +++ b/config.py @@ -0,0 +1,79 @@ +import os +import secrets +from dotenv import load_dotenv + +# 加载环境变量 +load_dotenv() + +class BaseConfig: + """基础配置类""" + # 应用设置 + SECRET_KEY = os.environ.get('SECRET_KEY') or secrets.token_hex(16) + DEBUG = False + + # 数据库设置 + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///email_system.db' + SQLALCHEMY_TRACK_MODIFICATIONS = False + + # SMTP服务器设置 + SMTP_HOST = '0.0.0.0' + SMTP_PORT = 25 + + # 存储设置 + MAIL_STORAGE_PATH = os.path.join(os.getcwd(), 'email_data') + + # 日志设置 + LOG_LEVEL = 'INFO' + LOG_FILE = os.path.join(os.getcwd(), 'logs', 'email_system.log') + + # API设置 + API_RATE_LIMIT = '200/hour' + API_TOKEN_EXPIRE = 3600 * 24 * 7 # 7天 + + # 邮件域名配置 + MAIL_DOMAINS = os.environ.get('MAIL_DOMAINS', 'example.com,mail.example.com').split(',') + + +class DevelopmentConfig(BaseConfig): + """开发环境配置""" + DEBUG = True + LOG_LEVEL = 'DEBUG' + + # 使用内存数据库方便开发 + # SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:' + + +class TestingConfig(BaseConfig): + """测试环境配置""" + TESTING = True + DEBUG = True + + # 使用内存数据库进行测试 + SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:' + + # 使用随机端口进行测试 + SMTP_PORT = 2525 + + +class ProductionConfig(BaseConfig): + """生产环境配置""" + DEBUG = False + LOG_LEVEL = 'ERROR' + + # SMTP服务器设置 + SMTP_PORT = 25 + + # API设置 + API_RATE_LIMIT = '100/hour' + + +# 配置映射 +config_by_name = { + 'development': DevelopmentConfig, + 'testing': TestingConfig, + 'production': ProductionConfig, + 'default': DevelopmentConfig +} + +# 根据环境变量选择配置 +active_config = config_by_name.get(os.environ.get('FLASK_ENV', 'default'), DevelopmentConfig) \ No newline at end of file diff --git a/create_test_domain.py b/create_test_domain.py new file mode 100644 index 0000000..ec736a9 --- /dev/null +++ b/create_test_domain.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import requests +import json + +# 基础URL +BASE_URL = "http://localhost:5000/api" + +def create_domain(name, description): + """创建一个新域名""" + print(f"创建域名: {name}") + url = f"{BASE_URL}/domains" + data = { + "name": name, + "description": description + } + + try: + response = requests.post(url, json=data) + print(f"状态码: {response.status_code}") + print(f"响应内容: {response.text}") + response.raise_for_status() + result = response.json() + print(f"域名创建成功: {result}") + return result.get("domain", {}) + except Exception as e: + print(f"域名创建失败: {str(e)}") + return None + +def create_mailbox(domain_id, address, description=""): + """在指定域名下创建邮箱""" + print(f"在域 {domain_id} 下创建邮箱: {address}") + url = f"{BASE_URL}/mailboxes" + data = { + "domain_id": domain_id, + "address": address, + "description": description + } + + try: + response = requests.post(url, json=data) + print(f"状态码: {response.status_code}") + print(f"响应内容: {response.text}") + response.raise_for_status() + result = response.json() + print(f"邮箱创建成功: {result}") + return result.get("mailbox", {}) + except Exception as e: + print(f"邮箱创建失败: {str(e)}") + return None + +def main(): + # 检查API状态 + try: + status_response = requests.get(f"{BASE_URL}/status") + print(f"API状态: {status_response.status_code}") + print(f"API响应: {status_response.text[:200]}...") + except Exception as e: + print(f"API状态检查失败: {str(e)}") + + # 创建测试域名 nosqli.com + domain = create_domain("nosqli.com", "测试域名") + if not domain: + print("无法继续,域名创建失败") + return + + # 获取域名ID + domain_id = domain.get("id") + + # 创建测试邮箱 testaa@nosqli.com + mailbox = create_mailbox(domain_id, "testaa", "测试用户") + + print("\n测试环境准备完成:") + print(f"- 域名: nosqli.com (ID: {domain_id})") + if mailbox: + print(f"- 邮箱: testaa@nosqli.com (ID: {mailbox.get('id')})") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/deploy_email_system.sh b/deploy_email_system.sh new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/deploy_email_system.sh @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/deploy_production.sh b/deploy_production.sh new file mode 100644 index 0000000..8e6d02b --- /dev/null +++ b/deploy_production.sh @@ -0,0 +1,248 @@ +#!/bin/bash +# +# 邮箱系统生产环境部署和启动脚本 +# 使用前请确保已经阅读DEPLOYMENT_GUIDE.md和DNS_CONFIGURATION.md文档 +# + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' # 无颜色 + +# 显示标题 +echo -e "${BLUE}=======================================================${NC}" +echo -e "${BLUE} 邮箱系统生产环境部署脚本 ${NC}" +echo -e "${BLUE}=======================================================${NC}" +echo "" + +# 检查是否为root用户 +if [ "$EUID" -ne 0 ]; then + echo -e "${YELLOW}警告: 此脚本未以root用户运行,可能无法绑定25端口${NC}" + read -p "是否继续? (y/n): " continue_without_root + if [ "$continue_without_root" != "y" ]; then + echo "退出部署" + exit 1 + fi +fi + +# 检查Python版本 +echo -e "${BLUE}[1/7]${NC} 检查Python版本..." +if command -v python3 &>/dev/null; then + python_version=$(python3 --version) + echo -e "${GREEN}检测到 $python_version${NC}" + + # 检查Python版本是否符合要求 (>= 3.7) + python_major=$(python3 -c "import sys; print(sys.version_info.major)") + python_minor=$(python3 -c "import sys; print(sys.version_info.minor)") + + if [ "$python_major" -ge 3 ] && [ "$python_minor" -ge 7 ]; then + echo -e "${GREEN}Python版本满足要求${NC}" + else + echo -e "${RED}错误: 需要Python 3.7或更高版本${NC}" + exit 1 + fi +else + echo -e "${RED}错误: 未找到Python 3${NC}" + echo "请安装Python 3.7或更高版本" + exit 1 +fi + +# 检查必要的目录 +echo -e "${BLUE}[2/7]${NC} 创建必要的目录..." +mkdir -p logs email_data db +echo -e "${GREEN}目录创建完成${NC}" + +# 检查依赖 +echo -e "${BLUE}[3/7]${NC} 检查并安装依赖..." +if [ -f "requirements.txt" ]; then + pip3 install --no-cache-dir -r requirements.txt + if [ $? -eq 0 ]; then + echo -e "${GREEN}依赖安装成功${NC}" + else + echo -e "${RED}依赖安装失败${NC}" + exit 1 + fi +else + echo -e "${RED}错误: requirements.txt文件不存在${NC}" + exit 1 +fi + +# 设置配置文件 +echo -e "${BLUE}[4/7]${NC} 配置系统..." + +# 创建.env文件 +if [ ! -f ".env" ]; then + echo "创建.env文件..." + # 生成随机密钥 + SECRET_KEY=$(python3 -c 'import secrets; print(secrets.token_hex(16))') + + read -p "请输入邮件域名,多个域名用逗号分隔 (默认: example.com): " mail_domains + mail_domains=${mail_domains:-example.com} + + cat > .env << EOF +FLASK_ENV=production +SECRET_KEY=${SECRET_KEY} +MAIL_DOMAINS=${mail_domains} +DATABASE_URL=sqlite:///db/email_system.db +EOF + echo -e "${GREEN}.env文件创建成功${NC}" +else + echo -e "${YELLOW}注意: .env文件已存在,跳过创建${NC}" +fi + +# 设置生产环境配置 +if [ -f "production_local_settings.py" ]; then + if [ -f "local_settings.py" ]; then + echo "备份现有local_settings.py..." + mv local_settings.py local_settings.py.bak + fi + echo "复制生产环境配置..." + cp production_local_settings.py local_settings.py + echo -e "${GREEN}生产环境配置设置完成${NC}" +else + echo -e "${YELLOW}警告: production_local_settings.py不存在,无法设置生产环境配置${NC}" +fi + +# 检查端口 +echo -e "${BLUE}[5/7]${NC} 检查系统端口..." + +# 检查25端口 +nc -z -w5 127.0.0.1 25 &>/dev/null +if [ $? -eq 0 ]; then + echo -e "${YELLOW}警告: 端口25已被占用${NC}" + echo "要使用标准SMTP端口25,您需要停止现有服务或修改配置使用其他端口" + + read -p "是否使用备用端口3825? (y/n): " use_alt_port + if [ "$use_alt_port" = "y" ]; then + # 修改local_settings.py中的SMTP端口 + sed -i 's/SMTP_PORT = 25/SMTP_PORT = 3825/g' local_settings.py + echo -e "${GREEN}已修改配置使用端口3825${NC}" + smtp_port=3825 + else + echo "请手动修改local_settings.py中的SMTP_PORT配置" + smtp_port=25 + fi +else + echo -e "${GREEN}端口25可用${NC}" + smtp_port=25 +fi + +# 检查5000端口 +nc -z -w5 127.0.0.1 5000 &>/dev/null +if [ $? -eq 0 ]; then + echo -e "${YELLOW}警告: 端口5000已被占用${NC}" + echo "请确保端口5000未被其他服务使用" + + read -p "是否使用备用端口5001? (y/n): " use_alt_api_port + if [ "$use_alt_api_port" = "y" ]; then + api_port=5001 + else + echo "继续使用端口5000,可能导致启动失败" + api_port=5000 + fi +else + echo -e "${GREEN}端口5000可用${NC}" + api_port=5000 +fi + +# 初始化数据库 +echo -e "${BLUE}[6/7]${NC} 初始化数据库..." +python3 -c "from app.models import init_db; init_db()" +if [ $? -eq 0 ]; then + echo -e "${GREEN}数据库初始化成功${NC}" +else + echo -e "${RED}数据库初始化失败${NC}" + exit 1 +fi + +# 创建systemd服务文件 +echo -e "${BLUE}[7/7]${NC} 创建系统服务..." +if [ "$EUID" -eq 0 ]; then + # 获取当前工作目录的绝对路径 + WORKING_DIR=$(pwd) + + cat > /etc/systemd/system/email-system.service << EOF +[Unit] +Description=Email System Service +After=network.target + +[Service] +User=root +WorkingDirectory=${WORKING_DIR} +ExecStart=$(which python3) run.py --host 0.0.0.0 --port ${api_port} --smtp-port ${smtp_port} +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +EOF + + systemctl daemon-reload + systemctl enable email-system + echo -e "${GREEN}系统服务创建并启用成功${NC}" +else + echo -e "${YELLOW}注意: 未以root用户运行,跳过创建系统服务${NC}" + echo "您可以使用以下命令手动创建系统服务:" + echo "sudo bash -c 'cat > /etc/systemd/system/email-system.service << EOF" + echo "[Unit]" + echo "Description=Email System Service" + echo "After=network.target" + echo "" + echo "[Service]" + echo "User=root" + echo "WorkingDirectory=$(pwd)" + echo "ExecStart=$(which python3) run.py --host 0.0.0.0 --port ${api_port} --smtp-port ${smtp_port}" + echo "Restart=always" + echo "RestartSec=10" + echo "" + echo "[Install]" + echo "WantedBy=multi-user.target" + echo "EOF'" + echo "sudo systemctl daemon-reload" + echo "sudo systemctl enable email-system" +fi + +# 部署总结 +echo "" +echo -e "${BLUE}=======================================================${NC}" +echo -e "${BLUE} 部署完成 ${NC}" +echo -e "${BLUE}=======================================================${NC}" +echo "" +echo -e "API服务端口: ${GREEN}${api_port}${NC}" +echo -e "SMTP服务端口: ${GREEN}${smtp_port}${NC}" +echo -e "工作目录: ${GREEN}$(pwd)${NC}" +echo -e "日志文件: ${GREEN}$(pwd)/logs/email_system.log${NC}" +echo "" + +# 启动服务 +if [ "$EUID" -eq 0 ]; then + echo -e "${BLUE}正在启动邮箱系统服务...${NC}" + systemctl start email-system + sleep 2 + + systemctl status email-system --no-pager + + echo "" + echo -e "${GREEN}邮箱系统已启动并运行${NC}" + echo "您可以使用以下命令管理服务:" + echo " 查看状态: sudo systemctl status email-system" + echo " 停止服务: sudo systemctl stop email-system" + echo " 重启服务: sudo systemctl restart email-system" + echo " 查看日志: sudo journalctl -u email-system -f" +else + echo -e "${YELLOW}服务未启动,您可以使用以下命令手动启动服务:${NC}" + echo "sudo systemctl start email-system" + echo "" + echo "或者直接运行应用:" + echo "python3 run.py --host 0.0.0.0 --port ${api_port} --smtp-port ${smtp_port}" +fi + +echo "" +echo -e "${BLUE}部署后检查事项:${NC}" +echo "1. 确认您的DNS记录已正确配置 (请参考DNS_CONFIGURATION.md)" +echo "2. 验证邮件接收功能 (执行 python3 test_smtp_server.py)" +echo "3. 检查防火墙是否允许${api_port}和${smtp_port}端口的流量" +echo "" +echo -e "${GREEN}如有任何问题,请查看日志文件或联系系统管理员${NC}" \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..774d025 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,49 @@ +version: '3.8' + +services: + email_system: + build: + context: . + dockerfile: Dockerfile + container_name: email_system + restart: unless-stopped + ports: + - "5000:5000" # API端口 + - "25:25" # SMTP端口 + volumes: + - ./email_data:/app/email_data # 邮件数据持久化 + - ./db:/app/db # 数据库持久化 + - ./logs:/app/logs # 日志持久化 + environment: + - FLASK_ENV=production + - MAIL_DOMAINS=example.com,test-domain.com + # 可以在此处设置更多环境变量 + networks: + - email_network + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:5000/api/status"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + +# 如果需要添加数据库服务,可以在此处添加 +# 例如: +# db: +# image: postgres:13-alpine +# volumes: +# - postgres_data:/var/lib/postgresql/data/ +# environment: +# - POSTGRES_PASSWORD=password +# - POSTGRES_USER=user +# - POSTGRES_DB=email_db +# networks: +# - email_network + +networks: + email_network: + driver: bridge + +# 如果使用外部数据库,添加卷定义 +# volumes: +# postgres_data: \ No newline at end of file diff --git a/email_analyzer.py b/email_analyzer.py new file mode 100644 index 0000000..3061f79 --- /dev/null +++ b/email_analyzer.py @@ -0,0 +1,298 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import requests +import re +import json +import logging +from datetime import datetime +import smtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart + +# 配置日志 +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger('EmailAnalyzer') + +class EmailAnalyzer: + """邮件分析类,用于操作邮箱系统并分析邮件内容""" + + def __init__(self, api_base_url="http://localhost:5000/api", smtp_host="localhost", smtp_port=3825): + """初始化邮件分析器""" + self.api_base_url = api_base_url + self.smtp_host = smtp_host + self.smtp_port = smtp_port + logger.info(f"初始化邮件分析器: API={api_base_url}, SMTP={smtp_host}:{smtp_port}") + + # API相关方法 + def get_all_domains(self): + """获取所有域名""" + try: + response = requests.get(f"{self.api_base_url}/domains") + if response.status_code == 200: + domains = response.json().get('domains', []) + logger.info(f"获取到 {len(domains)} 个域名") + return domains + else: + logger.error(f"获取域名失败: HTTP {response.status_code}") + return [] + except Exception as e: + logger.error(f"获取域名出错: {str(e)}") + return [] + + def create_domain(self, domain_name, description=""): + """创建新域名""" + try: + data = { + "name": domain_name, + "description": description + } + response = requests.post(f"{self.api_base_url}/domains", json=data) + if response.status_code in (200, 201): + result = response.json() + domain = result.get("domain", {}) + logger.info(f"成功创建域名: {domain_name} (ID: {domain.get('id')})") + return domain + else: + logger.error(f"创建域名失败: HTTP {response.status_code} - {response.text}") + return None + except Exception as e: + logger.error(f"创建域名出错: {str(e)}") + return None + + def get_all_mailboxes(self): + """获取所有邮箱""" + try: + response = requests.get(f"{self.api_base_url}/mailboxes") + if response.status_code == 200: + mailboxes = response.json().get('mailboxes', []) + logger.info(f"获取到 {len(mailboxes)} 个邮箱") + return mailboxes + else: + logger.error(f"获取邮箱失败: HTTP {response.status_code}") + return [] + except Exception as e: + logger.error(f"获取邮箱出错: {str(e)}") + return [] + + def create_mailbox(self, domain_id, address): + """创建新邮箱""" + try: + data = { + "domain_id": domain_id, + "address": address + } + response = requests.post(f"{self.api_base_url}/mailboxes", json=data) + if response.status_code in (200, 201): + result = response.json() + mailbox = result.get("mailbox", {}) + logger.info(f"成功创建邮箱: {mailbox.get('full_address')} (ID: {mailbox.get('id')})") + return mailbox + else: + logger.error(f"创建邮箱失败: HTTP {response.status_code} - {response.text}") + return None + except Exception as e: + logger.error(f"创建邮箱出错: {str(e)}") + return None + + def get_mailbox_emails(self, mailbox_id): + """获取指定邮箱的所有邮件""" + try: + response = requests.get(f"{self.api_base_url}/mailboxes/{mailbox_id}/emails") + if response.status_code == 200: + emails = response.json().get('emails', []) + logger.info(f"获取到邮箱ID={mailbox_id}的 {len(emails)} 封邮件") + return emails + else: + logger.error(f"获取邮件失败: HTTP {response.status_code}") + return [] + except Exception as e: + logger.error(f"获取邮件出错: {str(e)}") + return [] + + def get_email_detail(self, email_id): + """获取指定邮件的详细信息""" + try: + response = requests.get(f"{self.api_base_url}/emails/{email_id}") + if response.status_code == 200: + email = response.json().get('email', {}) + logger.info(f"获取到邮件ID={email_id}的详细信息") + return email + else: + logger.error(f"获取邮件详情失败: HTTP {response.status_code}") + return {} + except Exception as e: + logger.error(f"获取邮件详情出错: {str(e)}") + return {} + + # SMTP相关方法 + def send_email(self, from_addr, to_addr, subject, body_text="", body_html=""): + """发送邮件""" + try: + # 创建邮件 + msg = MIMEMultipart('alternative') + msg['From'] = from_addr + msg['To'] = to_addr + msg['Subject'] = subject + + # 添加文本内容 + if body_text: + msg.attach(MIMEText(body_text, 'plain')) + + # 添加HTML内容 + if body_html: + msg.attach(MIMEText(body_html, 'html')) + + # 连接SMTP服务器并发送 + server = smtplib.SMTP(self.smtp_host, self.smtp_port) + server.set_debuglevel(1) # 开启调试 + server.sendmail(from_addr, to_addr, msg.as_string()) + server.quit() + + logger.info(f"成功发送邮件: {from_addr} -> {to_addr}, 主题: {subject}") + return True + except Exception as e: + logger.error(f"发送邮件失败: {str(e)}") + return False + + def send_verification_code(self, to_addr, code_length=6): + """发送验证码邮件""" + # 生成随机验证码 + import random + import string + code = ''.join(random.choices(string.digits, k=code_length)) + + # 邮件主题 + subject = f'您的验证码 - {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}' + + # 邮件HTML内容 + html = f''' + + +
+

验证码

+

您好!

+

您的验证码是:

+
+ {code} +
+

此验证码将在30分钟内有效。

+

如果这不是您请求的,请忽略此邮件。

+

谢谢!

+
+ 此邮件由系统自动发送,请勿回复。 +
+
+ + + ''' + + # 发送邮件 + result = self.send_email('noreply@system.com', to_addr, subject, body_html=html) + + return result, code + + # 邮件分析方法 + def extract_verification_code(self, email): + """从邮件中提取验证码""" + # 如果邮件对象已经包含验证码字段,直接返回 + if email.get('verification_code'): + return email.get('verification_code') + + # 尝试从HTML内容中提取 + body_html = email.get('body_html', '') + if body_html: + # 尝试匹配验证码样式 + code_match = re.search(r'letter-spacing: 5px[^>]*>([^<]+)<', body_html) + if code_match: + return code_match.group(1).strip() + + # 尝试从文本内容中提取6位数字验证码 + body_text = email.get('body_text', '') + if body_text: + code_match = re.search(r'\b(\d{6})\b', body_text) + if code_match: + return code_match.group(1) + + # 无法提取到验证码 + return None + + def extract_verification_link(self, email): + """从邮件中提取验证链接""" + # 如果邮件对象已经包含验证链接字段,直接返回 + if email.get('verification_link'): + return email.get('verification_link') + + # 尝试从HTML内容中提取链接 + body_html = email.get('body_html', '') + if body_html: + link_match = re.search(r'href=[\'"]([^\'"]*verify[^\'"]*)[\'"]', body_html) + if link_match: + return link_match.group(1) + + # 尝试从文本内容中提取链接 + body_text = email.get('body_text', '') + if body_text: + link_match = re.search(r'https?://\S+?(?:verify|confirm|activate)\S+', body_text) + if link_match: + return link_match.group(0) + + # 无法提取到验证链接 + return None + +# 使用示例 +def main(): + # 创建邮件分析器实例 + analyzer = EmailAnalyzer() + + # 获取所有域名 + domains = analyzer.get_all_domains() + for domain in domains: + print(f"域名: {domain.get('name')} (ID: {domain.get('id')})") + + # 如果没有域名,创建一个 + if not domains: + domain = analyzer.create_domain("test-domain.com", "测试域名") + print(f"创建域名: {domain.get('name')} (ID: {domain.get('id')}") + else: + domain = domains[0] + + # 获取所有邮箱 + mailboxes = analyzer.get_all_mailboxes() + for mailbox in mailboxes: + print(f"邮箱: {mailbox.get('full_address')} (ID: {mailbox.get('id')})") + + # 如果没有邮箱,创建一个 + if not mailboxes: + mailbox = analyzer.create_mailbox(domain.get('id'), "test-user") + print(f"创建邮箱: {mailbox.get('full_address')} (ID: {mailbox.get('id')}") + else: + mailbox = mailboxes[0] + + # 发送一封测试邮件 + to_addr = mailbox.get('full_address') + sent, code = analyzer.send_verification_code(to_addr) + print(f"验证码邮件发送{'成功' if sent else '失败'}, 验证码: {code}") + + # 等待2秒,让邮件被处理 + import time + time.sleep(2) + + # 获取邮箱中的邮件 + mailbox_id = mailbox.get('id') + emails = analyzer.get_mailbox_emails(mailbox_id) + print(f"获取到 {len(emails)} 封邮件") + + # 分析最新的邮件 + if emails: + latest_email = emails[0] # 假设按时间降序排列 + print(f"最新邮件: {latest_email.get('subject')}") + + verification_code = analyzer.extract_verification_code(latest_email) + print(f"提取到的验证码: {verification_code}") + + verification_link = analyzer.extract_verification_link(latest_email) + print(f"提取到的验证链接: {verification_link}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/get_code.py b/get_code.py new file mode 100644 index 0000000..3b5d520 --- /dev/null +++ b/get_code.py @@ -0,0 +1,21 @@ +import requests + +# 获取邮箱ID为2的所有邮件 +response = requests.get("http://localhost:5000/api/mailboxes/2/emails") +data = response.json() + +# 打印响应状态 +print(f"API响应状态: {response.status_code}") +print(f"API响应内容: {data}") + +# 如果成功,提取验证码 +if data.get('success', False): + emails = data.get('emails', []) + for email in emails: + print(f"\n邮件ID: {email.get('id')}") + print(f"主题: {email.get('subject')}") + print(f"发件人: {email.get('sender')}") + print(f"接收时间: {email.get('received_at')}") + print(f"验证码字段: {email.get('verification_code')}") +else: + print(f"获取邮件失败: {data.get('error', '未知错误')}") \ No newline at end of file diff --git a/get_full_email.py b/get_full_email.py new file mode 100644 index 0000000..904b2eb --- /dev/null +++ b/get_full_email.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import requests +import json +from pprint import pprint + +def get_all_domains(): + """获取所有域名""" + response = requests.get("http://localhost:5000/api/domains") + if response.status_code == 200: + return response.json().get('domains', []) + return [] + +def get_all_mailboxes(): + """获取所有邮箱""" + response = requests.get("http://localhost:5000/api/mailboxes") + if response.status_code == 200: + return response.json().get('mailboxes', []) + return [] + +def get_mailbox_emails(mailbox_id): + """获取指定邮箱的所有邮件""" + response = requests.get(f"http://localhost:5000/api/mailboxes/{mailbox_id}/emails") + print(f"API响应状态码: {response.status_code}") + if response.status_code == 200: + return response.json().get('emails', []) + return [] + +def get_email_detail(email_id): + """获取指定邮件的详细信息""" + response = requests.get(f"http://localhost:5000/api/emails/{email_id}") + if response.status_code == 200: + return response.json().get('email', {}) + return {} + +def main(): + # 获取所有域名 + print("获取所有域名...") + domains = get_all_domains() + print(f"找到 {len(domains)} 个域名:") + for domain in domains: + print(f" - {domain.get('name')} (ID: {domain.get('id')})") + + # 获取所有邮箱 + print("\n获取所有邮箱...") + mailboxes = get_all_mailboxes() + print(f"找到 {len(mailboxes)} 个邮箱:") + for mailbox in mailboxes: + print(f" - {mailbox.get('full_address')} (ID: {mailbox.get('id')})") + + # 遍历所有邮箱获取邮件 + for mailbox in mailboxes: + mailbox_id = mailbox.get('id') + mailbox_address = mailbox.get('full_address') + print(f"\n获取邮箱 {mailbox_address} (ID: {mailbox_id}) 的邮件...") + + emails = get_mailbox_emails(mailbox_id) + print(f"找到 {len(emails)} 封邮件") + + for index, email in enumerate(emails, 1): + email_id = email.get('id') + print(f"\n--- 邮件 {index} (ID: {email_id}) ---") + print(f"主题: {email.get('subject')}") + print(f"发件人: {email.get('sender')}") + print(f"接收时间: {email.get('received_at')}") + + print("\n完整邮件信息:") + # 打印所有字段 + for key, value in email.items(): + print(f" - {key}: {value}") + + # 获取邮件验证码相关字段 + verification_code = email.get('verification_code') + if verification_code: + print(f"\n提取到的验证码: {verification_code}") + + print("-" * 50) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/guihua.doc b/guihua.doc new file mode 100644 index 0000000..6d39503 --- /dev/null +++ b/guihua.doc @@ -0,0 +1,214 @@ +用户要求我们规划系统的基本结构,这与之前我们已经分析和完善的系统是一致的。我们已经查看了现有代码,发现系统已经有了基本的框架,包括: +SMTP服务器(使用aiosmtpd) +数据库模型(使用SQLAlchemy) +Web界面和API(使用Flask) +配置文件和启动脚本 +让我回顾一下已经存在的文件和目录,然后规划一个更完整的结构: +已有结构: +app/ +init_.py +api/ +init_.py +routes.py +domain_routes.py +email_routes.py +mailbox_routes.py +models/ +init_.py +domain.py +email.py +mailbox.py +attachment.py +services/ +init_.py +smtp_server.py +email_processor.py +mail_store.py +config.py +run.py +requirements.txt +Dockerfile +docker-compose.yml +start.sh +start_windows.ps1 +init_1panel.sh +现在需要规划一个更完整的结构,包括: +更完善的目录结构 +SMTP服务器设计 +数据库模型设计 +Web界面和API设计 +5. 配置和启动脚本 +我将按照用户的要求,规划一个完整的系统结构。 + +我将帮您规划这个邮箱系统的完整结构。根据之前的分析,系统已有基本框架,我们可以进一步规划和完善 + +emailsystem/ +├── app/ # 应用主目录 +│ ├── __init__.py # 应用初始化 +│ ├── api/ # API接口 +│ │ ├── __init__.py +│ │ ├── routes.py # 主路由 +│ │ ├── domain_routes.py # 域名管理 +│ │ ├── email_routes.py # 邮件管理 +│ │ └── mailbox_routes.py # 邮箱管理 +│ ├── models/ # 数据库模型 +│ │ ├── __init__.py +│ │ ├── domain.py # 域名模型 +│ │ ├── email.py # 邮件模型 +│ │ ├── mailbox.py # 邮箱模型 +│ │ └── attachment.py # 附件模型 +│ ├── services/ # 核心服务 +│ │ ├── __init__.py +│ │ ├── smtp_server.py # SMTP服务器 +│ │ ├── email_processor.py # 邮件处理器 +│ │ └── mail_store.py # 邮件存储 +│ ├── static/ # 静态资源 +│ │ ├── css/ +│ │ ├── js/ +│ │ └── img/ +│ ├── templates/ # 模板文件 +│ │ ├── base.html # 基础模板 +│ │ ├── index.html # 首页 +│ │ ├── dashboard.html # 控制面板 +│ │ └── mailbox/ # 邮箱相关页面 +│ └── utils/ # 工具函数 +│ ├── __init__.py +│ ├── validators.py # 验证工具 +│ └── extractors.py # 信息提取工具 +├── db/ # 数据库文件 +├── email_data/ # 邮件存储目录 +├── logs/ # 日志目录 +├── tests/ # 测试目录 +├── .env # 环境变量 +├── config.py # 配置文件 +├── requirements.txt # 依赖列表 +├── run.py # 主启动文件 +├── Dockerfile # Docker构建文件 +├── docker-compose.yml # Docker Compose配置 +├── start.sh # Linux启动脚本 +├── start_windows.ps1 # Windows启动脚本 +└── init_1panel.sh # 1Panel初始化脚本 + + +2. SMTP服务器设计 (aiosmtpd) +SMTP服务器负责接收邮件并存储到系统中。 +主要功能: +监听指定端口接收SMTP连接 +验证收件人地址是否在系统中 +接收并解析邮件内容 +将邮件内容转发给邮件存储服务 +支持多域名配置 +实现方案: +使用aiosmtpd库创建异步SMTP服务器 +在smtp_server.py中实现SMTPServer类 +自定义处理器处理接收到的邮件 +使用线程模式运行服务器,避免阻塞主进程 +3. 数据库模型设计 (SQLAlchemy) +数据库设计采用SQLAlchemy ORM,使用SQLite作为存储后端。 +主要模型: +Domain (域名) +id: 主键 +name: 域名 +description: 描述 +active: 是否激活 +created_at: 创建时间 +Mailbox (邮箱) +id: 主键 +username: 用户名 +domain_id: 外键关联Domain +description: 描述 +active: 是否激活 +created_at: 创建时间 +last_accessed: 上次访问时间 +Email (邮件) +id: 主键 +mailbox_id: 外键关联Mailbox +sender: 发件人 +subject: 主题 +content_text: 文本内容 +content_html: HTML内容 +received_at: 接收时间 +read: 是否已读 +verification_links: 验证链接JSON +verification_codes: 验证码JSON +raw_data: 原始邮件数据 +Attachment (附件) +id: 主键 +email_id: 外键关联Email +filename: 文件名 +content_type: 内容类型 +size: 大小 +storage_path: 存储路径 +4. Web界面和API设计 (Flask) +API设计: +域名管理API +GET /api/domains: 获取所有域名 +POST /api/domains: 创建新域名 +GET /api/domains/: 获取指定域名 +PUT /api/domains//toggle: 启用/禁用域名 +邮箱管理API +GET /api/mailboxes: 获取所有邮箱 +POST /api/mailboxes: 创建新邮箱 +POST /api/mailboxes/batch: 批量创建邮箱 +GET /api/mailboxes/: 获取指定邮箱 +GET /api/mailboxes//emails: 获取邮箱的所有邮件 +邮件管理API +GET /api/emails/: 获取指定邮件 +GET /api/emails//verification: 获取邮件中的验证信息 +GET /api/mailboxes//poll: 轮询新邮件 +4. 系统状态API +GET /api/status: 获取系统状态 +Web界面设计: +1. 首页/登录页 +系统概述 +登录入口 +控制面板 +系统状态概览 +最近接收邮件统计 +邮箱使用情况 +域名管理 +域名列表 +添加/编辑域名 +邮箱管理 +邮箱列表 +创建/批量创建邮箱 +邮件查看 +邮件列表 +邮件详情 +验证码/链接提取 +5. 配置和启动脚本 +配置文件 (config.py): +基础配置类 +开发环境配置 +测试环境配置 +生产环境配置 +根据环境变量选择配置 +环境变量 (.env): +FLASK_ENV: 运行环境 +SECRET_KEY: 安全密钥 +MAIL_DOMAINS: 邮件域名 +DATABASE_URL: 数据库连接字符串 +启动脚本: +run.py: 主启动文件 +start.sh: Linux启动脚本 +start_windows.ps1: Windows启动脚本 +init_1panel.sh: 1Panel初始化脚本 +Docker部署: +Dockerfile: 容器构建配置 +docker-compose.yml: 多容器配置 +实现建议 +ARM优化: +减少内存使用,避免大量对象创建 +异步处理耗时操作 +配置适当的线程数和工作进程数 +安全性考虑: +对敏感操作添加认证 +定期清理过期邮件 +在Docker中使用非root用户运行 +性能优化: +使用连接池管理数据库连接 +实现邮件缓存机制 +定时任务处理耗时操作 +监控与维护: +详细的日志记录 +系统状态API \ No newline at end of file diff --git a/init_1panel.sh b/init_1panel.sh new file mode 100644 index 0000000..b604628 --- /dev/null +++ b/init_1panel.sh @@ -0,0 +1,80 @@ +#!/bin/bash + +# 邮箱系统 1Panel 初始化脚本 +# 用于在1Panel环境中首次部署系统 + +echo "开始初始化邮箱系统..." + +# 检查是否在ARM架构上运行 +ARCH=$(uname -m) +if [[ $ARCH == "aarch64" || $ARCH == "arm64" ]]; then + echo "检测到ARM架构: $ARCH" +else + echo "警告: 当前架构 $ARCH 不是ARM,可能会影响性能优化" +fi + +# 创建必要的目录 +mkdir -p logs email_data db +echo "创建目录完成" + +# 检查环境变量文件是否存在,不存在则创建 +if [ ! -f .env ]; then + echo "创建.env文件..." + # 生成随机密钥 + SECRET_KEY=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32) + + cat > .env << EOF +FLASK_ENV=production +SECRET_KEY=${SECRET_KEY} +MAIL_DOMAINS=example.com,mail.example.com +DATABASE_URL=sqlite:///db/email_system.db +EOF + echo ".env文件已创建" +fi + +# 安装Python依赖 +echo "安装Python依赖..." +pip install --no-cache-dir -r requirements.txt +echo "依赖安装完成" + +# 初始化数据库 +echo "初始化数据库..." +python -c "from app.models import init_db; init_db()" +echo "数据库初始化完成" + +# 检查SMTP服务器端口是否可用 +PORT_CHECK=$(netstat -ln | grep ':25 ' | wc -l) +if [ $PORT_CHECK -gt 0 ]; then + echo "警告: 端口25已被占用,可能需要修改SMTP端口配置" + # 自动修改为备用端口587 + sed -i 's/SMTP_PORT = 25/SMTP_PORT = 587/g' config.py + echo "已将SMTP端口更改为587" +fi + +# 创建服务管理脚本 +cat > email_system.service << EOF +[Unit] +Description=Email System Service +After=network.target + +[Service] +User=root +WorkingDirectory=$(pwd) +ExecStart=$(which gunicorn) --bind 0.0.0.0:5000 --workers 4 run:app +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +EOF + +echo "服务管理脚本已创建: email_system.service" +echo "可以使用以下命令安装系统服务:" +echo "sudo cp email_system.service /etc/systemd/system/" +echo "sudo systemctl daemon-reload" +echo "sudo systemctl enable email_system.service" +echo "sudo systemctl start email_system.service" + +echo "初始化完成,系统已准备就绪" +echo "可以通过以下命令启动系统:" +echo "python run.py --host 0.0.0.0 --port 5000" \ No newline at end of file diff --git a/local_settings.py b/local_settings.py new file mode 100644 index 0000000..f878b57 --- /dev/null +++ b/local_settings.py @@ -0,0 +1,25 @@ +""" +Windows环境下的本地设置 +这个文件包含仅适用于本地Windows开发环境的设置 +不会影响生产环境部署 +""" +import platform +import logging + +# 是否启用Windows特殊设置 +IS_WINDOWS = platform.system().lower() == 'windows' + +# Windows环境特定配置 +if IS_WINDOWS: + # SMTP服务器配置 + SMTP_HOST = '127.0.0.1' # 在Windows上使用localhost而不是0.0.0.0 + SMTP_PORT = 3825 # 使用高端口避免权限问题 + + # 日志配置 + LOG_LEVEL = 'DEBUG' + + # 调试信息 + logging.info(f"已加载Windows环境特殊配置: SMTP端口={SMTP_PORT}") +else: + # 非Windows环境下不做特殊处理 + pass \ No newline at end of file diff --git a/monitor_email_system.py b/monitor_email_system.py new file mode 100644 index 0000000..df6ab80 --- /dev/null +++ b/monitor_email_system.py @@ -0,0 +1,708 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +邮件系统监控脚本 + +此脚本用于监控邮件系统的状态,包括: +- API可用性 +- SMTP连接 +- 磁盘使用量 +- 系统资源使用情况 +- 邮件队列状态 + +使用方法: + python monitor_email_system.py [--host HOST] [--api-port API_PORT] [--smtp-port SMTP_PORT] [--alert] + +选项: + --host HOST 服务器主机名或IP地址 (默认: localhost) + --api-port API_PORT API服务端口 (默认: 5000) + --smtp-port SMTP_PORT SMTP服务端口 (默认: 25) + --alert 启用警报 (通过邮件发送) + --interval SECONDS 监控间隔,单位为秒 (默认: 300) + --log 将结果记录到日志文件 + --help 显示帮助信息 +""" + +import os +import sys +import time +import socket +import smtplib +import argparse +import requests +import platform +import json +import logging +import datetime +import subprocess +import psutil +from pathlib import Path +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart + +# 颜色定义(终端输出) +class Colors: + GREEN = '\033[92m' + YELLOW = '\033[93m' + RED = '\033[91m' + BLUE = '\033[94m' + ENDC = '\033[0m' + BOLD = '\033[1m' + +# 配置日志 +def setup_logging(log_enabled): + logger = logging.getLogger('email_system_monitor') + logger.setLevel(logging.INFO) + + # 控制台处理器 + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.INFO) + console_format = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + console_handler.setFormatter(console_format) + logger.addHandler(console_handler) + + # 文件处理器(如果启用) + if log_enabled: + log_dir = Path('logs') + log_dir.mkdir(exist_ok=True) + log_file = log_dir / 'email_system_monitor.log' + file_handler = logging.FileHandler(log_file) + file_handler.setLevel(logging.INFO) + file_format = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + file_handler.setFormatter(file_format) + logger.addHandler(file_handler) + + return logger + +def print_header(title): + """打印格式化的标题""" + print(f"\n{Colors.BLUE}{Colors.BOLD}{'=' * 50}{Colors.ENDC}") + print(f"{Colors.BLUE}{Colors.BOLD}{title.center(50)}{Colors.ENDC}") + print(f"{Colors.BLUE}{Colors.BOLD}{'=' * 50}{Colors.ENDC}\n") + +def print_status(name, status, message=""): + """打印状态信息""" + if status == "OK": + status_color = f"{Colors.GREEN}OK{Colors.ENDC}" + elif status == "WARNING": + status_color = f"{Colors.YELLOW}WARNING{Colors.ENDC}" + else: + status_color = f"{Colors.RED}ERROR{Colors.ENDC}" + + print(f"{name.ljust(30)}: {status_color} {message}") + return status + +def check_api_status(host, port, logger): + """检查API服务状态""" + url = f"http://{host}:{port}/api/status" + try: + response = requests.get(url, timeout=5) + if response.status_code == 200: + data = response.json() + logger.info(f"API服务正常运行 - 状态: {data.get('status', 'OK')}") + return "OK", data + else: + logger.warning(f"API服务返回非正常状态码: {response.status_code}") + return "WARNING", {"message": f"状态码: {response.status_code}"} + except requests.RequestException as e: + logger.error(f"API服务连接失败: {str(e)}") + return "ERROR", {"message": f"连接错误: {str(e)}"} + +def check_smtp_status(host, port, logger): + """检查SMTP服务状态""" + try: + with socket.create_connection((host, port), timeout=5) as sock: + # 接收欢迎消息 + data = sock.recv(1024).decode('utf-8') + if data.startswith('220'): + # 发送EHLO命令 + sock.sendall(b'EHLO example.com\r\n') + response = sock.recv(1024).decode('utf-8') + + logger.info("SMTP服务正常运行") + return "OK", {"message": "SMTP服务响应正常", "response": response.split('\n')[0]} + else: + logger.warning(f"SMTP服务返回非标准欢迎消息: {data}") + return "WARNING", {"message": f"非标准欢迎消息: {data}"} + except (socket.timeout, socket.error, ConnectionRefusedError) as e: + logger.error(f"SMTP服务连接失败: {str(e)}") + return "ERROR", {"message": f"连接错误: {str(e)}"} + +def check_disk_usage(logger): + """检查磁盘使用情况""" + try: + # 获取当前目录磁盘使用情况 + disk = psutil.disk_usage('.') + + # 计算使用百分比 + used_percent = disk.percent + + if used_percent < 70: + status = "OK" + elif used_percent < 90: + status = "WARNING" + else: + status = "ERROR" + + # 格式化大小 + total_gb = disk.total / (1024**3) + used_gb = disk.used / (1024**3) + free_gb = disk.free / (1024**3) + + message = f"使用率: {used_percent:.1f}% (已用: {used_gb:.1f}GB, 可用: {free_gb:.1f}GB, 总计: {total_gb:.1f}GB)" + logger.info(f"磁盘使用情况: {message}") + + return status, { + "used_percent": used_percent, + "used_gb": used_gb, + "free_gb": free_gb, + "total_gb": total_gb, + "message": message + } + except Exception as e: + logger.error(f"检查磁盘使用情况时出错: {str(e)}") + return "ERROR", {"message": f"错误: {str(e)}"} + +def check_email_data_size(logger): + """检查邮件数据目录大小""" + try: + email_data_path = Path('email_data') + if not email_data_path.exists(): + logger.warning("邮件数据目录不存在") + return "WARNING", {"message": "邮件数据目录不存在"} + + # 计算目录大小 + total_size = 0 + for path in email_data_path.rglob('*'): + if path.is_file(): + total_size += path.stat().st_size + + # 转换为MB + size_mb = total_size / (1024**2) + + if size_mb < 100: + status = "OK" + elif size_mb < 500: + status = "WARNING" + else: + status = "ERROR" + + message = f"邮件数据大小: {size_mb:.2f}MB" + logger.info(message) + + return status, { + "size_mb": size_mb, + "message": message + } + except Exception as e: + logger.error(f"检查邮件数据大小时出错: {str(e)}") + return "ERROR", {"message": f"错误: {str(e)}"} + +def check_system_resources(logger): + """检查系统资源使用情况""" + try: + # CPU使用率 + cpu_percent = psutil.cpu_percent(interval=1) + + # 内存使用情况 + memory = psutil.virtual_memory() + memory_percent = memory.percent + + # 系统负载 + if platform.system() != 'Windows': + load1, load5, load15 = os.getloadavg() + load_status = "OK" + + # 检查系统负载 + cpu_count = psutil.cpu_count() + if load5 > cpu_count * 0.7: + load_status = "WARNING" + if load5 > cpu_count: + load_status = "ERROR" + + load_message = f"系统负载: {load1:.2f}, {load5:.2f}, {load15:.2f} (1, 5, 15 min)" + else: + load_status = "OK" + load1, load5, load15 = 0, 0, 0 + load_message = "Windows系统不支持负载检查" + + # CPU状态 + if cpu_percent < 70: + cpu_status = "OK" + elif cpu_percent < 90: + cpu_status = "WARNING" + else: + cpu_status = "ERROR" + + # 内存状态 + if memory_percent < 70: + memory_status = "OK" + elif memory_percent < 90: + memory_status = "WARNING" + else: + memory_status = "ERROR" + + # 综合状态 + if cpu_status == "ERROR" or memory_status == "ERROR" or load_status == "ERROR": + status = "ERROR" + elif cpu_status == "WARNING" or memory_status == "WARNING" or load_status == "WARNING": + status = "WARNING" + else: + status = "OK" + + message = ( + f"CPU使用率: {cpu_percent:.1f}%, " + f"内存使用率: {memory_percent:.1f}% " + f"({memory.used / (1024**3):.1f}GB/{memory.total / (1024**3):.1f}GB)" + ) + + logger.info(f"系统资源: {message}") + logger.info(load_message) + + return status, { + "cpu_percent": cpu_percent, + "memory_percent": memory_percent, + "memory_used_gb": memory.used / (1024**3), + "memory_total_gb": memory.total / (1024**3), + "load1": load1, + "load5": load5, + "load15": load15, + "message": message, + "load_message": load_message + } + except Exception as e: + logger.error(f"检查系统资源时出错: {str(e)}") + return "ERROR", {"message": f"错误: {str(e)}"} + +def check_log_files(logger): + """检查日志文件大小和错误情况""" + try: + log_dir = Path('logs') + if not log_dir.exists(): + logger.warning("日志目录不存在") + return "WARNING", {"message": "日志目录不存在"} + + log_files = list(log_dir.glob('*.log')) + if not log_files: + logger.warning("未找到日志文件") + return "WARNING", {"message": "未找到日志文件"} + + total_size = 0 + error_count = 0 + large_files = [] + + # 检查最近的错误日志 + for log_file in log_files: + # 检查文件大小 + size_mb = log_file.stat().st_size / (1024**2) + total_size += size_mb + + if size_mb > 100: + large_files.append((log_file.name, size_mb)) + + # 检查文件中的错误 + try: + # 只检查最后100行 + if log_file.exists(): + with log_file.open('r', encoding='utf-8', errors='ignore') as f: + # 使用逆序读取最后100行来寻找错误 + lines = f.readlines()[-100:] if len(lines := f.readlines()) > 100 else lines + for line in lines: + if "ERROR" in line or "Exception" in line or "Error" in line: + error_count += 1 + except Exception as e: + logger.error(f"读取日志文件 {log_file} 时出错: {str(e)}") + + # 确定状态 + if error_count > 10 or len(large_files) > 2: + status = "ERROR" + elif error_count > 0 or large_files: + status = "WARNING" + else: + status = "OK" + + message = f"日志总大小: {total_size:.2f}MB, 最近错误数: {error_count}" + if large_files: + message += f", 大文件: {', '.join([f'{name} ({size:.1f}MB)' for name, size in large_files])}" + + logger.info(f"日志文件状态: {message}") + + return status, { + "total_size_mb": total_size, + "error_count": error_count, + "large_files": large_files, + "message": message + } + except Exception as e: + logger.error(f"检查日志文件时出错: {str(e)}") + return "ERROR", {"message": f"错误: {str(e)}"} + +def check_process_status(logger): + """检查关联进程状态""" + try: + python_processes = [] + for proc in psutil.process_iter(['pid', 'name', 'cmdline', 'cpu_percent', 'memory_percent', 'create_time']): + try: + # 过滤出Python进程,并且命令行中包含run.py + if 'python' in proc.info['name'].lower() and proc.info['cmdline'] and any('run.py' in cmd for cmd in proc.info['cmdline']): + # 计算运行时间 + uptime = datetime.datetime.now() - datetime.datetime.fromtimestamp(proc.info['create_time']) + + # 获取CPU和内存使用情况 + cpu_percent = proc.info['cpu_percent'] + memory_percent = proc.info['memory_percent'] + + python_processes.append({ + 'pid': proc.info['pid'], + 'cmdline': ' '.join(proc.info['cmdline']), + 'cpu_percent': cpu_percent, + 'memory_percent': memory_percent, + 'uptime': str(uptime).split('.')[0] # 去除微秒 + }) + except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): + pass + + if not python_processes: + logger.warning("未找到邮件系统相关进程") + return "WARNING", {"message": "未找到邮件系统相关进程"} + + status = "OK" + message = f"找到 {len(python_processes)} 个邮件系统相关进程" + logger.info(message) + + # 检查是否有进程使用过高的资源 + high_resource_procs = [] + for proc in python_processes: + if proc['cpu_percent'] > 50 or proc['memory_percent'] > 30: + high_resource_procs.append(proc) + status = "WARNING" + + if high_resource_procs: + logger.warning(f"发现 {len(high_resource_procs)} 个进程使用较高资源") + + return status, { + "processes": python_processes, + "high_resource": high_resource_procs, + "message": message + } + except Exception as e: + logger.error(f"检查进程状态时出错: {str(e)}") + return "ERROR", {"message": f"错误: {str(e)}"} + +def send_alert_email(subject, message, to_email, from_email, smtp_host, smtp_port, logger): + """发送警报邮件""" + try: + msg = MIMEMultipart() + msg['Subject'] = subject + msg['From'] = from_email + msg['To'] = to_email + + # 添加HTML内容 + html_content = f""" + + + + + +
+

邮件系统监控警报

+
+
+ {message} +
+ + + + """ + + msg.attach(MIMEText(html_content, 'html')) + + # 连接SMTP服务器并发送 + with smtplib.SMTP(smtp_host, smtp_port) as server: + server.sendmail(from_email, to_email, msg.as_string()) + + logger.info(f"成功发送警报邮件到 {to_email}") + return True + except Exception as e: + logger.error(f"发送警报邮件失败: {str(e)}") + return False + +def format_duration(seconds): + """格式化持续时间""" + days, remainder = divmod(seconds, 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + + parts = [] + if days > 0: + parts.append(f"{int(days)}天") + if hours > 0 or parts: + parts.append(f"{int(hours)}小时") + if minutes > 0 or parts: + parts.append(f"{int(minutes)}分钟") + if seconds > 0 or not parts: + parts.append(f"{int(seconds)}秒") + + return "".join(parts) + +def generate_report(results, host, api_port, smtp_port, start_time, logger): + """生成监控报告""" + end_time = time.time() + duration = end_time - start_time + + # 计算总体状态 + overall_status = "OK" + for check_name, (status, _) in results.items(): + if status == "ERROR": + overall_status = "ERROR" + break + elif status == "WARNING" and overall_status != "ERROR": + overall_status = "WARNING" + + # 创建报告 + report = { + "timestamp": datetime.datetime.now().isoformat(), + "host": host, + "api_port": api_port, + "smtp_port": smtp_port, + "duration": f"{duration:.2f}秒", + "overall_status": overall_status, + "checks": {} + } + + # 添加检查结果 + for check_name, (status, data) in results.items(): + report["checks"][check_name] = { + "status": status, + "data": data + } + + # 保存报告到文件 + report_dir = Path('logs') + report_dir.mkdir(exist_ok=True) + report_file = report_dir / f"monitor_report_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.json" + + try: + with open(report_file, 'w') as f: + json.dump(report, f, indent=2, ensure_ascii=False) + logger.info(f"监控报告已保存到 {report_file}") + except Exception as e: + logger.error(f"保存监控报告失败: {str(e)}") + + return report, overall_status + +def format_html_report(report): + """格式化HTML报告""" + status_colors = { + "OK": "green", + "WARNING": "orange", + "ERROR": "red" + } + + overall_color = status_colors.get(report["overall_status"], "black") + + html = f""" +

邮件系统监控报告

+

时间: {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}

+

主机: {report["host"]}

+

总体状态: {report["overall_status"]}

+ +

检查结果:

+ + + + + + + """ + + for check_name, check_data in report["checks"].items(): + status = check_data["status"] + color = status_colors.get(status, "black") + + # 格式化消息 + if "message" in check_data["data"]: + message = check_data["data"]["message"] + else: + # 尝试提取有用的信息 + message_parts = [] + for key, value in check_data["data"].items(): + if key != "message" and not isinstance(value, dict) and not isinstance(value, list): + message_parts.append(f"{key}: {value}") + message = ", ".join(message_parts) + + html += f""" + + + + + + """ + + html += """ +
检查项状态详情
{check_name}{status}{message}
+ """ + + return html + +def main(): + parser = argparse.ArgumentParser(description='邮件系统监控脚本') + parser.add_argument('--host', default='localhost', help='服务器主机名或IP地址') + parser.add_argument('--api-port', type=int, default=5000, help='API服务端口') + parser.add_argument('--smtp-port', type=int, default=25, help='SMTP服务端口') + parser.add_argument('--alert', action='store_true', help='启用警报') + parser.add_argument('--interval', type=int, default=300, help='监控间隔,单位为秒') + parser.add_argument('--log', action='store_true', help='将结果记录到日志文件') + parser.add_argument('--daemon', action='store_true', help='以守护进程模式运行') + args = parser.parse_args() + + # 设置日志 + logger = setup_logging(args.log) + + # 警报设置 + alert_settings = { + 'to_email': 'admin@example.com', + 'from_email': 'monitor@example.com', + 'smtp_host': 'localhost', + 'smtp_port': args.smtp_port + } + + # 打印监控信息 + logger.info("邮件系统监控启动") + logger.info(f"主机: {args.host}, API端口: {args.api_port}, SMTP端口: {args.smtp_port}") + if args.daemon: + logger.info(f"守护进程模式,间隔: {args.interval}秒") + + # 记录启动次数 + run_count = 0 + + # 循环运行(如果是守护进程模式) + while True: + run_count += 1 + start_time = time.time() + + try: + if not args.daemon or run_count == 1: + print_header("邮件系统监控") + + # 执行检查 + results = {} + + # 检查API状态 + api_status, api_data = check_api_status(args.host, args.api_port, logger) + results["API服务"] = (api_status, api_data) + if not args.daemon or run_count == 1: + print_status("API服务", api_status, api_data.get("message", "")) + + # 检查SMTP状态 + smtp_status, smtp_data = check_smtp_status(args.host, args.smtp_port, logger) + results["SMTP服务"] = (smtp_status, smtp_data) + if not args.daemon or run_count == 1: + print_status("SMTP服务", smtp_status, smtp_data.get("message", "")) + + # 检查磁盘使用情况 + disk_status, disk_data = check_disk_usage(logger) + results["磁盘使用情况"] = (disk_status, disk_data) + if not args.daemon or run_count == 1: + print_status("磁盘使用情况", disk_status, disk_data.get("message", "")) + + # 检查邮件数据大小 + email_data_status, email_data_info = check_email_data_size(logger) + results["邮件数据"] = (email_data_status, email_data_info) + if not args.daemon or run_count == 1: + print_status("邮件数据", email_data_status, email_data_info.get("message", "")) + + # 检查系统资源 + resources_status, resources_data = check_system_resources(logger) + results["系统资源"] = (resources_status, resources_data) + if not args.daemon or run_count == 1: + print_status("系统资源", resources_status, resources_data.get("message", "")) + if platform.system() != 'Windows': + print(f" {resources_data.get('load_message', '')}") + + # 检查日志文件 + logs_status, logs_data = check_log_files(logger) + results["日志文件"] = (logs_status, logs_data) + if not args.daemon or run_count == 1: + print_status("日志文件", logs_status, logs_data.get("message", "")) + + # 检查进程状态 + process_status, process_data = check_process_status(logger) + results["进程状态"] = (process_status, process_data) + if not args.daemon or run_count == 1: + print_status("进程状态", process_status, process_data.get("message", "")) + + # 显示进程详情 + if process_data.get("processes"): + print("\n进程详情:") + for i, proc in enumerate(process_data["processes"]): + print(f" 进程 {i+1}: PID {proc['pid']}, CPU {proc['cpu_percent']:.1f}%, " + f"内存 {proc['memory_percent']:.1f}%, 运行时间 {proc['uptime']}") + + # 生成报告 + report, overall_status = generate_report(results, args.host, args.api_port, args.smtp_port, start_time, logger) + + # 显示总体状态 + if not args.daemon or run_count == 1: + print("\n" + "-" * 50) + status_msg = print_status("总体状态", overall_status) + print("-" * 50) + + # 发送警报邮件(如果启用) + if args.alert and (overall_status == "WARNING" or overall_status == "ERROR"): + logger.warning(f"发现问题,正在发送警报邮件...") + + # 格式化HTML报告 + html_report = format_html_report(report) + + # 发送邮件 + subject = f"[{'警告' if overall_status == 'WARNING' else '错误'}] 邮件系统监控报告" + send_alert_email( + subject, + html_report, + alert_settings['to_email'], + alert_settings['from_email'], + alert_settings['smtp_host'], + alert_settings['smtp_port'], + logger + ) + + # 如果是守护进程模式,等待下一次检查 + if args.daemon: + execution_time = time.time() - start_time + sleep_time = max(1, args.interval - execution_time) + + if run_count == 1: + print(f"\n守护进程模式已启动,间隔 {args.interval} 秒") + print(f"下次检查将在 {format_duration(sleep_time)} 后进行...") + + time.sleep(sleep_time) + else: + break + + except KeyboardInterrupt: + logger.info("监控被用户中断") + print("\n监控已停止") + break + except Exception as e: + logger.error(f"监控过程中发生错误: {str(e)}") + if not args.daemon: + raise + time.sleep(args.interval) + +if __name__ == "__main__": + main() diff --git a/performance_test.py b/performance_test.py new file mode 100644 index 0000000..bf7621b --- /dev/null +++ b/performance_test.py @@ -0,0 +1,504 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +邮箱系统性能测试脚本 (轻量版) +用于测试系统基本功能的响应时间和资源使用情况 +""" + +import requests +import smtplib +import time +import datetime +import random +import string +import argparse +import psutil +import threading +import json +import os +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from concurrent.futures import ThreadPoolExecutor + +# 颜色输出 +class Colors: + GREEN = '\033[92m' + YELLOW = '\033[93m' + RED = '\033[91m' + BLUE = '\033[94m' + CYAN = '\033[96m' + BOLD = '\033[1m' + ENDC = '\033[0m' + +def print_header(title): + """打印格式化的标题""" + print(f"\n{Colors.BLUE}{Colors.BOLD}{'=' * 60}{Colors.ENDC}") + print(f"{Colors.BLUE}{Colors.BOLD}{title.center(60)}{Colors.ENDC}") + print(f"{Colors.BLUE}{Colors.BOLD}{'=' * 60}{Colors.ENDC}\n") + +def print_info(message): + """打印信息消息""" + print(f"{Colors.CYAN}[INFO] {message}{Colors.ENDC}") + +def print_success(message): + """打印成功消息""" + print(f"{Colors.GREEN}[SUCCESS] {message}{Colors.ENDC}") + +def print_warning(message): + """打印警告消息""" + print(f"{Colors.YELLOW}[WARNING] {message}{Colors.ENDC}") + +def print_error(message): + """打印错误消息""" + print(f"{Colors.RED}[ERROR] {message}{Colors.ENDC}") + +def generate_random_string(length=8): + """生成随机字符串""" + return ''.join(random.choices(string.ascii_letters + string.digits, k=length)) + +def monitor_resources(duration=10, interval=1): + """监控系统资源使用""" + print_info(f"开始监控系统资源使用 ({duration}秒)") + + # 初始化结果数据 + samples = [] + start_time = time.time() + end_time = start_time + duration + + # 收集样本 + while time.time() < end_time: + # 获取当前进程 + current_process = psutil.Process(os.getpid()) + + # 系统CPU和内存 + cpu_percent = psutil.cpu_percent(interval=0.1) + memory = psutil.virtual_memory() + + # 当前进程的CPU和内存 + process_cpu = current_process.cpu_percent(interval=0.1) + process_memory = current_process.memory_info().rss / 1024 / 1024 # MB + + # 记录样本 + sample = { + 'timestamp': time.time() - start_time, + 'system_cpu': cpu_percent, + 'system_memory': memory.percent, + 'process_cpu': process_cpu, + 'process_memory': process_memory + } + samples.append(sample) + + # 等待下一个间隔 + time.sleep(interval) + + # 计算平均值 + if samples: + avg_system_cpu = sum(s['system_cpu'] for s in samples) / len(samples) + avg_system_memory = sum(s['system_memory'] for s in samples) / len(samples) + avg_process_cpu = sum(s['process_cpu'] for s in samples) / len(samples) + avg_process_memory = sum(s['process_memory'] for s in samples) / len(samples) + + print_info(f"资源监控完成,平均值:") + print(f" 系统CPU使用率: {avg_system_cpu:.1f}%") + print(f" 系统内存使用率: {avg_system_memory:.1f}%") + print(f" 进程CPU使用率: {avg_process_cpu:.1f}%") + print(f" 进程内存使用: {avg_process_memory:.1f}MB") + + return { + 'samples': samples, + 'averages': { + 'system_cpu': avg_system_cpu, + 'system_memory': avg_system_memory, + 'process_cpu': avg_process_cpu, + 'process_memory': avg_process_memory + } + } + + return None + +def test_api_responsiveness(api_base_url="http://localhost:5000/api", num_requests=10): + """测试API响应性能""" + print_info(f"开始测试API响应性能 ({num_requests}个请求)") + + endpoints = [ + '/status', + '/domains', + '/mailboxes' + ] + + results = {} + for endpoint in endpoints: + url = f"{api_base_url}{endpoint}" + response_times = [] + success_count = 0 + + print_info(f"测试端点: {endpoint}") + for i in range(num_requests): + try: + start_time = time.time() + response = requests.get(url, timeout=5) + end_time = time.time() + + response_time = (end_time - start_time) * 1000 # 毫秒 + response_times.append(response_time) + + if response.status_code == 200: + success_count += 1 + print(f" 请求 {i+1}/{num_requests}: {response_time:.2f}ms (成功)") + else: + print(f" 请求 {i+1}/{num_requests}: {response_time:.2f}ms (失败: {response.status_code})") + except Exception as e: + print(f" 请求 {i+1}/{num_requests}: 失败 ({str(e)})") + + # 计算结果 + if response_times: + avg_time = sum(response_times) / len(response_times) + min_time = min(response_times) + max_time = max(response_times) + success_rate = (success_count / num_requests) * 100 + + results[endpoint] = { + 'avg_time': avg_time, + 'min_time': min_time, + 'max_time': max_time, + 'success_rate': success_rate + } + + print(f" 结果: 平均 {avg_time:.2f}ms, 最小 {min_time:.2f}ms, 最大 {max_time:.2f}ms, 成功率 {success_rate:.1f}%") + else: + results[endpoint] = { + 'error': '所有请求失败' + } + print_error(" 所有请求失败") + + return results + +def send_test_email(smtp_host, smtp_port, from_addr, to_addr): + """发送测试邮件并计时""" + message_id = f"{generate_random_string()}@test.local" + subject = f"测试邮件 {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + + # 创建邮件 + msg = MIMEMultipart() + msg['From'] = from_addr + msg['To'] = to_addr + msg['Subject'] = subject + msg['Message-ID'] = message_id + + # 添加文本内容 + body = f""" + 这是一封测试邮件。 + + 发送时间: {datetime.datetime.now().isoformat()} + 唯一ID: {message_id} + + 此邮件用于测试系统性能。 + """ + msg.attach(MIMEText(body, 'plain')) + + try: + # 连接SMTP服务器 + start_time = time.time() + server = smtplib.SMTP(smtp_host, smtp_port) + + # 发送邮件 + server.sendmail(from_addr, to_addr, msg.as_string()) + + # 关闭连接 + server.quit() + end_time = time.time() + + # 计算响应时间 + response_time = (end_time - start_time) * 1000 # 毫秒 + + return { + 'success': True, + 'response_time': response_time, + 'message_id': message_id, + 'subject': subject + } + except Exception as e: + return { + 'success': False, + 'error': str(e) + } + +def test_email_sending(smtp_host="localhost", smtp_port=3825, num_emails=5, concurrent=False): + """测试邮件发送性能""" + from_addr = "sender@example.com" + to_addr = "recipient@example.com" + + print_info(f"开始测试邮件发送性能 ({num_emails}封邮件, {'并发' if concurrent else '顺序'})") + + results = [] + start_time = time.time() + + if concurrent: + # 并发发送 + with ThreadPoolExecutor(max_workers=min(num_emails, 10)) as executor: + futures = [] + for i in range(num_emails): + futures.append(executor.submit( + send_test_email, smtp_host, smtp_port, from_addr, to_addr + )) + + for i, future in enumerate(futures): + result = future.result() + results.append(result) + + if result['success']: + print(f" 邮件 {i+1}/{num_emails}: {result['response_time']:.2f}ms (成功)") + else: + print(f" 邮件 {i+1}/{num_emails}: 失败 ({result['error']})") + else: + # 顺序发送 + for i in range(num_emails): + result = send_test_email(smtp_host, smtp_port, from_addr, to_addr) + results.append(result) + + if result['success']: + print(f" 邮件 {i+1}/{num_emails}: {result['response_time']:.2f}ms (成功)") + else: + print(f" 邮件 {i+1}/{num_emails}: 失败 ({result['error']})") + + end_time = time.time() + total_time = (end_time - start_time) * 1000 # 毫秒 + + # 计算结果 + success_count = sum(1 for r in results if r['success']) + success_rate = (success_count / num_emails) * 100 + + if success_count > 0: + response_times = [r['response_time'] for r in results if r['success']] + avg_time = sum(response_times) / len(response_times) + min_time = min(response_times) + max_time = max(response_times) + + print_info(f"邮件发送测试完成:") + print(f" 成功率: {success_rate:.1f}% ({success_count}/{num_emails})") + print(f" 平均响应时间: {avg_time:.2f}ms") + print(f" 最小响应时间: {min_time:.2f}ms") + print(f" 最大响应时间: {max_time:.2f}ms") + print(f" 总执行时间: {total_time:.2f}ms") + + return { + 'success_rate': success_rate, + 'avg_time': avg_time, + 'min_time': min_time, + 'max_time': max_time, + 'total_time': total_time, + 'results': results + } + else: + print_error("所有邮件发送失败") + return { + 'success_rate': 0, + 'error': '所有发送失败', + 'results': results + } + +def test_creating_mailboxes(api_base_url="http://localhost:5000/api", domain_id=1, num_mailboxes=5, concurrent=False): + """测试创建邮箱的性能""" + print_info(f"开始测试创建邮箱性能 ({num_mailboxes}个邮箱, {'并发' if concurrent else '顺序'})") + + results = [] + start_time = time.time() + + def create_mailbox(index): + """创建单个邮箱""" + address = f"testuser{generate_random_string()}_{index}" + data = { + "domain_id": domain_id, + "address": address + } + + try: + start_time = time.time() + response = requests.post(f"{api_base_url}/mailboxes", json=data, timeout=5) + end_time = time.time() + + response_time = (end_time - start_time) * 1000 # 毫秒 + + return { + 'success': response.status_code in (200, 201), + 'status_code': response.status_code, + 'response_time': response_time, + 'address': address, + 'response': response.json() if response.status_code in (200, 201) else None + } + except Exception as e: + return { + 'success': False, + 'error': str(e), + 'address': address + } + + if concurrent: + # 并发创建 + with ThreadPoolExecutor(max_workers=min(num_mailboxes, 10)) as executor: + futures = [] + for i in range(num_mailboxes): + futures.append(executor.submit(create_mailbox, i)) + + for i, future in enumerate(futures): + result = future.result() + results.append(result) + + if result['success']: + print(f" 邮箱 {i+1}/{num_mailboxes}: {result['response_time']:.2f}ms (成功)") + else: + error_msg = result.get('error', f"状态码: {result.get('status_code')}") + print(f" 邮箱 {i+1}/{num_mailboxes}: 失败 ({error_msg})") + else: + # 顺序创建 + for i in range(num_mailboxes): + result = create_mailbox(i) + results.append(result) + + if result['success']: + print(f" 邮箱 {i+1}/{num_mailboxes}: {result['response_time']:.2f}ms (成功)") + else: + error_msg = result.get('error', f"状态码: {result.get('status_code')}") + print(f" 邮箱 {i+1}/{num_mailboxes}: 失败 ({error_msg})") + + end_time = time.time() + total_time = (end_time - start_time) * 1000 # 毫秒 + + # 计算结果 + success_count = sum(1 for r in results if r['success']) + success_rate = (success_count / num_mailboxes) * 100 + + if success_count > 0: + response_times = [r['response_time'] for r in results if r['success']] + avg_time = sum(response_times) / len(response_times) + min_time = min(response_times) + max_time = max(response_times) + + print_info(f"创建邮箱测试完成:") + print(f" 成功率: {success_rate:.1f}% ({success_count}/{num_mailboxes})") + print(f" 平均响应时间: {avg_time:.2f}ms") + print(f" 最小响应时间: {min_time:.2f}ms") + print(f" 最大响应时间: {max_time:.2f}ms") + print(f" 总执行时间: {total_time:.2f}ms") + + return { + 'success_rate': success_rate, + 'avg_time': avg_time, + 'min_time': min_time, + 'max_time': max_time, + 'total_time': total_time, + 'results': results + } + else: + print_error("所有邮箱创建失败") + return { + 'success_rate': 0, + 'error': '所有创建失败', + 'results': results + } + +def save_results(results, filename="performance_results.json"): + """保存测试结果到文件""" + try: + with open(filename, 'w') as f: + json.dump(results, f, indent=2) + print_success(f"结果已保存到 {filename}") + return True + except Exception as e: + print_error(f"保存结果失败: {str(e)}") + return False + +def run_all_tests(args): + """运行所有测试""" + results = { + 'timestamp': datetime.datetime.now().isoformat(), + 'config': vars(args), + 'tests': {} + } + + # 测试API响应 + print_header("API响应性能测试") + api_results = test_api_responsiveness( + api_base_url=args.api_url, + num_requests=args.api_requests + ) + results['tests']['api_responsiveness'] = api_results + + # 测试邮件发送 + print_header("邮件发送性能测试") + email_results = test_email_sending( + smtp_host=args.smtp_host, + smtp_port=args.smtp_port, + num_emails=args.email_count, + concurrent=args.concurrent + ) + results['tests']['email_sending'] = email_results + + # 测试创建邮箱 + if not args.skip_mailbox: + print_header("创建邮箱性能测试") + mailbox_results = test_creating_mailboxes( + api_base_url=args.api_url, + domain_id=args.domain_id, + num_mailboxes=args.mailbox_count, + concurrent=args.concurrent + ) + results['tests']['mailbox_creation'] = mailbox_results + + # 监控资源使用 + print_header("系统资源监控") + resource_results = monitor_resources( + duration=args.monitor_duration, + interval=1 + ) + results['tests']['resources'] = resource_results + + # 保存结果 + if args.save: + save_results(results, args.output) + + return results + +def main(): + parser = argparse.ArgumentParser(description='邮箱系统性能测试脚本 (轻量版)') + parser.add_argument('--api-url', default='http://localhost:5000/api', help='API基础URL') + parser.add_argument('--smtp-host', default='localhost', help='SMTP服务器主机') + parser.add_argument('--smtp-port', type=int, default=3825, help='SMTP服务器端口') + parser.add_argument('--domain-id', type=int, default=1, help='用于创建邮箱的域名ID') + parser.add_argument('--api-requests', type=int, default=10, help='API测试请求数量') + parser.add_argument('--email-count', type=int, default=5, help='测试邮件数量') + parser.add_argument('--mailbox-count', type=int, default=5, help='测试创建的邮箱数量') + parser.add_argument('--monitor-duration', type=int, default=10, help='资源监控持续时间(秒)') + parser.add_argument('--concurrent', action='store_true', help='启用并发测试') + parser.add_argument('--skip-mailbox', action='store_true', help='跳过邮箱创建测试') + parser.add_argument('--save', action='store_true', help='保存测试结果') + parser.add_argument('--output', default='performance_results.json', help='结果输出文件') + args = parser.parse_args() + + print_header("邮箱系统性能测试 (轻量版)") + print_info("测试配置:") + print(f" API URL: {args.api_url}") + print(f" SMTP服务器: {args.smtp_host}:{args.smtp_port}") + print(f" 域名ID: {args.domain_id}") + print(f" API请求数: {args.api_requests}") + print(f" 邮件数量: {args.email_count}") + print(f" 邮箱数量: {args.mailbox_count}") + print(f" 并发模式: {'是' if args.concurrent else '否'}") + print(f" 资源监控时间: {args.monitor_duration}秒") + + try: + run_all_tests(args) + print_header("测试完成") + return 0 + except KeyboardInterrupt: + print_warning("\n测试被用户中断") + return 1 + except Exception as e: + print_error(f"\n测试过程中出错: {str(e)}") + import traceback + traceback.print_exc() + return 1 + +if __name__ == "__main__": + exit(main()) diff --git a/production_local_settings.py b/production_local_settings.py new file mode 100644 index 0000000..4701072 --- /dev/null +++ b/production_local_settings.py @@ -0,0 +1,104 @@ +""" +生产环境配置文件 +此文件包含在生产环境中运行邮箱系统的优化设置 +部署时请重命名为local_settings.py +""" +import platform +import os +import logging +from logging.handlers import RotatingFileHandler +import multiprocessing + +# 基础配置 +IS_PRODUCTION = True +LOG_LEVEL = 'INFO' # 可选: DEBUG, INFO, WARNING, ERROR, CRITICAL + +# SMTP服务器配置 +SMTP_HOST = '0.0.0.0' # 监听所有网络接口 +SMTP_PORT = 25 # 标准SMTP端口 + +# 如果ISP阻止了25端口,可以使用备用端口 +# SMTP_PORT = 587 # 备用端口 + +# 性能优化 +# 自动根据CPU核心数调整工作线程数 +CPU_COUNT = multiprocessing.cpu_count() +WORKER_THREADS = max(2, min(CPU_COUNT * 2, 8)) # 至少2个线程,最多8个线程 + +# 邮件处理优化 +MAX_EMAIL_SIZE = 25 * 1024 * 1024 # 25MB,单个邮件最大大小 +EMAIL_BATCH_SIZE = 50 # 批处理邮件数量 +EMAIL_RETENTION_DAYS = 30 # 邮件保留天数,超过此天数的邮件将被自动清理 + +# 存储路径配置 +MAIL_STORAGE_PATH = os.path.join(os.getcwd(), 'email_data') +LOG_FILE = os.path.join(os.getcwd(), 'logs', 'email_system.log') + +# 安全设置 +ENABLE_RATE_LIMITING = True # 启用API请求限流 +API_RATE_LIMIT = '120/minute' # 每分钟最大请求数 +ENABLE_IP_WHITELIST = False # 是否启用IP白名单 +IP_WHITELIST = ['127.0.0.1'] # 允许访问API的IP列表 + +# 垃圾邮件过滤设置 +ENABLE_SPAM_FILTER = True # 启用简单的垃圾邮件过滤 +SPAM_KEYWORDS = ['casino', 'lottery', 'viagra', 'prescription', 'winner', 'prize', 'bitcoin'] +MAX_RECIPIENTS_PER_EMAIL = 50 # 单封邮件最大收件人数量 + +# 邮件域名白名单(只接收这些域名的邮件,空列表表示接收所有) +DOMAIN_WHITELIST = [] + +# 日志配置 +def setup_logging(): + """配置系统日志""" + log_dir = os.path.dirname(LOG_FILE) + if not os.path.exists(log_dir): + os.makedirs(log_dir) + + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + + # 文件处理器 - 使用循环日志文件,最大10MB,保留10个备份 + file_handler = RotatingFileHandler( + LOG_FILE, maxBytes=10*1024*1024, backupCount=10 + ) + file_handler.setFormatter(formatter) + file_handler.setLevel(getattr(logging, LOG_LEVEL)) + + # 控制台处理器 + console_handler = logging.StreamHandler() + console_handler.setFormatter(formatter) + console_handler.setLevel(getattr(logging, LOG_LEVEL)) + + # 配置根日志记录器 + root_logger = logging.getLogger() + root_logger.setLevel(getattr(logging, LOG_LEVEL)) + root_logger.addHandler(file_handler) + root_logger.addHandler(console_handler) + + logging.info("生产环境日志系统初始化完成") + +# 自动清理功能 +ENABLE_AUTO_CLEANUP = True # 启用自动清理 +CLEANUP_INTERVAL_HOURS = 24 # 每24小时运行一次清理 + +# 监控设置 +ENABLE_MONITORING = True # 启用系统监控 +MONITOR_INTERVAL_MINUTES = 5 # 监控间隔(分钟) + +# 在应用启动时设置日志 +try: + setup_logging() + logging.info("已加载生产环境配置") +except Exception as e: + print(f"日志设置失败: {str(e)}") + +# 系统启动时记录配置信息 +if IS_PRODUCTION: + logging.info(f"SMTP服务器配置: {SMTP_HOST}:{SMTP_PORT}") + logging.info(f"工作线程数: {WORKER_THREADS}") + logging.info(f"邮件存储路径: {MAIL_STORAGE_PATH}") + logging.info(f"日志级别: {LOG_LEVEL}") + + # 创建必要的目录 + os.makedirs(MAIL_STORAGE_PATH, exist_ok=True) + os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..72cb65f Binary files /dev/null and b/requirements.txt differ diff --git a/run.py b/run.py new file mode 100644 index 0000000..fe380a0 --- /dev/null +++ b/run.py @@ -0,0 +1,111 @@ +import os +import argparse +import logging +import platform +from app import create_app +from app.services import SMTPServer, MailStore, EmailProcessor +from app.services import register_smtp_server, register_mail_store, register_email_processor +from app.models import get_session +from flask import Flask + +# 检测是否Windows环境 +IS_WINDOWS = platform.system().lower() == 'windows' + +# 解析命令行参数 +parser = argparse.ArgumentParser(description='启动邮箱系统') +parser.add_argument('--host', type=str, default='0.0.0.0', help='HTTP服务器主机地址') +parser.add_argument('--port', type=int, default=5000, help='HTTP服务器端口') +parser.add_argument('--smtp-host', type=str, help='SMTP服务器主机地址') +parser.add_argument('--smtp-port', type=int, help='SMTP服务器端口') +parser.add_argument('--debug', action='store_true', help='启用调试模式') + +args = parser.parse_args() + +# 创建Flask应用 +app = create_app() + +# Windows环境下的特殊处理 +if IS_WINDOWS: + try: + # 尝试导入本地设置 + import local_settings + if getattr(local_settings, 'SMTP_HOST', None): + app.config['SMTP_HOST'] = local_settings.SMTP_HOST + logging.info(f"使用Windows特殊设置: SMTP_HOST={local_settings.SMTP_HOST}") + if getattr(local_settings, 'SMTP_PORT', None): + app.config['SMTP_PORT'] = local_settings.SMTP_PORT + logging.info(f"使用Windows特殊设置: SMTP_PORT={local_settings.SMTP_PORT}") + except ImportError: + logging.warning("未找到local_settings.py,将使用标准配置") + +# 更新SMTP配置,如果命令行参数中指定了 +if args.smtp_host: + app.config['SMTP_HOST'] = args.smtp_host +if args.smtp_port: + app.config['SMTP_PORT'] = args.smtp_port + +# 初始化服务实例 +mail_store = MailStore(get_session) +smtp_server = SMTPServer( + host=app.config.get('SMTP_HOST', '127.0.0.1' if IS_WINDOWS else '0.0.0.0'), + port=app.config.get('SMTP_PORT', 3825 if IS_WINDOWS else 25), + mail_store=mail_store +) +email_processor = EmailProcessor(mail_store) + +# 注册服务实例到全局对象 +register_mail_store(mail_store) +register_smtp_server(smtp_server) +register_email_processor(email_processor) + +def start_services(): + """启动所有服务""" + logging.info("正在启动所有服务...") + + # 启动邮件处理器 + if email_processor.start(): + logging.info("邮件处理器启动成功") + else: + logging.error("邮件处理器启动失败") + + # 启动SMTP服务器 + if smtp_server.start(): + logging.info(f"SMTP服务器启动成功,监听 {smtp_server.host}:{smtp_server.port}") + else: + logging.error("SMTP服务器启动失败") + + logging.info("所有服务已启动") + +def stop_services(): + """停止所有服务""" + logging.info("正在停止所有服务...") + + # 停止SMTP服务器 + if smtp_server.stop(): + logging.info("SMTP服务器已停止") + + # 停止邮件处理器 + if email_processor.stop(): + logging.info("邮件处理器已停止") + + logging.info("所有服务已停止") + +@app.teardown_appcontext +def teardown_appcontext(exception=None): + """应用上下文销毁时停止所有服务""" + stop_services() + +if __name__ == '__main__': + try: + # 启动服务 + start_services() + + # 启动Web服务器 + app.run( + host=args.host, + port=args.port, + debug=args.debug + ) + finally: + # 确保在退出时停止所有服务 + stop_services() \ No newline at end of file diff --git a/send_verification_email.py b/send_verification_email.py new file mode 100644 index 0000000..2bb6551 --- /dev/null +++ b/send_verification_email.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import smtplib +import random +import string +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from datetime import datetime + +def generate_verification_code(length=6): + """生成随机验证码""" + return ''.join(random.choices(string.digits, k=length)) + +def send_verification_email(smtp_host, smtp_port, to_email): + """发送带验证码的邮件""" + # 生成6位随机验证码 + verification_code = generate_verification_code() + + # 创建邮件 + msg = MIMEMultipart() + msg['From'] = 'noreply@system.com' + msg['To'] = to_email + msg['Subject'] = f'您的验证码 - {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}' + + # 邮件正文 + body = f''' + + +
+

验证码

+

您好!

+

您的验证码是:

+
+ {verification_code} +
+

此验证码将在30分钟内有效。

+

如果这不是您请求的,请忽略此邮件。

+

谢谢!

+
+ 此邮件由系统自动发送,请勿回复。 +
+
+ + +''' + + msg.attach(MIMEText(body, 'html')) + + # 连接SMTP服务器并发送 + try: + server = smtplib.SMTP(smtp_host, smtp_port) + server.set_debuglevel(1) # 开启调试模式 + print(f"连接到SMTP服务器: {smtp_host}:{smtp_port}") + + # 发送邮件 + server.sendmail(msg['From'], msg['To'], msg.as_string()) + print(f"成功发送验证码邮件到: {to_email}") + print(f"验证码: {verification_code}") + + server.quit() + return True, verification_code + except Exception as e: + print(f"发送邮件失败: {str(e)}") + return False, verification_code + +if __name__ == "__main__": + # SMTP服务器设置 + smtp_host = 'localhost' # 本地SMTP服务器 + smtp_port = 3825 # SMTP端口 + + # 收件人邮箱 + to_email = 'testaa@nosqli.com' + + # 发送验证码邮件 + success, code = send_verification_email(smtp_host, smtp_port, to_email) + if success: + print(f"验证码邮件已发送,验证码: {code}") + else: + print("邮件发送失败,请检查SMTP服务器设置") \ No newline at end of file diff --git a/simple_check.py b/simple_check.py new file mode 100644 index 0000000..269dd61 --- /dev/null +++ b/simple_check.py @@ -0,0 +1,35 @@ +import requests + +# 直接查询特定邮箱ID的邮件 +mailbox_id = 2 # testaa@nosqli.com 的ID + +try: + print(f"获取邮箱ID={mailbox_id}的邮件...") + response = requests.get(f"http://localhost:5000/api/mailboxes/{mailbox_id}/emails") + print(f"响应状态: {response.status_code}") + + if response.status_code == 200: + data = response.json() + print("API响应内容:") + print(data) + + if data.get('success', False): + emails = data.get('emails', []) + print(f"\n找到 {len(emails)} 封邮件") + + for email in emails: + print("\n邮件信息:") + print(f"ID: {email.get('id')}") + print(f"主题: {email.get('subject')}") + print(f"发件人: {email.get('sender')}") + print(f"时间: {email.get('received_at')}") + print(f"验证码: {email.get('verification_code')}") + else: + print(f"获取邮件失败: {data.get('error')}") + else: + print(f"API请求失败: {response.text}") + +except Exception as e: + print(f"发生错误: {str(e)}") + import traceback + traceback.print_exc() \ No newline at end of file diff --git a/simple_test.py b/simple_test.py new file mode 100644 index 0000000..0ddfba2 --- /dev/null +++ b/simple_test.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +print("Python环境测试成功!") +print("邮件系统测试脚本") + +# 打印系统信息 +import sys +import platform +import os + +print("\n系统信息:") +print(f"Python版本: {sys.version}") +print(f"操作系统: {platform.system()} {platform.version()}") +print(f"当前目录: {os.getcwd()}") + +# 检查必要的库 +try: + import requests + print("\n导入requests库成功") +except ImportError: + print("\n导入requests库失败") + +try: + import flask + print("导入flask库成功") +except ImportError: + print("导入flask库失败") + +print("\n测试完成!") \ No newline at end of file diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..953911c --- /dev/null +++ b/start.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# 创建必要的目录 +mkdir -p logs email_data db + +# 检查环境变量文件是否存在,不存在则创建 +if [ ! -f .env ]; then + echo "创建.env文件..." + cat > .env << EOF +FLASK_ENV=production +SECRET_KEY=$(openssl rand -hex 16) +MAIL_DOMAINS=example.com,mail.example.com +EOF + echo ".env文件已创建" +fi + +# 初始化数据库 +echo "初始化数据库..." +python -c "from app.models import init_db; init_db()" + +# 启动服务 +echo "启动服务..." +if [ "$1" == "docker" ]; then + # 使用Docker启动 + docker-compose up -d + echo "服务已在Docker中启动" +else + # 直接启动 + echo "直接启动应用..." + # 检查是否以root权限运行(25端口需要) + if [ "$(id -u)" -ne 0 ] && [ "$SMTP_PORT" -lt 1024 ]; then + echo "警告: 在端口 $SMTP_PORT 运行SMTP服务需要root权限" + fi + python run.py --host 0.0.0.0 --port 5000 --smtp-port ${SMTP_PORT:-25} +fi \ No newline at end of file diff --git a/start_windows.ps1 b/start_windows.ps1 new file mode 100644 index 0000000..7f02fd7 --- /dev/null +++ b/start_windows.ps1 @@ -0,0 +1,30 @@ +# 创建必要的目录 +New-Item -ItemType Directory -Path "logs", "email_data", "db" -Force | Out-Null + +# 检查环境变量文件是否存在,不存在则创建 +if (-not (Test-Path ".env")) { + Write-Host "创建.env文件..." + $secretKey = -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 32 | ForEach-Object {[char]$_}) + Set-Content -Path ".env" -Value @" +FLASK_ENV=production +SECRET_KEY=$secretKey +MAIL_DOMAINS=example.com,mail.example.com +"@ + Write-Host ".env文件已创建" +} + +# 初始化数据库 +Write-Host "初始化数据库..." +python -c "from app.models import init_db; init_db()" + +# 启动服务 +Write-Host "启动服务..." +$smtpPort = if ($env:SMTP_PORT) { $env:SMTP_PORT } else { 25 } + +# 检查是否以管理员权限运行(25端口需要) +$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +if (-not $isAdmin -and $smtpPort -lt 1024) { + Write-Host "警告: 在端口 $smtpPort 运行SMTP服务需要管理员权限" -ForegroundColor Yellow +} + +python run.py --host 0.0.0.0 --port 5000 --smtp-port $smtpPort \ No newline at end of file diff --git a/test_core_features.py b/test_core_features.py new file mode 100644 index 0000000..fbaf30b --- /dev/null +++ b/test_core_features.py @@ -0,0 +1,284 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import requests +import smtplib +import time +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +import logging +import sys + +# 配置日志 +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger('CoreFeatureTest') + +# API配置 +API_BASE_URL = "http://localhost:5000/api" +SMTP_HOST = "localhost" +SMTP_PORT = 3825 + +def print_divider(title=""): + """打印分隔线""" + line = "=" * 80 + if title: + print(f"\n{line}\n{title.center(80)}\n{line}\n") + else: + print(f"\n{line}\n") + +def get_all_domains(): + """获取所有域名""" + try: + response = requests.get(f"{API_BASE_URL}/domains") + if response.status_code == 200: + domains = response.json().get('domains', []) + logger.info(f"获取到 {len(domains)} 个域名") + return domains + else: + logger.error(f"获取域名失败: HTTP {response.status_code}") + return [] + except Exception as e: + logger.error(f"获取域名出错: {str(e)}") + return [] + +def create_domain(domain_name, description=""): + """创建新域名""" + try: + data = { + "name": domain_name, + "description": description + } + response = requests.post(f"{API_BASE_URL}/domains", json=data) + if response.status_code in (200, 201): + result = response.json() + domain = result.get("domain", {}) + logger.info(f"成功创建域名: {domain_name} (ID: {domain.get('id')})") + return domain + else: + logger.error(f"创建域名失败: HTTP {response.status_code} - {response.text}") + return None + except Exception as e: + logger.error(f"创建域名出错: {str(e)}") + return None + +def get_all_mailboxes(): + """获取所有邮箱""" + try: + response = requests.get(f"{API_BASE_URL}/mailboxes") + if response.status_code == 200: + mailboxes = response.json().get('mailboxes', []) + logger.info(f"获取到 {len(mailboxes)} 个邮箱") + return mailboxes + else: + logger.error(f"获取邮箱失败: HTTP {response.status_code}") + return [] + except Exception as e: + logger.error(f"获取邮箱出错: {str(e)}") + return [] + +def create_mailbox(domain_id, address): + """创建新邮箱""" + try: + data = { + "domain_id": domain_id, + "address": address + } + response = requests.post(f"{API_BASE_URL}/mailboxes", json=data) + if response.status_code in (200, 201): + result = response.json() + mailbox = result.get("mailbox", {}) + logger.info(f"成功创建邮箱: {mailbox.get('full_address')} (ID: {mailbox.get('id')})") + return mailbox + else: + logger.error(f"创建邮箱失败: HTTP {response.status_code} - {response.text}") + return None + except Exception as e: + logger.error(f"创建邮箱出错: {str(e)}") + return None + +def get_mailbox_emails(mailbox_id): + """获取指定邮箱的所有邮件""" + try: + response = requests.get(f"{API_BASE_URL}/mailboxes/{mailbox_id}/emails") + if response.status_code == 200: + emails = response.json().get('emails', []) + logger.info(f"获取到邮箱ID={mailbox_id}的 {len(emails)} 封邮件") + return emails + else: + logger.error(f"获取邮件失败: HTTP {response.status_code}") + return [] + except Exception as e: + logger.error(f"获取邮件出错: {str(e)}") + return [] + +def send_email(from_addr, to_addr, subject, body_text="", body_html=""): + """发送邮件""" + try: + # 创建邮件 + msg = MIMEMultipart('alternative') + msg['From'] = from_addr + msg['To'] = to_addr + msg['Subject'] = subject + + # 添加文本内容 + if body_text: + msg.attach(MIMEText(body_text, 'plain')) + + # 添加HTML内容 + if body_html: + msg.attach(MIMEText(body_html, 'html')) + + # 连接SMTP服务器并发送 + server = smtplib.SMTP(SMTP_HOST, SMTP_PORT) + server.set_debuglevel(1) # 开启调试 + server.sendmail(from_addr, to_addr, msg.as_string()) + server.quit() + + logger.info(f"成功发送邮件: {from_addr} -> {to_addr}, 主题: {subject}") + return True + except Exception as e: + logger.error(f"发送邮件失败: {str(e)}") + return False + +def test_create_multiple_domains(): + """测试创建多个域名""" + print_divider("测试创建多个域名") + + # 创建3个不同的域名 + domains = [] + test_domains = [ + {"name": "example1.com", "desc": "测试域名1"}, + {"name": "example2.com", "desc": "测试域名2"}, + {"name": "example3.com", "desc": "测试域名3"} + ] + + for domain_info in test_domains: + domain = create_domain(domain_info["name"], domain_info["desc"]) + if domain: + domains.append(domain) + + # 验证域名是否创建成功 + all_domains = get_all_domains() + print(f"\n当前系统中共有 {len(all_domains)} 个域名:") + for domain in all_domains: + print(f" - {domain.get('name')} (ID: {domain.get('id')})") + + return domains + +def test_create_multiple_mailboxes(domains): + """测试为每个域名创建多个邮箱""" + print_divider("测试创建多个邮箱") + + mailboxes = [] + + # 为每个域名创建多个邮箱 + for domain in domains: + domain_id = domain.get('id') + domain_name = domain.get('name') + + print(f"\n为域名 {domain_name} 创建邮箱:") + + # 为每个域名创建2个邮箱 + users = ["user1", "user2"] + for user in users: + mailbox = create_mailbox(domain_id, user) + if mailbox: + mailboxes.append(mailbox) + print(f" - 创建了邮箱: {mailbox.get('full_address')} (ID: {mailbox.get('id')})") + + # 验证邮箱是否创建成功 + all_mailboxes = get_all_mailboxes() + print(f"\n当前系统中共有 {len(all_mailboxes)} 个邮箱:") + for mailbox in all_mailboxes: + print(f" - {mailbox.get('full_address')} (ID: {mailbox.get('id')})") + + return mailboxes + +def test_email_receiving(mailboxes): + """测试邮件接收功能""" + print_divider("测试邮件接收功能") + + if not mailboxes: + print("没有可用的邮箱,无法测试邮件接收功能") + return False + + # 选择第一个邮箱作为发件人 + sender_mailbox = mailboxes[0] + from_addr = sender_mailbox.get('full_address') + + # 发送邮件到其他所有邮箱 + for recipient_mailbox in mailboxes[1:]: + to_addr = recipient_mailbox.get('full_address') + subject = f"测试邮件 - 从 {from_addr} 发送到 {to_addr}" + body_text = f""" + 你好! + + 这是一封测试邮件,发送时间:{time.strftime('%Y-%m-%d %H:%M:%S')} + + 此邮件用于测试邮件系统的接收功能。 + + 来自: {from_addr} + """ + + success = send_email(from_addr, to_addr, subject, body_text) + if success: + print(f"成功发送邮件: {from_addr} -> {to_addr}") + else: + print(f"发送邮件失败: {from_addr} -> {to_addr}") + + # 等待邮件处理 + print("\n等待5秒钟,让系统处理邮件...") + time.sleep(5) + + # 检查每个邮箱是否收到邮件 + print("\n检查邮箱收件情况:") + for mailbox in mailboxes[1:]: # 跳过发件人邮箱 + mailbox_id = mailbox.get('id') + mailbox_address = mailbox.get('full_address') + + emails = get_mailbox_emails(mailbox_id) + if emails: + print(f" - 邮箱 {mailbox_address} 收到了 {len(emails)} 封邮件") + for email in emails: + print(f" * 邮件ID: {email.get('id')}, 主题: {email.get('subject')}") + print(f" 发件人: {email.get('sender')}, 时间: {email.get('received_at')}") + else: + print(f" - 邮箱 {mailbox_address} 没有收到任何邮件") + + return True + +def main(): + try: + print_divider("邮件系统核心功能测试") + print("此脚本将测试以下功能:") + print(" 1. 创建多个不同的域名") + print(" 2. 为每个域名创建多个邮箱") + print(" 3. 测试邮件接收功能") + + # 测试创建多个域名 + domains = test_create_multiple_domains() + + # 测试创建多个邮箱 + if domains: + mailboxes = test_create_multiple_mailboxes(domains) + + # 测试邮件接收功能 + if mailboxes: + test_email_receiving(mailboxes) + else: + print("邮箱创建失败,无法测试邮件接收功能") + else: + print("域名创建失败,无法测试邮箱创建和邮件接收功能") + + print_divider("测试完成") + + except Exception as e: + print(f"测试过程中发生错误: {str(e)}") + import traceback + traceback.print_exc() + return 1 + + return 0 + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/test_smtp.py b/test_smtp.py new file mode 100644 index 0000000..d40ee55 --- /dev/null +++ b/test_smtp.py @@ -0,0 +1,64 @@ +import smtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +import sys + +def send_test_email(host='localhost', port=2525, sender='test@example.com', recipient='user@example.com'): + """发送测试邮件""" + + # 创建邮件内容 + msg = MIMEMultipart() + msg['From'] = sender + msg['To'] = recipient + msg['Subject'] = '测试邮件 - 邮箱系统' + + # 添加正文 + body = """ + 这是一封测试邮件。 + + 邮箱系统测试。 + + 验证码: TEST123 + 验证链接: http://example.com/verify?code=123456 + """ + msg.attach(MIMEText(body, 'plain')) + + try: + # 连接SMTP服务器 + server = smtplib.SMTP(host, port) + server.set_debuglevel(1) # 启用调试 + + # 发送邮件 + server.sendmail(sender, recipient, msg.as_string()) + + # 关闭连接 + server.quit() + + print(f"邮件发送成功: {sender} -> {recipient}") + return True + except Exception as e: + print(f"邮件发送失败: {str(e)}") + return False + +if __name__ == "__main__": + # 获取命令行参数 + host = 'localhost' + port = 2525 + sender = 'test@example.com' + recipient = 'user@example.com' + + # 处理命令行参数 + if len(sys.argv) > 1: + host = sys.argv[1] + if len(sys.argv) > 2: + port = int(sys.argv[2]) + if len(sys.argv) > 3: + sender = sys.argv[3] + if len(sys.argv) > 4: + recipient = sys.argv[4] + + print(f"发送测试邮件到SMTP服务器 {host}:{port}") + print(f"发件人: {sender}") + print(f"收件人: {recipient}") + + send_test_email(host, port, sender, recipient) \ No newline at end of file diff --git a/test_smtp_server.py b/test_smtp_server.py new file mode 100644 index 0000000..38f36eb --- /dev/null +++ b/test_smtp_server.py @@ -0,0 +1,377 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +SMTP服务器测试脚本 +用于验证邮件系统的发送和接收功能 +""" + +import smtplib +import argparse +import socket +import time +import sys +import json +import random +import string +import requests +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from datetime import datetime + +# ANSI颜色代码 +class Colors: + GREEN = '\033[92m' + YELLOW = '\033[93m' + RED = '\033[91m' + BLUE = '\033[94m' + BOLD = '\033[1m' + ENDC = '\033[0m' + +def generate_random_string(length=8): + """生成随机字符串""" + return ''.join(random.choices(string.ascii_letters + string.digits, k=length)) + +def print_step(step, message): + """打印测试步骤""" + print(f"\n{Colors.BLUE}{Colors.BOLD}[步骤 {step}]{Colors.ENDC} {message}") + +def print_success(message): + """打印成功消息""" + print(f"{Colors.GREEN}✓ {message}{Colors.ENDC}") + +def print_warning(message): + """打印警告消息""" + print(f"{Colors.YELLOW}⚠ {message}{Colors.ENDC}") + +def print_error(message): + """打印错误消息""" + print(f"{Colors.RED}✗ {message}{Colors.ENDC}") + +def check_smtp_connection(host, port, timeout=5): + """检查SMTP服务器连接""" + print_step(1, f"检查SMTP服务器连接 ({host}:{port})") + + try: + # 尝试创建TCP连接 + sock = socket.create_connection((host, port), timeout) + banner = sock.recv(1024).decode('utf-8', errors='ignore') + sock.close() + + if banner and banner.startswith('2'): + print_success(f"SMTP服务器连接成功,服务器响应: {banner.strip()}") + return True + else: + print_warning(f"SMTP服务器连接成功,但响应不标准: {banner.strip()}") + return True + except socket.timeout: + print_error(f"连接超时,请检查SMTP服务器是否运行在 {host}:{port}") + return False + except ConnectionRefusedError: + print_error(f"连接被拒绝,请检查SMTP服务器是否运行在 {host}:{port}") + return False + except Exception as e: + print_error(f"连接失败: {str(e)}") + return False + +def check_smtp_handshake(host, port): + """测试SMTP握手过程""" + print_step(2, "测试SMTP协议握手") + + try: + # 创建SMTP连接并打印通信过程 + smtp = smtplib.SMTP(host, port, timeout=10) + smtp.set_debuglevel(1) # 打开调试模式 + + # 发送EHLO命令 + _, ehlo_response = smtp.ehlo() + ehlo_success = all(code == 250 for code, _ in ehlo_response) + + if ehlo_success: + print_success("SMTP握手成功,服务器支持的功能:") + for code, feature in ehlo_response: + print(f" {feature.decode('utf-8')}") + else: + print_warning("SMTP握手部分成功,可能有功能受限") + + smtp.quit() + return ehlo_success + except Exception as e: + print_error(f"SMTP握手失败: {str(e)}") + return False + +def check_api_status(api_base_url): + """检查API服务器状态""" + print_step(3, f"检查API服务器状态 ({api_base_url})") + + try: + response = requests.get(f"{api_base_url}/status", timeout=5) + response.raise_for_status() + data = response.json() + + print_success("API服务器状态正常") + print(f" 状态信息: {json.dumps(data, indent=2)}") + return True + except requests.exceptions.RequestException as e: + print_error(f"API服务器连接失败: {str(e)}") + return False + +def get_test_mailbox(api_base_url): + """获取或创建测试邮箱""" + print_step(4, "获取测试邮箱") + + try: + # 获取所有域名 + response = requests.get(f"{api_base_url}/domains") + response.raise_for_status() + domains = response.json().get("domains", []) + + if not domains: + print_error("没有可用的域名,请先创建域名") + return None + + # 获取第一个域名 + domain = domains[0] + domain_id = domain.get("id") + domain_name = domain.get("name") + + print_success(f"使用域名: {domain_name} (ID: {domain_id})") + + # 生成随机地址前缀 + address_prefix = f"test-{generate_random_string(6)}" + + # 创建新邮箱 + mailbox_data = { + "domain_id": domain_id, + "address": address_prefix + } + + response = requests.post(f"{api_base_url}/mailboxes", json=mailbox_data) + response.raise_for_status() + mailbox = response.json().get("mailbox", {}) + + mailbox_id = mailbox.get("id") + full_address = mailbox.get("full_address") + + if not mailbox_id or not full_address: + print_error("创建邮箱失败,使用现有邮箱") + + # 尝试获取现有邮箱 + response = requests.get(f"{api_base_url}/mailboxes") + response.raise_for_status() + mailboxes = response.json().get("mailboxes", []) + + if not mailboxes: + print_error("没有可用的邮箱") + return None + + mailbox = mailboxes[0] + mailbox_id = mailbox.get("id") + full_address = mailbox.get("full_address") + + print_success(f"使用邮箱: {full_address} (ID: {mailbox_id})") + return {"id": mailbox_id, "address": full_address} + except Exception as e: + print_error(f"获取邮箱失败: {str(e)}") + return None + +def send_test_email(smtp_host, smtp_port, recipient, verbose=False): + """发送测试邮件""" + print_step(5, f"发送测试邮件到 {recipient}") + + # 生成随机验证码 + verification_code = ''.join(random.choices(string.digits, k=6)) + + # 生成随机主题标识 + subject_id = generate_random_string(8) + + try: + # 创建邮件 + msg = MIMEMultipart() + msg['From'] = 'test@example.com' + msg['To'] = recipient + msg['Subject'] = f'测试邮件 {subject_id} - 验证码 {verification_code}' + + # 添加HTML内容 + html_content = f""" + + +
+

邮箱系统测试

+

您好!

+

这是一封测试邮件,用于验证邮箱系统的功能。

+

您的验证码是:

+
+ {verification_code} +
+

此测试邮件发送时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}

+

测试ID: {subject_id}

+

请不要回复此邮件。

+
+ 此邮件由系统自动发送,用于测试目的。 +
+
+ + + """ + + msg.attach(MIMEText(html_content, 'html')) + + # 连接到SMTP服务器并发送 + if verbose: + server = smtplib.SMTP(smtp_host, smtp_port) + server.set_debuglevel(1) # 开启详细输出 + else: + server = smtplib.SMTP(smtp_host, smtp_port) + + server.send_message(msg) + server.quit() + + print_success(f"邮件发送成功!") + print(f" 验证码: {verification_code}") + print(f" 测试ID: {subject_id}") + + return {"verification_code": verification_code, "subject_id": subject_id} + except Exception as e: + print_error(f"邮件发送失败: {str(e)}") + return None + +def check_email_received(api_base_url, mailbox_id, test_data, max_attempts=10, delay=2): + """检查邮件是否被接收""" + print_step(6, "检查邮件接收状态") + + verification_code = test_data.get("verification_code") + subject_id = test_data.get("subject_id") + + print(f"正在查找包含验证码 {verification_code} 和测试ID {subject_id} 的邮件...") + + for attempt in range(max_attempts): + try: + print(f"尝试 {attempt + 1}/{max_attempts}...") + + response = requests.get(f"{api_base_url}/mailboxes/{mailbox_id}/emails") + response.raise_for_status() + + emails = response.json().get("emails", []) + + if not emails: + print_warning("未找到邮件,等待中...") + time.sleep(delay) + continue + + # 查找包含测试ID的邮件 + test_email = None + for email in emails: + subject = email.get("subject", "") + if subject_id in subject: + test_email = email + break + + if test_email: + email_id = test_email.get("id") + received_at = test_email.get("received_at") or test_email.get("created_at") + + print_success(f"找到测试邮件! ID: {email_id}, 接收时间: {received_at}") + + # 获取详细信息 + response = requests.get(f"{api_base_url}/emails/{email_id}") + response.raise_for_status() + email_detail = response.json().get("email", {}) + + # 尝试从邮件中提取验证码 + body_html = email_detail.get("body_html", "") + extracted_code = None + + if verification_code in body_html: + print_success("邮件内容中包含正确的验证码") + extracted_code = verification_code + else: + print_warning("无法在邮件内容中找到验证码") + + return { + "success": True, + "email_id": email_id, + "verification_code": extracted_code, + "received_at": received_at + } + else: + print_warning("未找到包含测试ID的邮件,等待中...") + time.sleep(delay) + except Exception as e: + print_error(f"检查邮件失败: {str(e)}") + time.sleep(delay) + + print_error(f"在 {max_attempts} 次尝试后仍未找到测试邮件") + return {"success": False} + +def main(): + """主函数""" + parser = argparse.ArgumentParser(description='SMTP服务器测试工具') + parser.add_argument('--smtp-host', default='localhost', help='SMTP服务器地址,默认为localhost') + parser.add_argument('--smtp-port', type=int, default=3825, help='SMTP服务器端口,默认为3825') + parser.add_argument('--api-url', default='http://localhost:5000/api', help='API服务器URL,默认为http://localhost:5000/api') + parser.add_argument('--verbose', action='store_true', help='显示详细的SMTP通信过程') + args = parser.parse_args() + + print(f"{Colors.BOLD}邮箱系统SMTP服务器测试{Colors.ENDC}") + print(f"测试时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + print(f"SMTP服务器: {args.smtp_host}:{args.smtp_port}") + print(f"API服务器: {args.api_url}") + + # 检查SMTP连接 + if not check_smtp_connection(args.smtp_host, args.smtp_port): + print_error("SMTP服务器连接测试失败,无法继续后续测试") + return 1 + + # 测试SMTP握手 + if not check_smtp_handshake(args.smtp_host, args.smtp_port): + print_warning("SMTP握手测试失败,但将继续后续测试") + + # 检查API状态 + if not check_api_status(args.api_url): + print_error("API服务器连接测试失败,无法继续后续测试") + return 1 + + # 获取测试邮箱 + mailbox = get_test_mailbox(args.api_url) + if not mailbox: + print_error("无法获取测试邮箱,测试终止") + return 1 + + # 发送测试邮件 + test_data = send_test_email(args.smtp_host, args.smtp_port, mailbox["address"], args.verbose) + if not test_data: + print_error("发送测试邮件失败,测试终止") + return 1 + + # 检查邮件接收 + result = check_email_received(args.api_url, mailbox["id"], test_data) + + # 打印测试结果摘要 + print(f"\n{Colors.BOLD}测试结果摘要{Colors.ENDC}") + if result.get("success"): + print_success("邮件发送和接收测试全部成功!") + print(f" 邮箱: {mailbox['address']}") + print(f" 邮件ID: {result.get('email_id')}") + print(f" 验证码: {test_data.get('verification_code')}") + print(f" 接收时间: {result.get('received_at')}") + print(f"\n{Colors.GREEN}{Colors.BOLD}恭喜!您的邮箱系统运行正常,可以进行部署。{Colors.ENDC}") + return 0 + else: + print_error("测试失败: 发送的邮件未被接收") + print(f" 邮箱: {mailbox['address']}") + print(f" 验证码: {test_data.get('verification_code')}") + print(f"\n{Colors.RED}{Colors.BOLD}请检查系统配置并解决问题后再次测试。{Colors.ENDC}") + return 1 + +if __name__ == "__main__": + try: + sys.exit(main()) + except KeyboardInterrupt: + print("\n测试被用户中断") + sys.exit(130) + except Exception as e: + print(f"\n{Colors.RED}测试过程中出现未处理的异常: {str(e)}{Colors.ENDC}") + import traceback + traceback.print_exc() + sys.exit(1) \ No newline at end of file