From fdd79fbba838f3ea46b9b77cd5817866b05b67a0 Mon Sep 17 00:00:00 2001 From: clumxc <2088043998@qq.com> Date: Mon, 15 Dec 2025 15:11:33 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=AF=BC=E8=88=AA=E6=A0=8F?= =?UTF-8?q?=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 920 +++++++++++------------ config/database.php | 42 +- docker-compose.yml | 86 +-- logs/pop3.log | 6 + logs/smtp.log | 6 + public/admin.php | 113 --- public/broadcast.php | 387 +++++----- public/emails.php | 478 ++++++------ public/filters.php | 488 ++++++------ public/help.php | 382 +++++----- public/index.php | 435 ++++++----- public/index_test.php | 428 +++++++++++ public/logout.php | 36 +- public/logs.php | 513 ++++++------- public/register.php | 576 +++++++------- public/services.php | 455 +++++++---- public/settings.php | 641 ++++++++-------- public/users.php | 596 +++++++-------- reset_db.sh | 22 +- scripts/create_all_tables.sql | 270 +++---- scripts/manage_user.php | 196 ----- scripts/start_pop3.php | 34 +- scripts/start_smtp.php | 34 +- scripts/test_register.php | 212 +++--- src/admin/BroadcastService.php | 300 ++++---- src/protocol/Pop3Handler.php | 0 src/protocol/Pop3Server.php | 616 +++++++-------- src/protocol/SmtpHandler.php | 220 ------ src/protocol/SmtpServer.php | 898 +++++++++++----------- src/storage/EmailRepository.php | 352 ++++----- src/storage/FilterRepository.php | 228 +++--- src/storage/MailboxRepository.php | 168 ++--- src/storage/ServiceRepository.php | 158 ++-- src/storage/SystemSettingsRepository.php | 112 +-- src/storage/UserRepository.php | 346 ++++----- src/utils/Security.php | 258 +++---- src/utils/Validator.php | 314 ++++---- test_smtp.sh | 58 +- 38 files changed, 5710 insertions(+), 5674 deletions(-) delete mode 100644 public/admin.php create mode 100644 public/index_test.php delete mode 100644 scripts/manage_user.php delete mode 100644 src/protocol/Pop3Handler.php delete mode 100644 src/protocol/SmtpHandler.php diff --git a/README.md b/README.md index 2ea27a4..ee9b8b5 100644 --- a/README.md +++ b/README.md @@ -1,460 +1,460 @@ -# 邮件服务器项目 - -基于POP3和SMTP协议的邮件服务端实现 - -## 环境要求 - -- Docker & Docker Compose -- PHP 7.4+ (需要扩展: php-mysql, php-sockets) -- WSL2 (Windows环境) - -## 快速开始 - -### 1. 安装PHP扩展(如果未安装) - -```bash -sudo apt update -sudo apt install php php-cli php-mysql php-sockets -y -``` - -### 2. 启动数据库 - -```bash -cd /mnt/d/mailserver/mailserver - -# 首次启动或重置数据库 -docker-compose down -v -docker-compose up -d - -# 等待10-15秒让数据库初始化完成 -sleep 15 -``` - -### 3. 初始化管理功能数据库表(首次使用) - -```bash -# 执行管理功能相关的数据库表创建 -docker-compose exec mysql mysql -umail_user -puser123 mail_server < scripts/create_admin_tables.sql -``` - -### 4. 查看数据库(phpMyAdmin) - -- 访问:http://localhost:8088 -- 登录信息: - - 服务器:`mysql`(或留空) - - 用户名:`root` - - 密码:`root123` - -### 5. 测试账号 - -- 管理员:`admin@test.com` / `123456` -- 普通用户:`user1@test.com` / `123456` - -## 端口说明 - -- **25** - SMTP服务器(发送邮件) -- **110** - POP3服务器(接收邮件) -- **3308** - MySQL数据库 -- **8080** - Web管理后台 -- **8088** - phpMyAdmin管理界面 - -## 启动服务器 - -### SMTP服务器(发送邮件) - -```bash -sudo php scripts/start_smtp.php -``` - -### POP3服务器(接收邮件) - -```bash -sudo php scripts/start_pop3.php -``` - -**注意**:两个服务器需要分别在两个终端运行,都需要sudo权限(因为使用25和110端口) - -## Web管理后台 - -### 启动Web服务器 - -**方式1(推荐):从public目录启动** -```bash -cd /mnt/d/mailserver/mailserver/public -php -S localhost:8080 -``` - -**方式2:从项目根目录启动** -```bash -cd /mnt/d/mailserver/mailserver -php -S localhost:8080 -t public -``` - -### 访问管理后台 - -- 访问:http://localhost:8080 -- 登录账号: - - 管理员:`admin@test.com` / `123456` - - 普通用户:`user1@test.com` / `123456` - -### 功能模块 - -#### 1. 用户注册 -- **页面**:`register.php` -- **功能**:新用户注册,邮箱域名限制为 @test.com - -#### 2. 用户管理(管理员) -- **页面**:`users.php` -- **功能**: - - 创建新用户(设置密码、管理员权限、激活状态) - - 编辑用户信息(修改密码、权限、状态) - - 删除用户账号 - - 查看用户列表 - -#### 3. 邮件管理 -- **页面**:`emails.php` -- **功能**: - - 查看邮件(管理员查看全部,普通用户查看自己的收件箱) - - 查看邮件详情 - - 标记邮件为已读 - - 删除邮件 - - 分页浏览 - -#### 4. 群发邮件(管理员) -- **页面**:`broadcast.php` -- **功能**: - - 发送给所有用户 - - 发送给指定用户列表 - - 自定义邮件主题和内容 - -#### 5. 过滤规则 -- **页面**:`filters.php` -- **功能**: - - 创建邮箱过滤规则(阻止/允许特定邮箱) - - 创建IP地址过滤规则(阻止/允许特定IP) - - 启用/禁用过滤规则 - - 删除过滤规则 - -#### 6. 系统设置(管理员) -- **页面**:`settings.php` -- **功能**: - - 设置SMTP端口(默认25) - - 设置POP3端口(默认110) - - 设置服务器域名(默认test.com) - - 设置用户邮箱大小限制 - - 设置日志存储路径和最大大小 - - 修改管理员密码 - -#### 7. 服务管理(管理员) -- **页面**:`services.php` -- **功能**: - - 查看SMTP服务状态 - - 查看POP3服务状态 - - 启动/停止服务(状态管理) - -#### 8. 日志管理 -- **页面**:`logs.php` -- **功能**: - - 查看所有日志 - - 按类型过滤(SMTP/POP3) - - 查看日志统计信息 - - 清除日志(管理员) - -#### 9. 帮助 -- **页面**:`help.php` -- **功能**:提供系统使用帮助文档 - -## 测试方法 - -### 测试SMTP(发送邮件) - -**终端1:启动SMTP服务器** -```bash -sudo php scripts/start_smtp.php -``` - -**终端2:连接测试** -```bash -telnet localhost 25 -``` - -**输入命令:** -``` -HELO test -MAIL FROM: -RCPT TO: -DATA -Subject: 测试邮件 -From: user1@test.com -To: admin@test.com - -这是一封测试邮件! -. -QUIT -``` - -### 测试POP3(接收邮件) - -**终端1:启动POP3服务器** -```bash -sudo php scripts/start_pop3.php -``` - -**终端2:连接测试** -```bash -telnet localhost 110 -``` - -**输入命令:** -``` -USER admin@test.com -PASS 123456 -STAT -LIST -RETR 1 -QUIT -``` - -### 测试Web管理后台 - -1. **用户注册测试** - - 访问:http://localhost:8080/register.php - - 注册新用户:`newuser@test.com` / `123456` - - 预期:注册成功,跳转到登录页 - -2. **用户管理测试(管理员)** - - 登录管理员账号 - - 访问:http://localhost:8080/users.php - - 创建、编辑、删除用户 - -3. **群发邮件测试(管理员)** - - 访问:http://localhost:8080/broadcast.php - - 选择"发送给所有用户"或"发送给指定用户" - - 填写主题和内容,发送 - - 预期:显示成功发送数量 - -4. **系统设置测试(管理员)** - - 访问:http://localhost:8080/settings.php - - 修改端口、域名、邮箱大小等设置 - - 修改管理员密码 - -5. **过滤规则测试** - - 访问:http://localhost:8080/filters.php - - 创建邮箱过滤规则和IP过滤规则 - - 测试启用/禁用功能 - -6. **日志管理测试** - - 访问:http://localhost:8080/logs.php - - 查看日志列表,按类型过滤 - - 测试清除日志功能(管理员) - -## 查看数据 - -### 方法1:phpMyAdmin(推荐) -访问 http://localhost:8088,选择 `mail_server` 数据库 - -### 方法2:命令行 -```bash -# 查看用户 -docker-compose exec mysql mysql -umail_user -puser123 mail_server -e "SELECT * FROM users;" - -# 查看邮件 -docker-compose exec mysql mysql -umail_user -puser123 mail_server -e "SELECT id, sender, recipient, subject, created_at FROM emails ORDER BY id DESC;" -``` - -## 重置数据库 - -```bash -docker-compose down -v -docker-compose up -d -sleep 15 -# 重新执行初始化脚本 -docker-compose exec mysql mysql -umail_user -puser123 mail_server < scripts/create_admin_tables.sql -``` - -## 常见问题 - -### 端口被占用 - -**SMTP服务器启动失败(25端口)** -```bash -# 检查端口占用 -sudo netstat -tlnp | grep 25 - -# 或使用 -sudo lsof -i :25 -``` - -**POP3服务器启动失败(110端口)** -```bash -sudo netstat -tlnp | grep 110 -``` - -### 数据库连接失败 - -```bash -# 检查Docker容器状态 -docker-compose ps - -# 查看数据库日志 -docker-compose logs mysql - -# 重启数据库 -docker-compose restart mysql -``` - -### telnet连接失败 - -```bash -# 安装telnet(如果未安装) -sudo apt install telnet - -# 或使用nc替代 -nc localhost 25 -``` - -### 密码验证失败 - -- 确认使用正确的测试账号:`admin@test.com` / `123456` -- 如果重置了数据库,密码会恢复为 `123456` - -## 项目结构 - -``` -mailserver/ -├── scripts/ # 启动脚本和SQL -│ ├── start_smtp.php # 启动SMTP服务器 -│ ├── start_pop3.php # 启动POP3服务器 -│ ├── create_tables.sql # 数据库初始化脚本 -│ └── create_admin_tables.sql # 管理功能数据库表 -├── src/ # 源代码 -│ ├── protocol/ # SMTP/POP3协议实现 -│ │ ├── SmtpServer.php -│ │ └── Pop3Server.php -│ ├── storage/ # 数据存储层 -│ │ ├── Database.php -│ │ ├── UserRepository.php -│ │ ├── EmailRepository.php -│ │ ├── SystemSettingsRepository.php -│ │ ├── FilterRepository.php -│ │ ├── ServiceRepository.php -│ │ └── MailboxRepository.php -│ ├── admin/ # 管理后台逻辑 -│ │ └── BroadcastService.php -│ └── utils/ # 工具类 -│ ├── Security.php -│ └── Validator.php -├── public/ # Web管理界面 -│ ├── index.php # 主页面(登录+仪表盘) -│ ├── register.php # 用户注册 -│ ├── logout.php # 退出登录 -│ ├── users.php # 用户管理 -│ ├── emails.php # 邮件管理 -│ ├── broadcast.php # 群发邮件 -│ ├── filters.php # 过滤规则 -│ ├── settings.php # 系统设置 -│ ├── services.php # 服务管理 -│ ├── logs.php # 日志管理 -│ └── help.php # 帮助 -├── config/ # 配置文件 -│ ├── database.php # 数据库配置 -│ └── constants.php # 常量定义 -└── docker-compose.yml # Docker配置 -``` - -## 功能完成情况 - -### ✅ 服务器端功能(已完成) - -根据课程设计说明书要求,服务器端功能已全部实现: - -1. **✅ 邮箱管理** - - 设置用户邮箱大小限制 - - 查看用户邮箱使用情况 - -2. **✅ 客户管理** - - 创建新客户账号和密码 - - 设置用户权限(管理员/普通用户) - - 启用/禁用用户 - - 删除客户账号 - - 编辑用户信息 - -3. **✅ 服务起停** - - SMTP服务状态管理 - - POP3服务状态管理 - - 服务启动/停止控制 - -4. **✅ 系统设置** - - SMTP端口设置(默认25) - - POP3端口设置(默认110) - - 服务器域名设置(默认test.com) - - 管理员密码修改 - - 邮件过滤(账号过滤) - - IP地址过滤 - -5. **✅ 日志管理** - - SMTP日志查看 - - POP3日志查看 - - 日志清除功能 - - 日志存储位置设置 - - 日志文件大小管理 - -6. **✅ 日常管理** - - 群发邮件功能(发送给所有用户或指定用户) - -7. **✅ 帮助** - - 系统使用帮助文档 - -### ❌ 移动客户端功能(未完成) - -根据课程设计说明书要求,Android移动客户端尚未实现: - -1. **❌ 邮件的操作** - - 邮件的发送 - - 邮件的接收 - - 邮件的删除 - -2. **❌ 用户管理** - - 用户修改自己邮箱的账户密码 - - 新用户注册功能 - -3. **❌ 管理员管理** - - 管理员远程登录 - - 客户端用户管理(创建、删除、授权、消权、禁用) - -## 简要操作指南 - -### 初始化项目 - -```bash -# 1. 启动数据库 -cd /mnt/d/mailserver/mailserver -docker-compose up -d -sleep 15 - -# 2. 初始化管理功能数据库表 -docker-compose exec mysql mysql -umail_user -puser123 mail_server < scripts/create_admin_tables.sql - -# 3. 启动Web服务器 -php -S localhost:8080 -t public -``` - -### 日常使用 - -```bash -# 启动SMTP服务器(终端1) -sudo php scripts/start_smtp.php - -# 启动POP3服务器(终端2) -sudo php scripts/start_pop3.php - -# 启动Web管理后台(终端3) -php -S localhost:8080 -t public -``` - -### 访问地址 - -- **Web管理后台**:http://localhost:8080 -- **phpMyAdmin**:http://localhost:8088 -- **SMTP服务器**:localhost:25 -- **POP3服务器**:localhost:110 +# 邮件服务器项目 + +基于POP3和SMTP协议的邮件服务端实现 + +## 环境要求 + +- Docker & Docker Compose +- PHP 7.4+ (需要扩展: php-mysql, php-sockets) +- WSL2 (Windows环境) + +## 快速开始 + +### 1. 安装PHP扩展(如果未安装) + +```bash +sudo apt update +sudo apt install php php-cli php-mysql php-sockets -y +``` + +### 2. 启动数据库 + +```bash +cd /mnt/d/mailserver/mailserver + +# 首次启动或重置数据库 +docker-compose down -v +docker-compose up -d + +# 等待10-15秒让数据库初始化完成 +sleep 15 +``` + +### 3. 初始化管理功能数据库表(首次使用) + +```bash +# 执行管理功能相关的数据库表创建 +docker-compose exec mysql mysql -umail_user -puser123 mail_server < scripts/create_admin_tables.sql +``` + +### 4. 查看数据库(phpMyAdmin) + +- 访问:http://localhost:8088 +- 登录信息: + - 服务器:`mysql`(或留空) + - 用户名:`root` + - 密码:`root123` + +### 5. 测试账号 + +- 管理员:`admin@test.com` / `123456` +- 普通用户:`user1@test.com` / `123456` + +## 端口说明 + +- **25** - SMTP服务器(发送邮件) +- **110** - POP3服务器(接收邮件) +- **3308** - MySQL数据库 +- **8080** - Web管理后台 +- **8088** - phpMyAdmin管理界面 + +## 启动服务器 + +### SMTP服务器(发送邮件) + +```bash +sudo php scripts/start_smtp.php +``` + +### POP3服务器(接收邮件) + +```bash +sudo php scripts/start_pop3.php +``` + +**注意**:两个服务器需要分别在两个终端运行,都需要sudo权限(因为使用25和110端口) + +## Web管理后台 + +### 启动Web服务器 + +**方式1(推荐):从public目录启动** +```bash +cd /mnt/d/mailserver/mailserver/public +php -S localhost:8080 +``` + +**方式2:从项目根目录启动** +```bash +cd /mnt/d/mailserver/mailserver +php -S localhost:8080 -t public +``` + +### 访问管理后台 + +- 访问:http://localhost:8080 +- 登录账号: + - 管理员:`admin@test.com` / `123456` + - 普通用户:`user1@test.com` / `123456` + +### 功能模块 + +#### 1. 用户注册 +- **页面**:`register.php` +- **功能**:新用户注册,邮箱域名限制为 @test.com + +#### 2. 用户管理(管理员) +- **页面**:`users.php` +- **功能**: + - 创建新用户(设置密码、管理员权限、激活状态) + - 编辑用户信息(修改密码、权限、状态) + - 删除用户账号 + - 查看用户列表 + +#### 3. 邮件管理 +- **页面**:`emails.php` +- **功能**: + - 查看邮件(管理员查看全部,普通用户查看自己的收件箱) + - 查看邮件详情 + - 标记邮件为已读 + - 删除邮件 + - 分页浏览 + +#### 4. 群发邮件(管理员) +- **页面**:`broadcast.php` +- **功能**: + - 发送给所有用户 + - 发送给指定用户列表 + - 自定义邮件主题和内容 + +#### 5. 过滤规则 +- **页面**:`filters.php` +- **功能**: + - 创建邮箱过滤规则(阻止/允许特定邮箱) + - 创建IP地址过滤规则(阻止/允许特定IP) + - 启用/禁用过滤规则 + - 删除过滤规则 + +#### 6. 系统设置(管理员) +- **页面**:`settings.php` +- **功能**: + - 设置SMTP端口(默认25) + - 设置POP3端口(默认110) + - 设置服务器域名(默认test.com) + - 设置用户邮箱大小限制 + - 设置日志存储路径和最大大小 + - 修改管理员密码 + +#### 7. 服务管理(管理员) +- **页面**:`services.php` +- **功能**: + - 查看SMTP服务状态 + - 查看POP3服务状态 + - 启动/停止服务(状态管理) + +#### 8. 日志管理 +- **页面**:`logs.php` +- **功能**: + - 查看所有日志 + - 按类型过滤(SMTP/POP3) + - 查看日志统计信息 + - 清除日志(管理员) + +#### 9. 帮助 +- **页面**:`help.php` +- **功能**:提供系统使用帮助文档 + +## 测试方法 + +### 测试SMTP(发送邮件) + +**终端1:启动SMTP服务器** +```bash +sudo php scripts/start_smtp.php +``` + +**终端2:连接测试** +```bash +telnet localhost 25 +``` + +**输入命令:** +``` +HELO test +MAIL FROM: +RCPT TO: +DATA +Subject: 测试邮件 +From: user1@test.com +To: admin@test.com + +这是一封测试邮件! +. +QUIT +``` + +### 测试POP3(接收邮件) + +**终端1:启动POP3服务器** +```bash +sudo php scripts/start_pop3.php +``` + +**终端2:连接测试** +```bash +telnet localhost 110 +``` + +**输入命令:** +``` +USER admin@test.com +PASS 123456 +STAT +LIST +RETR 1 +QUIT +``` + +### 测试Web管理后台 + +1. **用户注册测试** + - 访问:http://localhost:8080/register.php + - 注册新用户:`newuser@test.com` / `123456` + - 预期:注册成功,跳转到登录页 + +2. **用户管理测试(管理员)** + - 登录管理员账号 + - 访问:http://localhost:8080/users.php + - 创建、编辑、删除用户 + +3. **群发邮件测试(管理员)** + - 访问:http://localhost:8080/broadcast.php + - 选择"发送给所有用户"或"发送给指定用户" + - 填写主题和内容,发送 + - 预期:显示成功发送数量 + +4. **系统设置测试(管理员)** + - 访问:http://localhost:8080/settings.php + - 修改端口、域名、邮箱大小等设置 + - 修改管理员密码 + +5. **过滤规则测试** + - 访问:http://localhost:8080/filters.php + - 创建邮箱过滤规则和IP过滤规则 + - 测试启用/禁用功能 + +6. **日志管理测试** + - 访问:http://localhost:8080/logs.php + - 查看日志列表,按类型过滤 + - 测试清除日志功能(管理员) + +## 查看数据 + +### 方法1:phpMyAdmin(推荐) +访问 http://localhost:8088,选择 `mail_server` 数据库 + +### 方法2:命令行 +```bash +# 查看用户 +docker-compose exec mysql mysql -umail_user -puser123 mail_server -e "SELECT * FROM users;" + +# 查看邮件 +docker-compose exec mysql mysql -umail_user -puser123 mail_server -e "SELECT id, sender, recipient, subject, created_at FROM emails ORDER BY id DESC;" +``` + +## 重置数据库 + +```bash +docker-compose down -v +docker-compose up -d +sleep 15 +# 重新执行初始化脚本 +docker-compose exec mysql mysql -umail_user -puser123 mail_server < scripts/create_admin_tables.sql +``` + +## 常见问题 + +### 端口被占用 + +**SMTP服务器启动失败(25端口)** +```bash +# 检查端口占用 +sudo netstat -tlnp | grep 25 + +# 或使用 +sudo lsof -i :25 +``` + +**POP3服务器启动失败(110端口)** +```bash +sudo netstat -tlnp | grep 110 +``` + +### 数据库连接失败 + +```bash +# 检查Docker容器状态 +docker-compose ps + +# 查看数据库日志 +docker-compose logs mysql + +# 重启数据库 +docker-compose restart mysql +``` + +### telnet连接失败 + +```bash +# 安装telnet(如果未安装) +sudo apt install telnet + +# 或使用nc替代 +nc localhost 25 +``` + +### 密码验证失败 + +- 确认使用正确的测试账号:`admin@test.com` / `123456` +- 如果重置了数据库,密码会恢复为 `123456` + +## 项目结构 + +``` +mailserver/ +├── scripts/ # 启动脚本和SQL +│ ├── start_smtp.php # 启动SMTP服务器 +│ ├── start_pop3.php # 启动POP3服务器 +│ ├── create_tables.sql # 数据库初始化脚本 +│ └── create_admin_tables.sql # 管理功能数据库表 +├── src/ # 源代码 +│ ├── protocol/ # SMTP/POP3协议实现 +│ │ ├── SmtpServer.php +│ │ └── Pop3Server.php +│ ├── storage/ # 数据存储层 +│ │ ├── Database.php +│ │ ├── UserRepository.php +│ │ ├── EmailRepository.php +│ │ ├── SystemSettingsRepository.php +│ │ ├── FilterRepository.php +│ │ ├── ServiceRepository.php +│ │ └── MailboxRepository.php +│ ├── admin/ # 管理后台逻辑 +│ │ └── BroadcastService.php +│ └── utils/ # 工具类 +│ ├── Security.php +│ └── Validator.php +├── public/ # Web管理界面 +│ ├── index.php # 主页面(登录+仪表盘) +│ ├── register.php # 用户注册 +│ ├── logout.php # 退出登录 +│ ├── users.php # 用户管理 +│ ├── emails.php # 邮件管理 +│ ├── broadcast.php # 群发邮件 +│ ├── filters.php # 过滤规则 +│ ├── settings.php # 系统设置 +│ ├── services.php # 服务管理 +│ ├── logs.php # 日志管理 +│ └── help.php # 帮助 +├── config/ # 配置文件 +│ ├── database.php # 数据库配置 +│ └── constants.php # 常量定义 +└── docker-compose.yml # Docker配置 +``` + +## 功能完成情况 + +### ✅ 服务器端功能(已完成) + +根据课程设计说明书要求,服务器端功能已全部实现: + +1. **✅ 邮箱管理** + - 设置用户邮箱大小限制 + - 查看用户邮箱使用情况 + +2. **✅ 客户管理** + - 创建新客户账号和密码 + - 设置用户权限(管理员/普通用户) + - 启用/禁用用户 + - 删除客户账号 + - 编辑用户信息 + +3. **✅ 服务起停** + - SMTP服务状态管理 + - POP3服务状态管理 + - 服务启动/停止控制 + +4. **✅ 系统设置** + - SMTP端口设置(默认25) + - POP3端口设置(默认110) + - 服务器域名设置(默认test.com) + - 管理员密码修改 + - 邮件过滤(账号过滤) + - IP地址过滤 + +5. **✅ 日志管理** + - SMTP日志查看 + - POP3日志查看 + - 日志清除功能 + - 日志存储位置设置 + - 日志文件大小管理 + +6. **✅ 日常管理** + - 群发邮件功能(发送给所有用户或指定用户) + +7. **✅ 帮助** + - 系统使用帮助文档 + +### ❌ 移动客户端功能(未完成) + +根据课程设计说明书要求,Android移动客户端尚未实现: + +1. **❌ 邮件的操作** + - 邮件的发送 + - 邮件的接收 + - 邮件的删除 + +2. **❌ 用户管理** + - 用户修改自己邮箱的账户密码 + - 新用户注册功能 + +3. **❌ 管理员管理** + - 管理员远程登录 + - 客户端用户管理(创建、删除、授权、消权、禁用) + +## 简要操作指南 + +### 初始化项目 + +```bash +# 1. 启动数据库 +cd /mnt/d/mailserver/mailserver +docker-compose up -d +sleep 15 + +# 2. 初始化管理功能数据库表 +docker-compose exec mysql mysql -umail_user -puser123 mail_server < scripts/create_admin_tables.sql + +# 3. 启动Web服务器 +php -S localhost:8080 -t public +``` + +### 日常使用 + +```bash +# 启动SMTP服务器(终端1) +sudo php scripts/start_smtp.php + +# 启动POP3服务器(终端2) +sudo php scripts/start_pop3.php + +# 启动Web管理后台(终端3) +php -S localhost:8080 -t public +``` + +### 访问地址 + +- **Web管理后台**:http://localhost:8080 +- **phpMyAdmin**:http://localhost:8088 +- **SMTP服务器**:localhost:25 +- **POP3服务器**:localhost:110 diff --git a/config/database.php b/config/database.php index ce4835f..2ae4aef 100644 --- a/config/database.php +++ b/config/database.php @@ -1,21 +1,21 @@ - getenv('DB_HOST') ?: '127.0.0.1', // 使用127.0.0.1连接映射端口 - 'port' => getenv('DB_PORT') ?: '3308', // Docker映射端口 - 'database' => getenv('DB_DATABASE') ?: 'mail_server', - 'username' => getenv('DB_USERNAME') ?: 'mail_user', - 'password' => getenv('DB_PASSWORD') ?: 'user123', - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - 'options' => [ - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, - PDO::ATTR_EMULATE_PREPARES => false, - PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci" - ] -]; + getenv('DB_HOST') ?: '127.0.0.1', // 使用127.0.0.1连接映射端口 + 'port' => getenv('DB_PORT') ?: '3308', // Docker映射端口 + 'database' => getenv('DB_DATABASE') ?: 'mail_server', + 'username' => getenv('DB_USERNAME') ?: 'mail_user', + 'password' => getenv('DB_PASSWORD') ?: 'user123', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'options' => [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci" + ] +]; diff --git a/docker-compose.yml b/docker-compose.yml index c33b659..af38d51 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,44 +1,44 @@ - -services: - mysql: - image: mysql:8.0 - container_name: mail-mysql - restart: unless-stopped - environment: - MYSQL_ROOT_PASSWORD: root123 - MYSQL_DATABASE: mail_server - MYSQL_USER: mail_user - MYSQL_PASSWORD: user123 - ports: - - "3308:3306" - volumes: - - mysql-data:/var/lib/mysql - - ./scripts/create_all_tables.sql:/docker-entrypoint-initdb.d/init.sql - command: - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci - --default-authentication-plugin=mysql_native_password - healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-proot123"] - interval: 5s - timeout: 3s - retries: 10 - - phpmyadmin: - image: phpmyadmin/phpmyadmin - container_name: mail-phpmyadmin - restart: unless-stopped - environment: - PMA_HOST: mysql - PMA_PORT: 3306 - PMA_USER: root - PMA_PASSWORD: root123 - UPLOAD_LIMIT: 64M - ports: - - "8088:80" - depends_on: - mysql: - condition: service_healthy - -volumes: + +services: + mysql: + image: mysql:8.0 + container_name: mail-mysql + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: root123 + MYSQL_DATABASE: mail_server + MYSQL_USER: mail_user + MYSQL_PASSWORD: user123 + ports: + - "3308:3306" + volumes: + - mysql-data:/var/lib/mysql + - ./scripts/create_all_tables.sql:/docker-entrypoint-initdb.d/init.sql + command: + --character-set-server=utf8mb4 + --collation-server=utf8mb4_unicode_ci + --default-authentication-plugin=mysql_native_password + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-proot123"] + interval: 5s + timeout: 3s + retries: 10 + + phpmyadmin: + image: phpmyadmin/phpmyadmin + container_name: mail-phpmyadmin + restart: unless-stopped + environment: + PMA_HOST: mysql + PMA_PORT: 3306 + PMA_USER: root + PMA_PASSWORD: root123 + UPLOAD_LIMIT: 64M + ports: + - "8088:80" + depends_on: + mysql: + condition: service_healthy + +volumes: mysql-data: \ No newline at end of file diff --git a/logs/pop3.log b/logs/pop3.log index e69de29..b7e6455 100644 --- a/logs/pop3.log +++ b/logs/pop3.log @@ -0,0 +1,6 @@ +启动最简POP3邮件服务器 +============================== + +错误:需要管理员权限监听110端口 +请使用:sudo php /home/clumxc/projects/mailserver/scripts/start_pop3.php +或者使用其他端口(需要修改代码) diff --git a/logs/smtp.log b/logs/smtp.log index e69de29..49b9ab7 100644 --- a/logs/smtp.log +++ b/logs/smtp.log @@ -0,0 +1,6 @@ +启动最简SMTP邮件服务器 +============================== + +错误:需要管理员权限监听25端口 +请使用:sudo php /home/clumxc/projects/mailserver/scripts/start_smtp.php +或者使用其他端口(需要修改代码) diff --git a/public/admin.php b/public/admin.php deleted file mode 100644 index 8ee3fd8..0000000 --- a/public/admin.php +++ /dev/null @@ -1,113 +0,0 @@ -create($_POST['username'], $_POST['password'], - isset($_POST['is_admin']), $_POST['mailbox_size'])) { - $message = '用户创建成功'; - } - break; - case 'toggle': - $repo->toggleActive($_POST['user_id'], $_POST['active']); - break; - case 'delete': - $repo->delete($_POST['user_id']); - break; - } -} - -$users = $repo->getAll(); -?> - - - - 用户管理 - - - -

邮件服务器用户管理

- - -
- - -

创建新用户

-
- -
- -
-
- -
-
- -
-
- MB -
- -
- -

用户列表

- - - - - - - - - - - - - - - - - - - -
ID邮箱类型状态容量创建时间操作
MB -
- - - - -
-
- - - -
-
- - \ No newline at end of file diff --git a/public/broadcast.php b/public/broadcast.php index 12b3ce7..6b9d038 100644 --- a/public/broadcast.php +++ b/public/broadcast.php @@ -1,193 +1,194 @@ -broadcastToAll($senderEmail, $subject, $body); - } else { - // 群发给指定用户 - $recipientList = array_filter(array_map('trim', explode(',', $recipients))); - if (empty($recipientList)) { - $error = "请指定收件人"; - } else { - $result = $broadcastService->broadcastToUsers($senderEmail, $recipientList, $subject, $body); - } - } - - if (isset($result)) { - if ($result['success'] > 0) { - $message = "群发成功!成功发送 {$result['success']} 封邮件"; - if ($result['failed'] > 0) { - $message .= ",失败 {$result['failed']} 封"; - } - if (!empty($result['errors'])) { - $error = "部分失败:" . implode('
', array_slice($result['errors'], 0, 5)); - if (count($result['errors']) > 5) { - $error .= "
... 还有 " . (count($result['errors']) - 5) . " 个错误"; - } - } - } else { - $error = "群发失败:" . implode('
', $result['errors']); - } - } - } catch (Exception $e) { - $error = "群发失败: " . $e->getMessage(); - } - } -} - -// 获取所有用户列表(用于选择收件人) -$allUsers = $userRepo->getAll(); -?> - - - - 群发邮件 - 邮件服务器 - - - - -
-

邮件服务器管理后台

-
欢迎, - (退出) -
-
- - - -
-

群发邮件

- - -
- - - -
- - -
-
- -
- - -
-
- - - -
- - -
- -
- - -
- - -
-
- - - - - +broadcastToAll($senderEmail, $subject, $body); + } else { + // 群发给指定用户 + $recipientList = array_filter(array_map('trim', explode(',', $recipients))); + if (empty($recipientList)) { + $error = "请指定收件人"; + } else { + $result = $broadcastService->broadcastToUsers($senderEmail, $recipientList, $subject, $body); + } + } + + if (isset($result)) { + if ($result['success'] > 0) { + $message = "群发成功!成功发送 {$result['success']} 封邮件"; + if ($result['failed'] > 0) { + $message .= ",失败 {$result['failed']} 封"; + } + if (!empty($result['errors'])) { + $error = "部分失败:" . implode('
', array_slice($result['errors'], 0, 5)); + if (count($result['errors']) > 5) { + $error .= "
... 还有 " . (count($result['errors']) - 5) . " 个错误"; + } + } + } else { + $error = "群发失败:" . implode('
', $result['errors']); + } + } + } catch (Exception $e) { + $error = "群发失败: " . $e->getMessage(); + } + } +} + +// 获取所有用户列表(用于选择收件人) +$allUsers = $userRepo->getAll(); +?> + + + + 群发邮件 - 邮件服务器 + + + + +
+

邮件服务器管理后台

+
欢迎, + (退出) +
+
+ + + +
+

群发邮件

+ + +
+ + + +
+ + +
+
+ +
+ + +
+
+ + + +
+ + +
+ +
+ + +
+ + +
+
+ + + + + diff --git a/public/emails.php b/public/emails.php index 102d7b4..a4a2425 100644 --- a/public/emails.php +++ b/public/emails.php @@ -1,240 +1,238 @@ -delete($emailId)) { - $message = "邮件删除成功"; - } else { - $error = "删除失败"; - } -} - -// 处理标记已读 -if (isset($_GET['mark_read'])) { - $emailId = (int)$_GET['mark_read']; - if ($emailRepo->markAsRead($emailId)) { - $message = "邮件已标记为已读"; - } -} - -// 获取邮件列表 -$isAdmin = $_SESSION['is_admin'] ?? false; -$userId = $_SESSION['user_id']; - -// 分页参数 -$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1; -$perPage = 20; -$offset = ($page - 1) * $perPage; - -// 获取邮件 -if ($isAdmin) { - $emails = $emailRepo->getAll($perPage, $offset); - $totalEmails = $emailRepo->getCount(); -} else { - $emails = $emailRepo->getInbox($userId, $perPage, $offset); - $totalEmails = $emailRepo->getCount($userId); -} - -$totalPages = ceil($totalEmails / $perPage); -?> - - - - 邮件管理 - 邮件服务器 - - - - -
-

邮件服务器管理后台

-
欢迎, - (退出) -
-
- - - -
-

邮件管理 (全部邮件)(我的收件箱)

- - -
- - - -
- - -

封邮件

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ID发件人收件人主题状态时间操作
- 暂无邮件 -
- - 已读 - - 未读 - - - 查看 - - 标记已读 - - 删除 -
- - - 1): ?> - - -
- - - - - - - - +delete($emailId)) { + $message = "邮件删除成功"; + } else { + $error = "删除失败"; + } +} + +// 处理标记已读 +if (isset($_GET['mark_read'])) { + $emailId = (int)$_GET['mark_read']; + if ($emailRepo->markAsRead($emailId)) { + $message = "邮件已标记为已读"; + } +} + +// 获取邮件列表 +$isAdmin = $_SESSION['is_admin'] ?? false; +$userId = $_SESSION['user_id']; + +// 分页参数 +$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1; +$perPage = 20; +$offset = ($page - 1) * $perPage; + +// 获取邮件 +if ($isAdmin) { + $emails = $emailRepo->getAll($perPage, $offset); + $totalEmails = $emailRepo->getCount(); +} else { + $emails = $emailRepo->getInbox($userId, $perPage, $offset); + $totalEmails = $emailRepo->getCount($userId); +} + +$totalPages = ceil($totalEmails / $perPage); +?> + + + + 邮件管理 - 邮件服务器 + + + + +
+

邮件服务器管理后台

+
欢迎, + (退出) +
+
+ + + +
+

邮箱管理 (全部邮件)(我的收件箱)

+ + +
+ + + +
+ + +

封邮件

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID发件人收件人主题状态时间操作
+ 暂无邮件 +
+ + 已读 + + 未读 + + + 查看 + + 标记已读 + + 删除 +
+ + + 1): ?> + + +
+ + + + + + + + diff --git a/public/filters.php b/public/filters.php index 144b2a6..4e94f0d 100644 --- a/public/filters.php +++ b/public/filters.php @@ -1,243 +1,245 @@ -create($ruleType, $ruleValue, $action, $description)) { - $message = "过滤规则创建成功"; - } else { - $error = "创建失败,可能已存在相同规则"; - } - } catch (Exception $e) { - $error = "创建失败: " . $e->getMessage(); - } - } - } -} - -// 处理删除规则 -if (isset($_GET['delete'])) { - $id = (int)$_GET['delete']; - if ($filterRepo->delete($id)) { - $message = "规则删除成功"; - } else { - $error = "删除失败"; - } -} - -// 处理切换规则状态 -if (isset($_GET['toggle'])) { - $id = (int)$_GET['toggle']; - $rule = $filterRepo->getAll(); - $currentRule = null; - foreach ($rule as $r) { - if ($r['id'] == $id) { - $currentRule = $r; - break; - } - } - if ($currentRule) { - $newStatus = !$currentRule['is_active']; - if ($filterRepo->updateStatus($id, $newStatus)) { - $message = "规则状态已更新"; - } else { - $error = "更新失败"; - } - } -} - -// 获取所有规则 -$rules = $filterRepo->getAll(); -?> - - - - 过滤规则 - 邮件服务器 - - - - -
-

邮件服务器管理后台

-
欢迎, - (退出) -
-
- - - -
-

过滤规则管理

- - -
- - - -
- - - -

创建过滤规则

-
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
-
- - -

过滤规则列表 ()

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ID类型规则值动作描述状态创建时间操作
暂无过滤规则
- - - - - - - - - - - - - - - - 删除 -
-
- - - +create($ruleType, $ruleValue, $action, $description)) { + $message = "过滤规则创建成功"; + } else { + $error = "创建失败,可能已存在相同规则"; + } + } catch (Exception $e) { + $error = "创建失败: " . $e->getMessage(); + } + } + } +} + +// 处理删除规则 +if (isset($_GET['delete'])) { + $id = (int)$_GET['delete']; + if ($filterRepo->delete($id)) { + $message = "规则删除成功"; + } else { + $error = "删除失败"; + } +} + +// 处理切换规则状态 +if (isset($_GET['toggle'])) { + $id = (int)$_GET['toggle']; + $rule = $filterRepo->getAll(); + $currentRule = null; + foreach ($rule as $r) { + if ($r['id'] == $id) { + $currentRule = $r; + break; + } + } + if ($currentRule) { + $newStatus = !$currentRule['is_active']; + if ($filterRepo->updateStatus($id, $newStatus)) { + $message = "规则状态已更新"; + } else { + $error = "更新失败"; + } + } +} + +// 获取所有规则 +$rules = $filterRepo->getAll(); +?> + + + + 过滤规则 - 邮件服务器 + + + + +
+

邮件服务器管理后台

+
欢迎, + (退出) +
+
+ + + +
+

过滤规则管理

+ + +
+ + + +
+ + + +

创建过滤规则

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +

过滤规则列表 ()

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID类型规则值动作描述状态创建时间操作
暂无过滤规则
+ + + + + + + + + + + + + + + + 删除 +
+
+ + + diff --git a/public/help.php b/public/help.php index 3c64b8c..ec5b984 100644 --- a/public/help.php +++ b/public/help.php @@ -1,196 +1,186 @@ - - - - - 帮助 - 邮件服务器 - - - - -
-

邮件服务器管理后台

-
欢迎, - (退出) -
-
- - - -
-

使用帮助

- -
-

系统概述

-

这是一个基于POP3和SMTP协议的邮件服务器管理系统,支持用户注册、邮件收发、系统管理等功能。

-
- -
-

功能模块

- -

1. 用户管理(管理员功能)

-
    -
  • 创建用户:可以创建新的用户账号,设置密码、管理员权限和激活状态
  • -
  • 编辑用户:可以修改用户密码、权限和状态
  • -
  • 删除用户:可以删除用户账号(不能删除自己)
  • -
  • 用户列表:查看所有注册用户及其状态
  • -
- -

2. 邮件管理

-
    -
  • 查看邮件:管理员可以查看所有邮件,普通用户只能查看自己的收件箱
  • -
  • 标记已读:将未读邮件标记为已读
  • -
  • 删除邮件:删除不需要的邮件(软删除)
  • -
  • 邮件详情:点击邮件主题查看完整内容
  • -
- -

3. 群发邮件(管理员功能)

-
    -
  • 发送给所有用户:可以一次性向所有激活用户发送通知邮件
  • -
  • 发送给指定用户:可以选择特定用户进行群发
  • -
  • 邮件内容:支持自定义主题和内容
  • -
- -

4. 过滤规则

-
    -
  • 邮箱过滤:可以阻止或允许特定邮箱地址
  • -
  • IP过滤:可以阻止或允许特定IP地址
  • -
  • 规则管理:可以启用、禁用或删除过滤规则
  • -
- -

5. 系统设置(管理员功能)

-
    -
  • 端口设置:配置SMTP端口(默认25)和POP3端口(默认110)
  • -
  • 域名设置:设置邮件服务器域名(默认test.com)
  • -
  • 邮箱管理:设置用户邮箱大小限制
  • -
  • 日志设置:配置日志存储路径和最大大小
  • -
  • 密码修改:管理员可以修改自己的密码
  • -
- -

6. 服务管理(管理员功能)

-
    -
  • SMTP服务:查看和管理SMTP服务状态
  • -
  • POP3服务:查看和管理POP3服务状态
  • -
  • 服务起停:启动或停止邮件服务
  • -
- -

7. 日志管理

-
    -
  • 查看日志:查看SMTP和POP3服务器日志
  • -
  • 日志过滤:按类型过滤日志(全部/SMTP/POP3)
  • -
  • 清除日志:管理员可以清除日志记录
  • -
-
- -
-

启动服务器

-

要启动邮件服务器,需要在命令行执行以下命令:

-
-# 启动SMTP服务器(需要sudo权限)
-sudo php scripts/start_smtp.php

-# 启动POP3服务器(需要sudo权限)
-sudo php scripts/start_pop3.php -
-

注意:两个服务器需要分别在两个终端运行。

-
- -
-

测试邮件服务器

-

测试SMTP(发送邮件)

-
-telnet localhost 25
-HELO test
-MAIL FROM: <user1@test.com>
-RCPT TO: <admin@test.com>
-DATA
-Subject: 测试邮件
-From: user1@test.com
-To: admin@test.com

-这是一封测试邮件!
-.
-QUIT -
- -

测试POP3(接收邮件)

-
-telnet localhost 110
-USER admin@test.com
-PASS 123456
-STAT
-LIST
-RETR 1
-QUIT -
-
- -
-

常见问题

- -

Q: 端口被占用怎么办?

-

A: 检查端口占用情况:

-
-sudo netstat -tlnp | grep 25 # 检查SMTP端口
-sudo netstat -tlnp | grep 110 # 检查POP3端口 -
- -

Q: 数据库连接失败?

-

A: 确保Docker容器正在运行:

-
-docker-compose ps
-docker-compose up -d mysql -
- -

Q: 如何重置数据库?

-

A: 执行以下命令:

-
-docker-compose down -v
-docker-compose up -d
-sleep 15 -
-
- -
-

默认账号

-
    -
  • 管理员:admin@test.com / 123456
  • -
  • 普通用户:user1@test.com / 123456
  • -
-
-
- - - + + + + + 帮助 - 邮件服务器 + + + + +
+

邮件服务器管理后台

+
欢迎, + (退出) +
+
+ + + +
+

使用帮助

+ +
+

系统概述

+

这是一个基于POP3和SMTP协议的邮件服务器管理系统,支持用户注册、邮件收发、系统管理等功能。

+
+ +
+

功能模块

+ +

1. 用户管理

+
    +
  • 创建用户:可以创建新的用户账号,设置密码、管理员权限和激活状态
  • +
  • 编辑用户:可以修改用户密码、权限和状态
  • +
  • 删除用户:可以删除用户账号(不能删除自己)
  • +
  • 用户列表:查看所有注册用户及其状态
  • +
+ + +

2. 群发邮件

+
    +
  • 发送给所有用户:可以一次性向所有激活用户发送通知邮件
  • +
  • 发送给指定用户:可以选择特定用户进行群发
  • +
  • 邮件内容:支持自定义主题和内容
  • +
+ +

3. 过滤规则

+
    +
  • 邮箱过滤:可以阻止或允许特定邮箱地址
  • +
  • IP过滤:可以阻止或允许特定IP地址
  • +
  • 规则管理:可以启用、禁用或删除过滤规则
  • +
+ +

4. 系统设置

+
    +
  • 端口设置:配置SMTP端口(默认25)和POP3端口(默认110)
  • +
  • 域名设置:设置邮件服务器域名(默认test.com)
  • +
  • 邮箱管理:设置用户邮箱大小限制
  • +
  • 日志设置:配置日志存储路径和最大大小
  • +
  • 密码修改:管理员可以修改自己的密码
  • +
+ +

5. 服务管理

+
    +
  • SMTP服务:查看和管理SMTP服务状态
  • +
  • POP3服务:查看和管理POP3服务状态
  • +
  • 服务起停:启动或停止邮件服务
  • +
+ +

6. 日志管理

+
    +
  • 查看日志:查看SMTP和POP3服务器日志
  • +
  • 日志过滤:按类型过滤日志(全部/SMTP/POP3)
  • +
  • 清除日志:管理员可以清除日志记录
  • +
+
+ +
+

启动服务器

+

要启动邮件服务器,需要在命令行执行以下命令:

+
+# 启动SMTP服务器(需要sudo权限)
+sudo php scripts/start_smtp.php

+# 启动POP3服务器(需要sudo权限)
+sudo php scripts/start_pop3.php +
+

注意:两个服务器需要分别在两个终端运行。

+
+ +
+

测试邮件服务器

+

测试SMTP(发送邮件)

+
+telnet localhost 25
+HELO test
+MAIL FROM: <user1@test.com>
+RCPT TO: <admin@test.com>
+DATA
+Subject: 测试邮件
+From: user1@test.com
+To: admin@test.com

+这是一封测试邮件!
+.
+QUIT +
+ +

测试POP3(接收邮件)

+
+telnet localhost 110
+USER admin@test.com
+PASS 123456
+STAT
+LIST
+RETR 1
+QUIT +
+
+ +
+

常见问题

+ +

Q: 端口被占用怎么办?

+

A: 检查端口占用情况:

+
+sudo netstat -tlnp | grep 25 # 检查SMTP端口
+sudo netstat -tlnp | grep 110 # 检查POP3端口 +
+ +

Q: 数据库连接失败?

+

A: 确保Docker容器正在运行:

+
+docker-compose ps
+docker-compose up -d mysql +
+ +

Q: 如何重置数据库?

+

A: 执行以下命令:

+
+docker-compose down -v
+docker-compose up -d
+sleep 15 +
+
+ +
+

默认账号

+
    +
  • 管理员:admin@test.com / 123456
  • +
  • 普通用户:user1@test.com / 123456
  • +
+
+
+ + + diff --git a/public/index.php b/public/index.php index ca98734..ee75961 100644 --- a/public/index.php +++ b/public/index.php @@ -1,220 +1,217 @@ -verifyPassword($username, $password); - - if ($user && $user['is_active'] && $user['is_admin']) { - // 登录成功,清除尝试记录 - Security::clearLoginAttempts($username); - - $_SESSION['user_id'] = $user['id']; - $_SESSION['username'] = $user['username']; - $_SESSION['is_admin'] = $user['is_admin']; - header('Location: index.php'); - exit; - }else if($user && !$user['is_active']){ - $error = "用户被禁用"; - }else if($user && $user['is_active'] && !$user['is_admin']){ - $error = "没有权限"; - } - else{ - // 登录失败,记录尝试 - Security::recordLoginAttempt($username); - $error = "用户名或密码错误"; - } - } - } catch (Exception $e) { - $error = "登录失败: " . $e->getMessage(); - } -} - -// 如果是登录页面 -if (basename($_SERVER['PHP_SELF']) === 'index.php' && !isset($_SESSION['user_id'])) { - ?> - - - - - - 邮件服务器管理后台 - 登录 - - - - "; ?> -
-
- - -
-
- - -
- -
-

- 测试账号: admin@test.com / 123456
- 普通账号: user1@test.com / 123456 -

- - - - - - - - 邮件服务器管理后台 - - - -
-

邮件服务器管理后台

-
欢迎, - (退出) -
-
- - - -
- query("SELECT COUNT(*) as count FROM users"); - $userCount = $stmt->fetch()['count']; - - // 统计邮件数 - $stmt = $db->query("SELECT COUNT(*) as count FROM emails WHERE is_deleted = 0"); - $emailCount = $stmt->fetch()['count']; - - // 统计今日日志 - $stmt = $db->query("SELECT COUNT(*) as count FROM server_logs WHERE DATE(created_at) = CURDATE()"); - $logCount = $stmt->fetch()['count']; - - // 统计活跃会话(简化版) - $activeConnections = 0; - ?> - -
-

-

注册用户

-
-
-

-

总邮件数

-
-
-

-

今日日志

-
-
-

-

活跃连接

-
-
- -

最近邮件

- - - - - - - - - - - - query(" - SELECT e.*, - COALESCE(u1.username, e.sender) as sender_name, - COALESCE(u2.username, e.recipient) as recipient_name - FROM emails e - LEFT JOIN users u1 ON e.sender_id = u1.id - LEFT JOIN users u2 ON e.recipient_id = u2.id - WHERE e.is_deleted = 0 - ORDER BY e.created_at DESC - LIMIT 10 - "); - - while ($email = $stmt->fetch()) { - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - } - ?> - -
ID发件人收件人主题时间
{$email['id']}" . htmlspecialchars($email['sender_name'] ?? '未知') . "" . htmlspecialchars($email['recipient_name'] ?? '未知') . "" . htmlspecialchars($email['subject'] ?? '(无主题)') . "{$email['created_at']}
- +verifyPassword($username, $password); + + if ($user && $user['is_active'] && $user['is_admin']) { + // 登录成功,清除尝试记录 + Security::clearLoginAttempts($username); + + $_SESSION['user_id'] = $user['id']; + $_SESSION['username'] = $user['username']; + $_SESSION['is_admin'] = $user['is_admin']; + header('Location: index.php'); + exit; + }else if($user && !$user['is_active']){ + $error = "用户被禁用"; + }else if($user && $user['is_active'] && !$user['is_admin']){ + $error = "没有权限"; + }else{ + // 登录失败,记录尝试 + Security::recordLoginAttempt($username); + $error = "用户名或密码错误"; + } + } + } catch (Exception $e) { + $error = "登录失败: " . $e->getMessage(); + } +} + +// 如果是登录页面 +if (basename($_SERVER['PHP_SELF']) === 'index.php' && !isset($_SESSION['user_id'])) { + ?> + + + + + + 邮件服务器管理后台 - 登录 + + + + "; ?> +
+
+ + +
+
+ + +
+ +
+

+ 测试账号: admin@test.com / 123456
+ 普通账号: user1@test.com / 123456 +

+ + + + + + + + 邮件服务器管理后台 + + + +
+

邮件服务器管理后台

+
欢迎, + (退出) +
+
+ + + +
+ query("SELECT COUNT(*) as count FROM users"); + $userCount = $stmt->fetch()['count']; + + // 统计邮件数 + $stmt = $db->query("SELECT COUNT(*) as count FROM emails WHERE is_deleted = 0"); + $emailCount = $stmt->fetch()['count']; + + // 统计今日日志 + $stmt = $db->query("SELECT COUNT(*) as count FROM server_logs WHERE DATE(created_at) = CURDATE()"); + $logCount = $stmt->fetch()['count']; + + // 统计活跃会话(简化版) + $activeConnections = 0; + ?> + +
+

+

注册用户

+
+
+

+

总邮件数

+
+
+

+

今日日志

+
+
+

+

活跃连接

+
+
+ +

最近邮件

+ + + + + + + + + + + + query(" + SELECT e.*, + COALESCE(u1.username, e.sender) as sender_name, + COALESCE(u2.username, e.recipient) as recipient_name + FROM emails e + LEFT JOIN users u1 ON e.sender_id = u1.id + LEFT JOIN users u2 ON e.recipient_id = u2.id + WHERE e.is_deleted = 0 + ORDER BY e.created_at DESC + LIMIT 10 + "); + + while ($email = $stmt->fetch()) { + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + } + ?> + +
ID发件人收件人主题时间
{$email['id']}" . htmlspecialchars($email['sender_name'] ?? '未知') . "" . htmlspecialchars($email['recipient_name'] ?? '未知') . "" . htmlspecialchars($email['subject'] ?? '(无主题)') . "{$email['created_at']}
+ \ No newline at end of file diff --git a/public/index_test.php b/public/index_test.php new file mode 100644 index 0000000..573907f --- /dev/null +++ b/public/index_test.php @@ -0,0 +1,428 @@ +verifyPassword($username, $password); + + if ($user && $user['is_active'] && $user['is_admin']) { + // 登录成功,清除尝试记录 + Security::clearLoginAttempts($username); + + $_SESSION['user_id'] = $user['id']; + $_SESSION['username'] = $user['username']; + $_SESSION['is_admin'] = $user['is_admin']; + header('Location: index.php'); + exit; + } else { + // 登录失败,记录尝试 + Security::recordLoginAttempt($username); + $error = "用户名或密码错误"; + } + } + } catch (Exception $e) { + $error = "登录失败: " . $e->getMessage(); + } +} + +// 如果是登录页面 +if (basename($_SERVER['PHP_SELF']) === 'index.php' && !isset($_SESSION['user_id'])) { + ?> + + + + + + 邮件服务器管理后台 - 登录 + + + + + + + + + + + + + 邮件服务器管理后台 + + + +
+

邮件服务器管理后台

+
欢迎, + (退出) +
+
+ + + +
+ query("SELECT COUNT(*) as count FROM users"); + $userCount = $stmt->fetch()['count']; + + // 统计邮件数 + $stmt = $db->query("SELECT COUNT(*) as count FROM emails WHERE is_deleted = 0"); + $emailCount = $stmt->fetch()['count']; + + // 统计今日日志 + $stmt = $db->query("SELECT COUNT(*) as count FROM server_logs WHERE DATE(created_at) = CURDATE()"); + $logCount = $stmt->fetch()['count']; + + // 统计活跃会话(简化版) + $activeConnections = 0; + ?> + +
+

+

注册用户

+
+
+

+

总邮件数

+
+
+

+

今日日志

+
+
+

+

活跃连接

+
+
+ +

最近邮件

+ + + + + + + + + + + + query(" + SELECT e.*, + COALESCE(u1.username, e.sender) as sender_name, + COALESCE(u2.username, e.recipient) as recipient_name + FROM emails e + LEFT JOIN users u1 ON e.sender_id = u1.id + LEFT JOIN users u2 ON e.recipient_id = u2.id + WHERE e.is_deleted = 0 + ORDER BY e.created_at DESC + LIMIT 10 + "); + + while ($email = $stmt->fetch()) { + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + } + ?> + +
ID发件人收件人主题时间
{$email['id']}" . htmlspecialchars($email['sender_name'] ?? '未知') . "" . htmlspecialchars($email['recipient_name'] ?? '未知') . "" . htmlspecialchars($email['subject'] ?? '(无主题)') . "{$email['created_at']}
+ + \ No newline at end of file diff --git a/public/logout.php b/public/logout.php index 13fabfa..ae82d93 100644 --- a/public/logout.php +++ b/public/logout.php @@ -1,18 +1,18 @@ -prepare("DELETE FROM server_logs"); - $stmt->execute(); - $message = "所有日志已清除"; - } elseif ($logType === 'smtp') { - $stmt = $db->prepare("DELETE FROM server_logs WHERE log_type = 'SMTP'"); - $stmt->execute(); - $message = "SMTP日志已清除"; - } elseif ($logType === 'pop3') { - $stmt = $db->prepare("DELETE FROM server_logs WHERE log_type = 'POP3'"); - $stmt->execute(); - $message = "POP3日志已清除"; - } -} - -// 获取日志统计 -$stmt = $db->query("SELECT COUNT(*) as count FROM server_logs"); -$totalLogs = $stmt->fetch()['count']; - -$stmt = $db->query("SELECT COUNT(*) as count FROM server_logs WHERE log_type = 'SMTP'"); -$smtpLogs = $stmt->fetch()['count']; - -$stmt = $db->query("SELECT COUNT(*) as count FROM server_logs WHERE log_type = 'POP3'"); -$pop3Logs = $stmt->fetch()['count']; - -// 分页参数 -$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1; -$perPage = 50; -$offset = ($page - 1) * $perPage; - -// 过滤参数 -$filterType = $_GET['type'] ?? 'all'; - -// 获取日志列表 -if ($filterType === 'smtp') { - $stmt = $db->prepare(" - SELECT l.*, u.username - FROM server_logs l - LEFT JOIN users u ON l.user_id = u.id - WHERE l.log_type = 'SMTP' - ORDER BY l.created_at DESC - LIMIT ? OFFSET ? - "); - $stmt->execute([$perPage, $offset]); - - $countStmt = $db->prepare("SELECT COUNT(*) as count FROM server_logs WHERE log_type = 'SMTP'"); - $countStmt->execute(); - $totalLogs = $countStmt->fetch()['count']; -} elseif ($filterType === 'pop3') { - $stmt = $db->prepare(" - SELECT l.*, u.username - FROM server_logs l - LEFT JOIN users u ON l.user_id = u.id - WHERE l.log_type = 'POP3' - ORDER BY l.created_at DESC - LIMIT ? OFFSET ? - "); - $stmt->execute([$perPage, $offset]); - - $countStmt = $db->prepare("SELECT COUNT(*) as count FROM server_logs WHERE log_type = 'POP3'"); - $countStmt->execute(); - $totalLogs = $countStmt->fetch()['count']; -} else { - $stmt = $db->prepare(" - SELECT l.*, u.username - FROM server_logs l - LEFT JOIN users u ON l.user_id = u.id - ORDER BY l.created_at DESC - LIMIT ? OFFSET ? - "); - $stmt->execute([$perPage, $offset]); -} - -$logs = $stmt->fetchAll(); -$totalPages = ceil($totalLogs / $perPage); - -// 获取日志设置 -$logPath = $settingsRepo->get('log_path', '/var/log/mailserver'); -$logMaxSize = $settingsRepo->get('log_max_size', 10485760); -?> - - - - 系统日志 - 邮件服务器 - - - - -
-

邮件服务器管理后台

-
欢迎, - (退出) -
-
- - - -
-

系统日志管理

- - -
- - - -
- - - -
-
-

-

总日志数

-
-
-

-

SMTP日志

-
-
-

-

POP3日志

-
-
- - -
- 日志存储路径:
- 日志文件最大大小: MB -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ID类型消息用户IP地址时间
暂无日志
- - - 1): ?> - - -
- - - +prepare("DELETE FROM server_logs"); + $stmt->execute(); + $message = "所有日志已清除"; + } elseif ($logType === 'smtp') { + $stmt = $db->prepare("DELETE FROM server_logs WHERE log_type = 'SMTP'"); + $stmt->execute(); + $message = "SMTP日志已清除"; + } elseif ($logType === 'pop3') { + $stmt = $db->prepare("DELETE FROM server_logs WHERE log_type = 'POP3'"); + $stmt->execute(); + $message = "POP3日志已清除"; + } +} + +// 获取日志统计 +$stmt = $db->query("SELECT COUNT(*) as count FROM server_logs"); +$totalLogs = $stmt->fetch()['count']; + +$stmt = $db->query("SELECT COUNT(*) as count FROM server_logs WHERE log_type = 'SMTP'"); +$smtpLogs = $stmt->fetch()['count']; + +$stmt = $db->query("SELECT COUNT(*) as count FROM server_logs WHERE log_type = 'POP3'"); +$pop3Logs = $stmt->fetch()['count']; + +// 分页参数 +$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1; +$perPage = 50; +$offset = ($page - 1) * $perPage; + +// 过滤参数 +$filterType = $_GET['type'] ?? 'all'; + +// 获取日志列表 +if ($filterType === 'smtp') { + $stmt = $db->prepare(" + SELECT l.*, u.username + FROM server_logs l + LEFT JOIN users u ON l.user_id = u.id + WHERE l.log_type = 'SMTP' + ORDER BY l.created_at DESC + LIMIT ? OFFSET ? + "); + $stmt->execute([$perPage, $offset]); + + $countStmt = $db->prepare("SELECT COUNT(*) as count FROM server_logs WHERE log_type = 'SMTP'"); + $countStmt->execute(); + $totalLogs = $countStmt->fetch()['count']; +} elseif ($filterType === 'pop3') { + $stmt = $db->prepare(" + SELECT l.*, u.username + FROM server_logs l + LEFT JOIN users u ON l.user_id = u.id + WHERE l.log_type = 'POP3' + ORDER BY l.created_at DESC + LIMIT ? OFFSET ? + "); + $stmt->execute([$perPage, $offset]); + + $countStmt = $db->prepare("SELECT COUNT(*) as count FROM server_logs WHERE log_type = 'POP3'"); + $countStmt->execute(); + $totalLogs = $countStmt->fetch()['count']; +} else { + $stmt = $db->prepare(" + SELECT l.*, u.username + FROM server_logs l + LEFT JOIN users u ON l.user_id = u.id + ORDER BY l.created_at DESC + LIMIT ? OFFSET ? + "); + $stmt->execute([$perPage, $offset]); +} + +$logs = $stmt->fetchAll(); +$totalPages = ceil($totalLogs / $perPage); + +// 获取日志设置 +$logPath = $settingsRepo->get('log_path', '/var/log/mailserver'); +$logMaxSize = $settingsRepo->get('log_max_size', 10485760); +?> + + + + 系统日志 - 邮件服务器 + + + + +
+

邮件服务器管理后台

+
欢迎, + (退出) +
+
+ + + +
+

系统日志管理

+ + +
+ + + +
+ + + +
+
+

+

总日志数

+
+
+

+

SMTP日志

+
+
+

+

POP3日志

+
+
+ + +
+ 日志存储路径:
+ 日志文件最大大小: MB +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID类型消息用户IP地址时间
暂无日志
+ + + 1): ?> + + +
+ + + diff --git a/public/register.php b/public/register.php index cdea2c5..f26ca81 100644 --- a/public/register.php +++ b/public/register.php @@ -1,288 +1,288 @@ -', $usernameValidation['errors']); - } else { - // 验证邮箱域名(默认test.com) - $domain = 'test.com'; - if (!Validator::validateEmailDomain($username, $domain)) { - $error = "邮箱域名必须是 @{$domain}"; - } else { - // 验证密码 - $passwordValidation = Validator::validatePassword($password, 6); - if (!$passwordValidation['valid']) { - $error = implode('
', $passwordValidation['errors']); - } else { - // 验证密码确认 - $matchValidation = Validator::validatePasswordMatch($password, $confirmPassword); - if (!$matchValidation['valid']) { - $error = implode('
', $matchValidation['errors']); - } else { - // 尝试创建用户 - try { - $userRepo = new UserRepository(); - - // 检查用户名是否已存在 - if ($userRepo->usernameExists($username)) { - $error = "该邮箱已被注册"; - } else { - // 创建新用户(默认非管理员,激活状态) - $user = $userRepo->create($username, $password, false, true); - - if ($user) { - $success = "注册成功!请使用您的账号登录。"; - // 3秒后跳转到登录页面 - header("Refresh: 3; url=index.php"); - } else { - $error = "注册失败,请稍后重试"; - } - } - } catch (Exception $e) { - $error = "注册失败: " . $e->getMessage(); - } - } - } - } - } - } -} - -// 生成CSRF令牌 -$csrfToken = Security::generateCSRFToken(); -?> - - - - 用户注册 - 邮件服务器 - - - - -
-

用户注册

-

创建您的邮件服务器账号

- - -
- - - -
- -
- - -
- - -
请输入您的邮箱地址(域名必须是 @test.com
-
- -
- - -
密码长度至少需要6个字符
-
- -
- - -
- - -
- - - -
- - - +', $usernameValidation['errors']); + } else { + // 验证邮箱域名(默认test.com) + $domain = 'test.com'; + if (!Validator::validateEmailDomain($username, $domain)) { + $error = "邮箱域名必须是 @{$domain}"; + } else { + // 验证密码 + $passwordValidation = Validator::validatePassword($password, 6); + if (!$passwordValidation['valid']) { + $error = implode('
', $passwordValidation['errors']); + } else { + // 验证密码确认 + $matchValidation = Validator::validatePasswordMatch($password, $confirmPassword); + if (!$matchValidation['valid']) { + $error = implode('
', $matchValidation['errors']); + } else { + // 尝试创建用户 + try { + $userRepo = new UserRepository(); + + // 检查用户名是否已存在 + if ($userRepo->usernameExists($username)) { + $error = "该邮箱已被注册"; + } else { + // 创建新用户(默认非管理员,激活状态) + $user = $userRepo->create($username, $password, false, true); + + if ($user) { + $success = "注册成功!请使用您的账号登录。"; + // 3秒后跳转到登录页面 + header("Refresh: 3; url=index.php"); + } else { + $error = "注册失败,请稍后重试"; + } + } + } catch (Exception $e) { + $error = "注册失败: " . $e->getMessage(); + } + } + } + } + } + } +} + +// 生成CSRF令牌 +$csrfToken = Security::generateCSRFToken(); +?> + + + + 用户注册 - 邮件服务器 + + + + +
+

用户注册

+

创建您的邮件服务器账号

+ + +
+ + + +
+ +
+ + +
+ + +
请输入您的邮箱地址(域名必须是 @test.com
+
+ +
+ + +
密码长度至少需要6个字符
+
+ +
+ + +
+ + +
+ + + +
+ + + diff --git a/public/services.php b/public/services.php index 55e0df3..edaada4 100644 --- a/public/services.php +++ b/public/services.php @@ -1,166 +1,289 @@ -updateStatus($serviceName, true, $pid); - $message = strtoupper($serviceName) . "服务已启动"; - } elseif ($action === 'stop') { - // 停止服务 - $status = $serviceRepo->getStatus($serviceName); - if ($status && $status['pid']) { - // 尝试终止进程 - @exec("kill {$status['pid']} 2>/dev/null"); - } - $serviceRepo->updateStatus($serviceName, false, null); - $message = strtoupper($serviceName) . "服务已停止"; - } - } -} - -// 获取服务状态 -$smtpStatus = $serviceRepo->getStatus('smtp'); -$pop3Status = $serviceRepo->getStatus('pop3'); -$smtpRunning = $serviceRepo->isRunning('smtp'); -$pop3Running = $serviceRepo->isRunning('pop3'); - -// 获取端口设置 -$smtpPort = $settingsRepo->get('smtp_port', 25); -$pop3Port = $settingsRepo->get('pop3_port', 110); -?> - - - - 服务管理 - 邮件服务器 - - - - -
-

邮件服务器管理后台

-
欢迎, - (退出) -
-
- - - -
-

服务管理

- - -
- - - -
- - - -
-

SMTP服务(邮件发送)

-

- - - - - 停止服务 - - 启动服务 - -

-
- 端口:
- 进程ID:
- 最后启动:
- 最后停止: -
-
- - -
-

POP3服务(邮件接收)

-

- - - - - 停止服务 - - 启动服务 - -

-
- 端口:
- 进程ID:
- 最后启动:
- 最后停止: -
-
- -
- 注意:此页面仅用于管理服务状态。实际启动服务需要使用命令行: -
    -
  • SMTP服务:sudo php scripts/start_smtp.php
  • -
  • POP3服务:sudo php scripts/start_pop3.php
  • -
-
-
- - - + false, 'message' => '服务已在运行']; + } + + // 启动服务(后台运行) + $cmd = "php {$scriptPath} > {$logFile} 2>&1 & echo $!"; + $pid = trim(shell_exec($cmd)); + + sleep(2); // 等待服务启动 + + if (isServiceRunning($serviceName)) { + return ['success' => true, 'pid' => $pid]; + } else { + return ['success' => false, 'message' => '启动失败']; + } +} + +function stopService($serviceName, $pid) { + if ($pid) { + // 使用 kill 命令终止进程 + @exec("kill {$pid} 2>/dev/null"); + sleep(1); + + // 如果进程还在,强制终止 + if (isServiceRunning($serviceName)) { + @exec("kill -9 {$pid} 2>/dev/null"); + sleep(1); + } + } else { + // 如果没有PID,查找并终止所有相关进程 + @exec("pkill -f 'start_{$serviceName}.php' 2>/dev/null"); + @exec("pkill -f 'SmtpServer.php' 2>/dev/null"); + @exec("pkill -f 'Pop3Server.php' 2>/dev/null"); + } + + return !isServiceRunning($serviceName); +} + +function isServiceRunning($serviceName) { + // 检查端口是否被监听 + $port = ($serviceName === 'smtp') ? 25 : 110; + $cmd = "netstat -tlnp 2>/dev/null | grep :{$port} | grep LISTEN"; + $result = shell_exec($cmd); + return !empty($result); +} + +function getServicePid($serviceName) { + $port = ($serviceName === 'smtp') ? 25 : 110; + $cmd = "lsof -ti:{$port} 2>/dev/null | head -1"; + return trim(shell_exec($cmd)); +} +// ===================== 修改部分结束 ===================== +// 处理服务起停 +if (isset($_GET['action'])) { + $serviceName = $_GET['service'] ?? ''; + $action = $_GET['action'] ?? ''; + + if ($serviceName === 'smtp' || $serviceName === 'pop3') { + // ===================== 修改部分开始 ===================== + // 实际启动服务 + $port = ($serviceName === 'smtp') ? + $settingsRepo->get('smtp_port', 25) : + $settingsRepo->get('pop3_port', 110); + + $result = startService($serviceName, $port); + + if ($result['success']) { + $pid = $result['pid']; + $serviceRepo->updateStatus($serviceName, true, $pid); + $message = strtoupper($serviceName) . "服务已启动 (PID: {$pid})"; + } else { + $error = strtoupper($serviceName) . "服务启动失败: " . $result['message']; + } + // ===================== 修改部分结束 ===================== + } + elseif ($action === 'stop') { + // ===================== 修改部分开始 ===================== + // 实际停止服务 + $status = $serviceRepo->getStatus($serviceName); + $pid = $status['pid'] ?? getServicePid($serviceName); + + if (stopService($serviceName, $pid)) { + $serviceRepo->updateStatus($serviceName, false, null); + $message = strtoupper($serviceName) . "服务已停止"; + } else { + $error = strtoupper($serviceName) . "服务停止失败"; + } + // ===================== 修改部分结束 ===================== + } + +} + +// 获取服务状态 +$smtpStatus = $serviceRepo->getStatus('smtp'); +$pop3Status = $serviceRepo->getStatus('pop3'); +// 实际检查服务是否运行 +$smtpRunning = isServiceRunning('smtp'); +$pop3Running = isServiceRunning('pop3'); + +// 如果数据库状态与实际状态不一致,更新数据库 +if ($smtpStatus['is_running'] != $smtpRunning) { + $pid = $smtpRunning ? getServicePid('smtp') : null; + $serviceRepo->updateStatus('smtp', $smtpRunning, $pid); +} + +if ($pop3Status['is_running'] != $pop3Running) { + $pid = $pop3Running ? getServicePid('pop3') : null; + $serviceRepo->updateStatus('pop3', $pop3Running, $pid); +} + +// 重新获取更新后的状态 +$smtpStatus = $serviceRepo->getStatus('smtp'); +$pop3Status = $serviceRepo->getStatus('pop3'); +$smtpRunning = $smtpStatus['is_running']; +$pop3Running = $pop3Status['is_running']; +// ===================== 修改部分结束 ===================== + +// 获取端口设置 +$smtpPort = $settingsRepo->get('smtp_port', 25); +$pop3Port = $settingsRepo->get('pop3_port', 110); +?> + + + + 服务管理 - 邮件服务器 + + + + +
+

邮件服务器管理后台

+
欢迎, + (退出) +
+
+ + + +
+

服务管理

+ + +
+ + + +
+ + + +
+

SMTP服务(邮件发送)

+

+ + + + + 停止服务 + + 启动服务 + +

+
+ 端口:
+ 进程ID:
+ 最后启动:
+ 最后停止: +
+
+ + +
+

POP3服务(邮件接收)

+

+ + + + + 停止服务 + + 启动服务 + +

+
+ 端口:
+ 进程ID:
+ 最后启动:
+ 最后停止: +
+
+ +
+ 注意:此页面仅用于管理服务状态。实际启动服务需要使用命令行: +
    +
  • SMTP服务:sudo php scripts/start_smtp.php
  • +
  • POP3服务:sudo php scripts/start_pop3.php
  • +
  • 服务状态实时检测,无需手动刷新
  • +
  • 启动/停止可能需要几秒钟时间
  • +
  • 如果服务启动失败,请检查端口是否被占用
  • +
+
+
+ + + + + + diff --git a/public/settings.php b/public/settings.php index fb9bdf1..54798a9 100644 --- a/public/settings.php +++ b/public/settings.php @@ -1,320 +1,321 @@ -set('smtp_port', $port); - } else { - $error = "SMTP端口无效(1-65535)"; - } - } - - // POP3端口 - if (isset($_POST['pop3_port'])) { - $port = (int)$_POST['pop3_port']; - if (Validator::validatePort($port)) { - $settingsRepo->set('pop3_port', $port); - } else { - $error = "POP3端口无效(1-65535)"; - } - } - - // 域名 - if (isset($_POST['domain'])) { - $domain = trim($_POST['domain']); - if (!empty($domain)) { - $settingsRepo->set('domain', $domain); - } - } - - // 默认邮箱大小限制 - if (isset($_POST['mailbox_size_limit'])) { - $size = (int)$_POST['mailbox_size_limit']; - if ($size > 0) { - $settingsRepo->set('mailbox_size_limit', $size); - } - } - - // 日志路径 - if (isset($_POST['log_path'])) { - $settingsRepo->set('log_path', trim($_POST['log_path'])); - } - - // 日志最大大小 - if (isset($_POST['log_max_size'])) { - $size = (int)$_POST['log_max_size']; - if ($size > 0) { - $settingsRepo->set('log_max_size', $size); - } - } - - if (empty($error)) { - $message = "系统设置已更新"; - } -} - -// 处理管理员密码修改 -if (isset($_POST['change_admin_password'])) { - $oldPassword = $_POST['old_password'] ?? ''; - $newPassword = $_POST['new_password'] ?? ''; - $confirmPassword = $_POST['confirm_password'] ?? ''; - - $user = $userRepo->findById($_SESSION['user_id']); - - if (!Security::verifyPassword($oldPassword, $user['password_hash'])) { - $error = "原密码错误"; - } else { - $passwordValidation = Validator::validatePassword($newPassword, 6); - if (!$passwordValidation['valid']) { - $error = implode('
', $passwordValidation['errors']); - } elseif ($newPassword !== $confirmPassword) { - $error = "两次输入的密码不一致"; - } else { - if ($userRepo->update($_SESSION['user_id'], ['password' => $newPassword])) { - $message = "管理员密码已更新"; - } else { - $error = "密码更新失败"; - } - } - } -} - -// 处理用户邮箱大小设置 -if (isset($_POST['set_mailbox_size'])) { - $userId = (int)$_POST['user_id']; - $sizeBytes = (int)$_POST['mailbox_size']; - - if ($sizeBytes > 0) { - if ($mailboxRepo->setSizeLimit($userId, $sizeBytes)) { - $message = "邮箱大小限制已更新"; - } else { - $error = "更新失败"; - } - } else { - $error = "邮箱大小必须大于0"; - } -} - -// 获取当前设置 -$settings = $settingsRepo->getAll(); -$users = $userRepo->getAll(); -?> - - - - 系统设置 - 邮件服务器 - - - - -
-

邮件服务器管理后台

-
欢迎, - (退出) -
-
- - - -
-

系统设置

- - -
- - - -
- - - -
-

服务器端口设置

-
-
- - - SMTP服务器监听端口 -
-
- - - POP3服务器监听端口 -
- -
-
- - -
-

域名设置

-
-
- - - 邮件服务器域名,用户邮箱必须使用此域名 -
- -
-
- - -
-

邮箱大小管理

-
-
- - - 默认值:104857600 (100MB) -
- -
- -

用户邮箱大小设置

- - - - - - - - - - - - - getUsage($user['id']); - $usedMB = round($usage['used'] / 1048576, 2); - $limitMB = round($usage['limit'] / 1048576, 2); - ?> - - - - - - - - - -
用户当前使用限制大小使用率操作
MB MB% -
- - - -
-
-
- - -
-

日志设置

-
-
- - - 日志文件存储的目录路径 -
-
- - - 默认值:10485760 (10MB) -
- -
-
- - -
-

修改管理员密码

-
-
- - -
-
- - - 密码长度至少6个字符 -
-
- - -
- -
-
-
- - - +set('smtp_port', $port); + } else { + $error = "SMTP端口无效(1-65535)"; + } + } + + // POP3端口 + if (isset($_POST['pop3_port'])) { + $port = (int)$_POST['pop3_port']; + if (Validator::validatePort($port)) { + $settingsRepo->set('pop3_port', $port); + } else { + $error = "POP3端口无效(1-65535)"; + } + } + + // 域名 + if (isset($_POST['domain'])) { + $domain = trim($_POST['domain']); + if (!empty($domain)) { + $settingsRepo->set('domain', $domain); + } + } + + // 默认邮箱大小限制 + if (isset($_POST['mailbox_size_limit'])) { + $size = (int)$_POST['mailbox_size_limit']; + if ($size > 0) { + $settingsRepo->set('mailbox_size_limit', $size); + } + } + + // 日志路径 + if (isset($_POST['log_path'])) { + $settingsRepo->set('log_path', trim($_POST['log_path'])); + } + + // 日志最大大小 + if (isset($_POST['log_max_size'])) { + $size = (int)$_POST['log_max_size']; + if ($size > 0) { + $settingsRepo->set('log_max_size', $size); + } + } + + if (empty($error)) { + $message = "系统设置已更新"; + } +} + +// 处理管理员密码修改 +if (isset($_POST['change_admin_password'])) { + $oldPassword = $_POST['old_password'] ?? ''; + $newPassword = $_POST['new_password'] ?? ''; + $confirmPassword = $_POST['confirm_password'] ?? ''; + + $user = $userRepo->findById($_SESSION['user_id']); + + if (!Security::verifyPassword($oldPassword, $user['password_hash'])) { + $error = "原密码错误"; + } else { + $passwordValidation = Validator::validatePassword($newPassword, 6); + if (!$passwordValidation['valid']) { + $error = implode('
', $passwordValidation['errors']); + } elseif ($newPassword !== $confirmPassword) { + $error = "两次输入的密码不一致"; + } else { + if ($userRepo->update($_SESSION['user_id'], ['password' => $newPassword])) { + $message = "管理员密码已更新"; + } else { + $error = "密码更新失败"; + } + } + } +} + +// 处理用户邮箱大小设置 +if (isset($_POST['set_mailbox_size'])) { + $userId = (int)$_POST['user_id']; + $sizeBytes = (int)$_POST['mailbox_size']; + + if ($sizeBytes > 0) { + if ($mailboxRepo->setSizeLimit($userId, $sizeBytes)) { + $message = "邮箱大小限制已更新"; + } else { + $error = "更新失败"; + } + } else { + $error = "邮箱大小必须大于0"; + } +} + +// 获取当前设置 +$settings = $settingsRepo->getAll(); +$users = $userRepo->getAll(); +?> + + + + 系统设置 - 邮件服务器 + + + + +
+

邮件服务器管理后台

+
欢迎, + (退出) +
+
+ + + +
+

系统设置

+ + +
+ + + +
+ + + +
+

服务器端口设置

+
+
+ + + SMTP服务器监听端口 +
+
+ + + POP3服务器监听端口 +
+ +
+
+ + +
+

域名设置

+
+
+ + + 邮件服务器域名,用户邮箱必须使用此域名 +
+ +
+
+ + +
+

邮箱大小管理

+
+
+ + + 默认值:104857600 (100MB) +
+ +
+ +

用户邮箱大小设置

+ + + + + + + + + + + + + getUsage($user['id']); + $usedMB = round($usage['used'] / 1048576, 2); + $limitMB = round($usage['limit'] / 1048576, 2); + ?> + + + + + + + + + +
用户当前使用限制大小使用率操作
MB MB% +
+ + + +
+
+
+ + +
+

日志设置

+
+
+ + + 日志文件存储的目录路径 +
+
+ + + 默认值:10485760 (10MB) +
+ +
+
+ + +
+

修改管理员密码

+
+
+ + +
+
+ + + 密码长度至少6个字符 +
+
+ + +
+ +
+
+
+ + + diff --git a/public/users.php b/public/users.php index 13d7aaf..36376b8 100644 --- a/public/users.php +++ b/public/users.php @@ -1,297 +1,299 @@ -', $usernameValidation['errors']); - } else { - if (!Validator::validateEmailDomain($username, 'test.com')) { - $error = "邮箱域名必须是 @test.com"; - } else { - $passwordValidation = Validator::validatePassword($password, 6); - if (!$passwordValidation['valid']) { - $error = implode('
', $passwordValidation['errors']); - } else { - try { - if ($userRepo->usernameExists($username)) { - $error = "用户名已存在"; - } else { - $userRepo->create($username, $password, $isAdmin, $isActive); - $message = "用户创建成功"; - } - } catch (Exception $e) { - $error = "创建失败: " . $e->getMessage(); - } - } - } - } -} - -// 处理更新用户 -if (isset($_POST['update_user'])) { - $userId = (int)$_POST['user_id']; - $data = []; - - if (!empty($_POST['new_password'])) { - $passwordValidation = Validator::validatePassword($_POST['new_password'], 6); - if (!$passwordValidation['valid']) { - $error = implode('
', $passwordValidation['errors']); - } else { - $data['password'] = $_POST['new_password']; - } - } - - /**if (isset($_POST['is_admin'])) { - $data['is_admin'] = (int)$_POST['is_admin']; - } - - if (isset($_POST['is_active'])) { - $data['is_active'] = (int)$_POST['is_active']; - }**/ - // 管理员权限总是更新 - $data['is_admin'] = isset($_POST['is_admin']) ? 1 : 0; - - // 激活状态也是 - $data['is_active'] = isset($_POST['is_active']) ? 1 : 0; - - if (empty($error) && !empty($data)) { - if ($userRepo->update($userId, $data)) { - $message = "用户更新成功"; - } else { - $error = "更新失败"; - } - } -} - -// 处理删除用户 -if (isset($_GET['delete'])) { - $userId = (int)$_GET['delete']; - if ($userId != $_SESSION['user_id']) { // 不能删除自己 - if ($userRepo->delete($userId)) { - $message = "用户删除成功"; - } else { - $error = "删除失败"; - } - } else { - $error = "不能删除自己的账号"; - } -} - -// 获取所有用户 -$users = $userRepo->getAll(); -?> - - - - 用户管理 - 邮件服务器 - - - - -
-

邮件服务器管理后台

-
欢迎, - (退出) -
-
- - - -
-

用户管理

- - -
- - - -
- - - -

创建新用户

-
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
-
- - -

用户列表 ()

- - - - - - - - - - - - - - - - - - - - - - - -
ID用户名角色状态创建时间操作
- - 管理员 - - 普通用户 - - - - 激活 - - 禁用 - - - 编辑 - - 删除 - -
-
- - - - - - - - +', $usernameValidation['errors']); + } else { + if (!Validator::validateEmailDomain($username, 'test.com')) { + $error = "邮箱域名必须是 @test.com"; + } else { + $passwordValidation = Validator::validatePassword($password, 6); + if (!$passwordValidation['valid']) { + $error = implode('
', $passwordValidation['errors']); + } else { + try { + if ($userRepo->usernameExists($username)) { + $error = "用户名已存在"; + } else { + $userRepo->create($username, $password, $isAdmin, $isActive); + $message = "用户创建成功"; + } + } catch (Exception $e) { + $error = "创建失败: " . $e->getMessage(); + } + } + } + } +} + +// 处理更新用户 +if (isset($_POST['update_user'])) { + $userId = (int)$_POST['user_id']; + $data = []; + + if (!empty($_POST['new_password'])) { + $passwordValidation = Validator::validatePassword($_POST['new_password'], 6); + if (!$passwordValidation['valid']) { + $error = implode('
', $passwordValidation['errors']); + } else { + $data['password'] = $_POST['new_password']; + } + } + + /**if (isset($_POST['is_admin'])) { + $data['is_admin'] = (int)$_POST['is_admin']; + } + + if (isset($_POST['is_active'])) { + $data['is_active'] = (int)$_POST['is_active']; + }**/ + // 管理员权限总是更新 + $data['is_admin'] = isset($_POST['is_admin']) ? 1 : 0; + + // 激活状态也是 + $data['is_active'] = isset($_POST['is_active']) ? 1 : 0; + + if (empty($error) && !empty($data)) { + if ($userRepo->update($userId, $data)) { + $message = "用户更新成功"; + } else { + $error = "更新失败"; + } + } +} + +// 处理删除用户 +if (isset($_GET['delete'])) { + $userId = (int)$_GET['delete']; + if ($userId != $_SESSION['user_id']) { // 不能删除自己 + if ($userRepo->delete($userId)) { + $message = "用户删除成功"; + } else { + $error = "删除失败"; + } + } else { + $error = "不能删除自己的账号"; + } +} + +// 获取所有用户 +$users = $userRepo->getAll(); +?> + + + + 用户管理 - 邮件服务器 + + + + +
+

邮件服务器管理后台

+
欢迎, + (退出) +
+
+ + + +
+

用户管理

+ + +
+ + + +
+ + + +

创建新用户

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +

用户列表 ()

+ + + + + + + + + + + + + + + + + + + + + + + +
ID用户名角色状态创建时间操作
+ + 管理员 + + 普通用户 + + + + 激活 + + 禁用 + + + 编辑 + + 删除 + +
+
+ + + + + + + + diff --git a/reset_db.sh b/reset_db.sh index f984bea..c9315d8 100644 --- a/reset_db.sh +++ b/reset_db.sh @@ -1,11 +1,11 @@ -#!/bin/bash -echo "重置数据库..." -docker-compose down -v -echo "启动数据库(SQL会自动执行)..." -docker-compose up -d -echo "等待数据库初始化(10秒)..." -sleep 10 -echo "验证数据..." -docker-compose exec mysql mysql -umail_user -puser123 mail_server -e "SELECT id, username, is_admin FROM users; SELECT COUNT(*) as email_count FROM emails;" -echo "完成!" - +#!/bin/bash +echo "重置数据库..." +docker-compose down -v +echo "启动数据库(SQL会自动执行)..." +docker-compose up -d +echo "等待数据库初始化(10秒)..." +sleep 10 +echo "验证数据..." +docker-compose exec mysql mysql -umail_user -puser123 mail_server -e "SELECT id, username, is_admin FROM users; SELECT COUNT(*) as email_count FROM emails;" +echo "完成!" + diff --git a/scripts/create_all_tables.sql b/scripts/create_all_tables.sql index 0d39724..953855b 100644 --- a/scripts/create_all_tables.sql +++ b/scripts/create_all_tables.sql @@ -1,135 +1,135 @@ --- ============================================ --- 邮件服务器完整数据库初始化脚本 --- ============================================ --- 创建数据库(如果不存在) -CREATE DATABASE IF NOT EXISTS mail_server -CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - -USE mail_server; - --- 创建用户并授权(关键步骤!) -CREATE USER IF NOT EXISTS 'mail_user'@'%' IDENTIFIED BY 'user123'; -GRANT ALL PRIVILEGES ON mail_server.* TO 'mail_user'@'%'; -FLUSH PRIVILEGES; - --- ============================================ --- 核心功能表 --- ============================================ - --- 用户表 -CREATE TABLE IF NOT EXISTS users ( - id INT AUTO_INCREMENT PRIMARY KEY, - username VARCHAR(100) UNIQUE NOT NULL, - password_hash VARCHAR(255) NOT NULL, - is_admin TINYINT(1) DEFAULT 0, - is_active TINYINT(1) DEFAULT 1, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- 邮件表(兼容两种存储方式:SimpleSmtpServer用sender/recipient,SmtpHandler用sender_id/recipient_id) -CREATE TABLE IF NOT EXISTS emails ( - id INT AUTO_INCREMENT PRIMARY KEY, - sender VARCHAR(100), - recipient VARCHAR(100), - sender_id INT, - recipient_id INT, - subject VARCHAR(200), - body TEXT, - headers TEXT, - size_bytes INT DEFAULT 0, - is_deleted TINYINT(1) DEFAULT 0, - is_read TINYINT(1) DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (sender_id) REFERENCES users(id) ON DELETE SET NULL, - FOREIGN KEY (recipient_id) REFERENCES users(id) ON DELETE SET NULL -) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE=InnoDB; - --- 系统日志表 -CREATE TABLE IF NOT EXISTS server_logs ( - id INT AUTO_INCREMENT PRIMARY KEY, - log_type VARCHAR(50), - message TEXT, - client_ip VARCHAR(45), - user_id INT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL -) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE=InnoDB; - --- ============================================ --- 管理功能表 --- ============================================ - --- 系统设置表 -CREATE TABLE IF NOT EXISTS system_settings ( - id INT AUTO_INCREMENT PRIMARY KEY, - setting_key VARCHAR(100) UNIQUE NOT NULL, - setting_value TEXT, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE=InnoDB; - --- 过滤规则表 -CREATE TABLE IF NOT EXISTS filter_rules ( - id INT AUTO_INCREMENT PRIMARY KEY, - rule_type ENUM('email', 'ip') NOT NULL, - rule_value VARCHAR(255) NOT NULL, - action ENUM('block', 'allow') DEFAULT 'block', - description TEXT, - is_active TINYINT(1) DEFAULT 1, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE KEY unique_rule (rule_type, rule_value) -) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE=InnoDB; - --- 服务状态表 -CREATE TABLE IF NOT EXISTS service_status ( - id INT AUTO_INCREMENT PRIMARY KEY, - service_name VARCHAR(50) UNIQUE NOT NULL, - is_running TINYINT(1) DEFAULT 0, - pid INT, - last_started_at TIMESTAMP NULL, - last_stopped_at TIMESTAMP NULL, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE=InnoDB; - --- 用户邮箱大小限制表 -CREATE TABLE IF NOT EXISTS user_mailbox_limits ( - id INT AUTO_INCREMENT PRIMARY KEY, - user_id INT NOT NULL, - size_limit_bytes BIGINT DEFAULT 104857600, -- 100MB - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, - UNIQUE KEY unique_user (user_id) -) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE=InnoDB; - --- ============================================ --- 插入默认数据 --- ============================================ - --- 插入测试用户(密码都是:123456) -INSERT INTO users (username, password_hash, is_admin, is_active) VALUES -('admin@test.com', '$2y$10$jB21V61k9aLAyp5.5qBpV.L70Aq6.XrtJrvlNI28bOXeJboLBJwoq', 1, 1), -('user1@test.com', '$2y$10$jB21V61k9aLAyp5.5qBpV.L70Aq6.XrtJrvlNI28bOXeJboLBJwoq', 0, 1) -ON DUPLICATE KEY UPDATE username = VALUES(username); - --- 插入测试邮件 -INSERT INTO emails (sender, recipient, subject, body) VALUES -('admin@test.com', 'user1@test.com', '欢迎使用邮件系统', '这是第一封测试邮件'), -('user1@test.com', 'admin@test.com', '回复测试', '我收到了,谢谢!') -ON DUPLICATE KEY UPDATE id = id; - --- 插入系统默认设置 -INSERT INTO system_settings (setting_key, setting_value) VALUES -('smtp_port', '25'), -('pop3_port', '110'), -('domain', 'test.com'), -('mailbox_size_limit', '104857600'), -- 100MB -('smtp_enabled', '1'), -('pop3_enabled', '1'), -('log_path', '/var/log/mailserver'), -('log_max_size', '10485760') -- 10MB -ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value); - --- 插入服务状态初始记录 -INSERT INTO service_status (service_name, is_running) VALUES -('smtp', 0), -('pop3', 0) -ON DUPLICATE KEY UPDATE service_name = VALUES(service_name); - +-- ============================================ +-- 邮件服务器完整数据库初始化脚本 +-- ============================================ +-- 创建数据库(如果不存在) +CREATE DATABASE IF NOT EXISTS mail_server +CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +USE mail_server; + +-- 创建用户并授权(关键步骤!) +CREATE USER IF NOT EXISTS 'mail_user'@'%' IDENTIFIED BY 'user123'; +GRANT ALL PRIVILEGES ON mail_server.* TO 'mail_user'@'%'; +FLUSH PRIVILEGES; + +-- ============================================ +-- 核心功能表 +-- ============================================ + +-- 用户表 +CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(100) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + is_admin TINYINT(1) DEFAULT 0, + is_active TINYINT(1) DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- 邮件表(兼容两种存储方式:SimpleSmtpServer用sender/recipient,SmtpHandler用sender_id/recipient_id) +CREATE TABLE IF NOT EXISTS emails ( + id INT AUTO_INCREMENT PRIMARY KEY, + sender VARCHAR(100), + recipient VARCHAR(100), + sender_id INT, + recipient_id INT, + subject VARCHAR(200), + body TEXT, + headers TEXT, + size_bytes INT DEFAULT 0, + is_deleted TINYINT(1) DEFAULT 0, + is_read TINYINT(1) DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (sender_id) REFERENCES users(id) ON DELETE SET NULL, + FOREIGN KEY (recipient_id) REFERENCES users(id) ON DELETE SET NULL +) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE=InnoDB; + +-- 系统日志表 +CREATE TABLE IF NOT EXISTS server_logs ( + id INT AUTO_INCREMENT PRIMARY KEY, + log_type VARCHAR(50), + message TEXT, + client_ip VARCHAR(45), + user_id INT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL +) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE=InnoDB; + +-- ============================================ +-- 管理功能表 +-- ============================================ + +-- 系统设置表 +CREATE TABLE IF NOT EXISTS system_settings ( + id INT AUTO_INCREMENT PRIMARY KEY, + setting_key VARCHAR(100) UNIQUE NOT NULL, + setting_value TEXT, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE=InnoDB; + +-- 过滤规则表 +CREATE TABLE IF NOT EXISTS filter_rules ( + id INT AUTO_INCREMENT PRIMARY KEY, + rule_type ENUM('email', 'ip') NOT NULL, + rule_value VARCHAR(255) NOT NULL, + action ENUM('block', 'allow') DEFAULT 'block', + description TEXT, + is_active TINYINT(1) DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY unique_rule (rule_type, rule_value) +) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE=InnoDB; + +-- 服务状态表 +CREATE TABLE IF NOT EXISTS service_status ( + id INT AUTO_INCREMENT PRIMARY KEY, + service_name VARCHAR(50) UNIQUE NOT NULL, + is_running TINYINT(1) DEFAULT 0, + pid INT, + last_started_at TIMESTAMP NULL, + last_stopped_at TIMESTAMP NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE=InnoDB; + +-- 用户邮箱大小限制表 +CREATE TABLE IF NOT EXISTS user_mailbox_limits ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + size_limit_bytes BIGINT DEFAULT 104857600, -- 100MB + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + UNIQUE KEY unique_user (user_id) +) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE=InnoDB; + +-- ============================================ +-- 插入默认数据 +-- ============================================ + +-- 插入测试用户(密码都是:123456) +INSERT INTO users (username, password_hash, is_admin, is_active) VALUES +('admin@test.com', '$2y$10$jB21V61k9aLAyp5.5qBpV.L70Aq6.XrtJrvlNI28bOXeJboLBJwoq', 1, 1), +('user1@test.com', '$2y$10$jB21V61k9aLAyp5.5qBpV.L70Aq6.XrtJrvlNI28bOXeJboLBJwoq', 0, 1) +ON DUPLICATE KEY UPDATE username = VALUES(username); + +-- 插入测试邮件 +INSERT INTO emails (sender, recipient, subject, body) VALUES +('admin@test.com', 'user1@test.com', '欢迎使用邮件系统', '这是第一封测试邮件'), +('user1@test.com', 'admin@test.com', '回复测试', '我收到了,谢谢!') +ON DUPLICATE KEY UPDATE id = id; + +-- 插入系统默认设置 +INSERT INTO system_settings (setting_key, setting_value) VALUES +('smtp_port', '25'), +('pop3_port', '110'), +('domain', 'test.com'), +('mailbox_size_limit', '104857600'), -- 100MB +('smtp_enabled', '1'), +('pop3_enabled', '1'), +('log_path', '/var/log/mailserver'), +('log_max_size', '10485760') -- 10MB +ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value); + +-- 插入服务状态初始记录 +INSERT INTO service_status (service_name, is_running) VALUES +('smtp', 0), +('pop3', 0) +ON DUPLICATE KEY UPDATE service_name = VALUES(service_name); + diff --git a/scripts/manage_user.php b/scripts/manage_user.php deleted file mode 100644 index 97463fd..0000000 --- a/scripts/manage_user.php +++ /dev/null @@ -1,196 +0,0 @@ - [参数...] - */ - -// 加载配置和类 -require_once __DIR__ . '/../config/database.php'; - -class AdminTool -{ - private $db; - - public function __construct() - { - $config = require __DIR__ . '/../config/database.php'; - $dsn = "mysql:host={$config['host']};port={$config['port']};dbname={$config['database']}"; - $this->db = new PDO($dsn, $config['username'], $config['password'], $config['options']); - } - - // 主入口 - public function run($args) - { - if (count($args) < 2) { - $this->showHelp(); - return; - } - - $command = $args[1]; - - switch ($command) { - case 'list': - $this->listUsers(); - break; - case 'create': - $this->createUser($args); - break; - case 'enable': - case 'disable': - $this->toggleUser($args, $command === 'enable'); - break; - case 'delete': - $this->deleteUser($args); - break; - case 'help': - default: - $this->showHelp(); - } - } - - // 显示用户列表 - private function listUsers() - { - $stmt = $this->db->query(" - SELECT id, username, is_admin, is_active, max_mailbox_size, created_at - FROM users ORDER BY id - "); - $users = $stmt->fetchAll(PDO::FETCH_ASSOC); - - if (empty($users)) { - echo "暂无用户\n"; - return; - } - - echo "📋 用户列表 (" . count($users) . " 位)\n"; - echo str_repeat("=", 70) . "\n"; - - foreach ($users as $user) { - printf("ID: %-4d | 邮箱: %-25s | 类型: %-6s | 状态: %-4s | 容量: %dMB | 创建: %s\n", - $user['id'], - $user['username'], - $user['is_admin'] ? '管理员' : '普通', - $user['is_active'] ? '激活' : '禁用', - $user['max_mailbox_size'], - $user['created_at'] - ); - } - } - - // 创建用户 - private function createUser($args) - { - if (count($args) < 4) { - echo "❌ 用法: php admin_tool.php create <邮箱> <密码> [管理员=0] [容量=100]\n"; - echo " 示例: php admin_tool.php create user@test.com pass123 1 200\n"; - return; - } - - $username = $args[2]; - $password = $args[3]; - $isAdmin = isset($args[4]) ? (bool)$args[4] : false; - $mailboxSize = isset($args[5]) ? (int)$args[5] : 100; - - // 检查用户是否存在 - $check = $this->db->prepare("SELECT COUNT(*) FROM users WHERE username = ?"); - $check->execute([$username]); - - if ($check->fetchColumn() > 0) { - echo "❌ 用户 {$username} 已存在\n"; - return; - } - - // 创建用户 - $stmt = $this->db->prepare(" - INSERT INTO users (username, password_hash, is_admin, max_mailbox_size) - VALUES (?, ?, ?, ?) - "); - - $hashedPassword = password_hash($password, PASSWORD_DEFAULT); - $success = $stmt->execute([$username, $hashedPassword, $isAdmin ? 1 : 0, $mailboxSize]); - - if ($success) { - echo "✅ 用户创建成功: {$username}\n"; - echo " 类型: " . ($isAdmin ? "管理员" : "普通用户") . "\n"; - echo " 邮箱容量: {$mailboxSize}MB\n"; - } else { - echo "❌ 创建失败\n"; - } - } - - // 启用/禁用用户 - private function toggleUser($args, $enable) - { - if (count($args) < 3) { - echo "❌ 用法: php admin_tool.php " . ($enable ? "enable" : "disable") . " <用户ID>\n"; - echo " 示例: php admin_tool.php " . ($enable ? "enable" : "disable") . " 2\n"; - return; - } - - $userId = (int)$args[2]; - $status = $enable ? 1 : 0; - $action = $enable ? "启用" : "禁用"; - - $stmt = $this->db->prepare("UPDATE users SET is_active = ? WHERE id = ?"); - $success = $stmt->execute([$status, $userId]); - - if ($success && $stmt->rowCount() > 0) { - echo "✅ 用户 ID {$userId} 已{$action}\n"; - } else { - echo "❌ 操作失败(用户可能不存在)\n"; - } - } - - // 删除用户 - private function deleteUser($args) - { - if (count($args) < 3) { - echo " 用法: php admin_tool.php delete <用户ID>\n"; - echo " 示例: php admin_tool.php delete 2\n"; - return; - } - - $userId = (int)$args[2]; - - echo " 确认删除用户 ID {$userId}?(y/N): "; - $confirm = trim(fgets(STDIN)); - - if (strtolower($confirm) !== 'y') { - echo "操作已取消\n"; - return; - } - - $stmt = $this->db->prepare("DELETE FROM users WHERE id = ?"); - $success = $stmt->execute([$userId]); - - if ($success && $stmt->rowCount() > 0) { - echo "用户 ID {$userId} 已删除\n"; - } else { - echo "删除失败(用户可能不存在)\n"; - } - } - - // 显示帮助 - private function showHelp() - { - echo "邮件服务器管理工具\n"; - echo str_repeat("=", 40) . "\n"; - echo "命令列表:\n"; - echo " list - 显示所有用户\n"; - echo " create <邮箱> <密码> [管理员] [容量] - 创建用户\n"; - echo " enable <用户ID> - 启用用户\n"; - echo " disable <用户ID> - 禁用用户\n"; - echo " delete <用户ID> - 删除用户\n"; - echo " help - 显示此帮助\n"; - echo "\n示例:\n"; - echo " php admin_tool.php list\n"; - echo " php admin_tool.php create user@test.com password123 0 200\n"; - echo " php admin_tool.php disable 2\n"; - echo " php admin_tool.php delete 3\n"; - } -} - -// 运行工具 -$tool = new AdminTool(); -$tool->run($argv); -?> \ No newline at end of file diff --git a/scripts/start_pop3.php b/scripts/start_pop3.php index 9217123..3f232d5 100644 --- a/scripts/start_pop3.php +++ b/scripts/start_pop3.php @@ -1,18 +1,18 @@ -#!/usr/bin/env php -start(); +#!/usr/bin/env php +start(); ?> \ No newline at end of file diff --git a/scripts/start_smtp.php b/scripts/start_smtp.php index e216506..1e96c1f 100644 --- a/scripts/start_smtp.php +++ b/scripts/start_smtp.php @@ -1,18 +1,18 @@ -#!/usr/bin/env php -start(); +#!/usr/bin/env php +start(); ?> \ No newline at end of file diff --git a/scripts/test_register.php b/scripts/test_register.php index 43d3c6c..9c88387 100644 --- a/scripts/test_register.php +++ b/scripts/test_register.php @@ -1,106 +1,106 @@ - true, - 'invalid-email' => false, - 'test@test.com' => true, - 'user@wrong.com' => false, - ]; - - foreach ($testEmails as $email => $expected) { - $isValid = Validator::validateEmail($email); - $domainValid = Validator::validateEmailDomain($email, 'test.com'); - $result = $isValid && ($expected ? $domainValid : !$domainValid); - echo " {$email}: " . ($result ? "✓" : "✗") . "\n"; - } - - // 测试2: 验证密码强度 - echo "\n测试2: 验证密码强度\n"; - $testPasswords = [ - '12345' => false, // 太短 - '123456' => true, // 符合最小长度 - 'password123' => true, - ]; - - foreach ($testPasswords as $password => $expected) { - $validation = Validator::validatePassword($password, 6); - $result = $validation['valid'] === $expected; - echo " '{$password}': " . ($result ? "✓" : "✗") . "\n"; - if (!$validation['valid']) { - echo " 错误: " . implode(', ', $validation['errors']) . "\n"; - } - } - - // 测试3: 检查用户名是否存在 - echo "\n测试3: 检查用户名是否存在\n"; - $existingUser = $userRepo->findByUsername('admin@test.com'); - if ($existingUser) { - echo " admin@test.com 存在: ✓\n"; - } else { - echo " admin@test.com 不存在: ✗\n"; - } - - // 测试4: 创建测试用户(如果不存在) - echo "\n测试4: 创建测试用户\n"; - $testUsername = 'testuser@test.com'; - - if ($userRepo->usernameExists($testUsername)) { - echo " 测试用户已存在,跳过创建\n"; - } else { - try { - $newUser = $userRepo->create($testUsername, 'test123456', false, true); - echo " 创建用户成功: ✓\n"; - echo " 用户ID: {$newUser['id']}\n"; - echo " 用户名: {$newUser['username']}\n"; - echo " 是否管理员: " . ($newUser['is_admin'] ? '是' : '否') . "\n"; - } catch (Exception $e) { - echo " 创建用户失败: ✗ - " . $e->getMessage() . "\n"; - } - } - - // 测试5: 验证密码 - echo "\n测试5: 验证密码\n"; - $testUser = $userRepo->findByUsername($testUsername); - if ($testUser) { - $verified = $userRepo->verifyPassword($testUsername, 'test123456'); - if ($verified) { - echo " 密码验证成功: ✓\n"; - } else { - echo " 密码验证失败: ✗\n"; - } - } - - // 测试6: 获取所有用户 - echo "\n测试6: 获取用户列表\n"; - $users = $userRepo->getAll(10); - echo " 用户总数: " . count($users) . "\n"; - foreach ($users as $user) { - echo " - {$user['username']} (ID: {$user['id']}, " . - ($user['is_admin'] ? '管理员' : '普通用户') . ", " . - ($user['is_active'] ? '激活' : '禁用') . ")\n"; - } - - echo "\n=== 测试完成 ===\n"; - -} catch (Exception $e) { - echo "错误: " . $e->getMessage() . "\n"; - echo "堆栈跟踪:\n" . $e->getTraceAsString() . "\n"; -} - + true, + 'invalid-email' => false, + 'test@test.com' => true, + 'user@wrong.com' => false, + ]; + + foreach ($testEmails as $email => $expected) { + $isValid = Validator::validateEmail($email); + $domainValid = Validator::validateEmailDomain($email, 'test.com'); + $result = $isValid && ($expected ? $domainValid : !$domainValid); + echo " {$email}: " . ($result ? "✓" : "✗") . "\n"; + } + + // 测试2: 验证密码强度 + echo "\n测试2: 验证密码强度\n"; + $testPasswords = [ + '12345' => false, // 太短 + '123456' => true, // 符合最小长度 + 'password123' => true, + ]; + + foreach ($testPasswords as $password => $expected) { + $validation = Validator::validatePassword($password, 6); + $result = $validation['valid'] === $expected; + echo " '{$password}': " . ($result ? "✓" : "✗") . "\n"; + if (!$validation['valid']) { + echo " 错误: " . implode(', ', $validation['errors']) . "\n"; + } + } + + // 测试3: 检查用户名是否存在 + echo "\n测试3: 检查用户名是否存在\n"; + $existingUser = $userRepo->findByUsername('admin@test.com'); + if ($existingUser) { + echo " admin@test.com 存在: ✓\n"; + } else { + echo " admin@test.com 不存在: ✗\n"; + } + + // 测试4: 创建测试用户(如果不存在) + echo "\n测试4: 创建测试用户\n"; + $testUsername = 'testuser@test.com'; + + if ($userRepo->usernameExists($testUsername)) { + echo " 测试用户已存在,跳过创建\n"; + } else { + try { + $newUser = $userRepo->create($testUsername, 'test123456', false, true); + echo " 创建用户成功: ✓\n"; + echo " 用户ID: {$newUser['id']}\n"; + echo " 用户名: {$newUser['username']}\n"; + echo " 是否管理员: " . ($newUser['is_admin'] ? '是' : '否') . "\n"; + } catch (Exception $e) { + echo " 创建用户失败: ✗ - " . $e->getMessage() . "\n"; + } + } + + // 测试5: 验证密码 + echo "\n测试5: 验证密码\n"; + $testUser = $userRepo->findByUsername($testUsername); + if ($testUser) { + $verified = $userRepo->verifyPassword($testUsername, 'test123456'); + if ($verified) { + echo " 密码验证成功: ✓\n"; + } else { + echo " 密码验证失败: ✗\n"; + } + } + + // 测试6: 获取所有用户 + echo "\n测试6: 获取用户列表\n"; + $users = $userRepo->getAll(10); + echo " 用户总数: " . count($users) . "\n"; + foreach ($users as $user) { + echo " - {$user['username']} (ID: {$user['id']}, " . + ($user['is_admin'] ? '管理员' : '普通用户') . ", " . + ($user['is_active'] ? '激活' : '禁用') . ")\n"; + } + + echo "\n=== 测试完成 ===\n"; + +} catch (Exception $e) { + echo "错误: " . $e->getMessage() . "\n"; + echo "堆栈跟踪:\n" . $e->getTraceAsString() . "\n"; +} + diff --git a/src/admin/BroadcastService.php b/src/admin/BroadcastService.php index 0c85fd3..60e7109 100644 --- a/src/admin/BroadcastService.php +++ b/src/admin/BroadcastService.php @@ -1,150 +1,150 @@ -db = Database::getInstance(); - $this->userRepo = new UserRepository(); - $this->emailRepo = new EmailRepository(); - } - - /** - * 群发邮件给所有用户 - * @param string $senderEmail 发件人邮箱 - * @param string $subject 主题 - * @param string $body 内容 - * @return array ['success' => int, 'failed' => int, 'errors' => array] - */ - public function broadcastToAll($senderEmail, $subject, $body) { - $users = $this->userRepo->getAll(); - $sender = $this->userRepo->findByUsername($senderEmail); - - if (!$sender) { - return ['success' => 0, 'failed' => 0, 'errors' => ['发件人不存在']]; - } - - $success = 0; - $failed = 0; - $errors = []; - - foreach ($users as $user) { - // 跳过发件人自己 - if ($user['username'] === $senderEmail) { - continue; - } - - // 跳过禁用的用户 - if (!$user['is_active']) { - continue; - } - - try { - $this->sendEmail($sender['id'], $user['id'], $subject, $body); - $success++; - } catch (Exception $e) { - $failed++; - $errors[] = "发送给 {$user['username']} 失败: " . $e->getMessage(); - } - } - - return [ - 'success' => $success, - 'failed' => $failed, - 'errors' => $errors - ]; - } - - /** - * 群发邮件给指定用户列表 - * @param string $senderEmail 发件人邮箱 - * @param array $recipientEmails 收件人邮箱列表 - * @param string $subject 主题 - * @param string $body 内容 - * @return array ['success' => int, 'failed' => int, 'errors' => array] - */ - public function broadcastToUsers($senderEmail, $recipientEmails, $subject, $body) { - $sender = $this->userRepo->findByUsername($senderEmail); - - if (!$sender) { - return ['success' => 0, 'failed' => 0, 'errors' => ['发件人不存在']]; - } - - $success = 0; - $failed = 0; - $errors = []; - - foreach ($recipientEmails as $email) { - $recipient = $this->userRepo->findByUsername(trim($email)); - - if (!$recipient) { - $failed++; - $errors[] = "用户 {$email} 不存在"; - continue; - } - - if (!$recipient['is_active']) { - $failed++; - $errors[] = "用户 {$email} 已被禁用"; - continue; - } - - try { - $this->sendEmail($sender['id'], $recipient['id'], $subject, $body); - $success++; - } catch (Exception $e) { - $failed++; - $errors[] = "发送给 {$email} 失败: " . $e->getMessage(); - } - } - - return [ - 'success' => $success, - 'failed' => $failed, - 'errors' => $errors - ]; - } - - /** - * 发送邮件 - * @param int $senderId 发件人ID - * @param int $recipientId 收件人ID - * @param string $subject 主题 - * @param string $body 内容 - * @return bool 是否成功 - */ - private function sendEmail($senderId, $recipientId, $subject, $body) { - $sender = $this->userRepo->findById($senderId); - $recipient = $this->userRepo->findById($recipientId); - - if (!$sender || !$recipient) { - throw new Exception("发件人或收件人不存在"); - } - - $sizeBytes = strlen($subject) + strlen($body); - - $stmt = $this->db->prepare(" - INSERT INTO emails (sender_id, recipient_id, sender, recipient, subject, body, size_bytes, created_at) - VALUES (?, ?, ?, ?, ?, ?, ?, NOW()) - "); - - return $stmt->execute([ - $senderId, - $recipientId, - $sender['username'], - $recipient['username'], - $subject, - $body, - $sizeBytes - ]); - } -} - +db = Database::getInstance(); + $this->userRepo = new UserRepository(); + $this->emailRepo = new EmailRepository(); + } + + /** + * 群发邮件给所有用户 + * @param string $senderEmail 发件人邮箱 + * @param string $subject 主题 + * @param string $body 内容 + * @return array ['success' => int, 'failed' => int, 'errors' => array] + */ + public function broadcastToAll($senderEmail, $subject, $body) { + $users = $this->userRepo->getAll(); + $sender = $this->userRepo->findByUsername($senderEmail); + + if (!$sender) { + return ['success' => 0, 'failed' => 0, 'errors' => ['发件人不存在']]; + } + + $success = 0; + $failed = 0; + $errors = []; + + foreach ($users as $user) { + // 跳过发件人自己 + if ($user['username'] === $senderEmail) { + continue; + } + + // 跳过禁用的用户 + if (!$user['is_active']) { + continue; + } + + try { + $this->sendEmail($sender['id'], $user['id'], $subject, $body); + $success++; + } catch (Exception $e) { + $failed++; + $errors[] = "发送给 {$user['username']} 失败: " . $e->getMessage(); + } + } + + return [ + 'success' => $success, + 'failed' => $failed, + 'errors' => $errors + ]; + } + + /** + * 群发邮件给指定用户列表 + * @param string $senderEmail 发件人邮箱 + * @param array $recipientEmails 收件人邮箱列表 + * @param string $subject 主题 + * @param string $body 内容 + * @return array ['success' => int, 'failed' => int, 'errors' => array] + */ + public function broadcastToUsers($senderEmail, $recipientEmails, $subject, $body) { + $sender = $this->userRepo->findByUsername($senderEmail); + + if (!$sender) { + return ['success' => 0, 'failed' => 0, 'errors' => ['发件人不存在']]; + } + + $success = 0; + $failed = 0; + $errors = []; + + foreach ($recipientEmails as $email) { + $recipient = $this->userRepo->findByUsername(trim($email)); + + if (!$recipient) { + $failed++; + $errors[] = "用户 {$email} 不存在"; + continue; + } + + if (!$recipient['is_active']) { + $failed++; + $errors[] = "用户 {$email} 已被禁用"; + continue; + } + + try { + $this->sendEmail($sender['id'], $recipient['id'], $subject, $body); + $success++; + } catch (Exception $e) { + $failed++; + $errors[] = "发送给 {$email} 失败: " . $e->getMessage(); + } + } + + return [ + 'success' => $success, + 'failed' => $failed, + 'errors' => $errors + ]; + } + + /** + * 发送邮件 + * @param int $senderId 发件人ID + * @param int $recipientId 收件人ID + * @param string $subject 主题 + * @param string $body 内容 + * @return bool 是否成功 + */ + private function sendEmail($senderId, $recipientId, $subject, $body) { + $sender = $this->userRepo->findById($senderId); + $recipient = $this->userRepo->findById($recipientId); + + if (!$sender || !$recipient) { + throw new Exception("发件人或收件人不存在"); + } + + $sizeBytes = strlen($subject) + strlen($body); + + $stmt = $this->db->prepare(" + INSERT INTO emails (sender_id, recipient_id, sender, recipient, subject, body, size_bytes, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, NOW()) + "); + + return $stmt->execute([ + $senderId, + $recipientId, + $sender['username'], + $recipient['username'], + $subject, + $body, + $sizeBytes + ]); + } +} + diff --git a/src/protocol/Pop3Handler.php b/src/protocol/Pop3Handler.php deleted file mode 100644 index e69de29..0000000 diff --git a/src/protocol/Pop3Server.php b/src/protocol/Pop3Server.php index 8779219..f1f61ff 100644 --- a/src/protocol/Pop3Server.php +++ b/src/protocol/Pop3Server.php @@ -1,309 +1,309 @@ -connectDB(); - } - - private function connectDB() - { - try { - $this->db = new PDO( - 'mysql:host=127.0.0.1;port=3308;dbname=mail_server', - 'mail_user', - 'user123' - ); - $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - echo "数据库连接成功\n"; - } catch (PDOException $e) { - echo "数据库连接失败: " . $e->getMessage() . "\n"; - exit(1); - } - } - - public function start() - { - $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); - socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, 1); - socket_bind($this->socket, '0.0.0.0', 110); - socket_listen($this->socket, 5); - - $this->isRunning = true; - - while ($this->isRunning) { - $client = socket_accept($this->socket); - if ($client !== false) { - $this->handleClient($client); - socket_close($client); - $this->currentUser = null; // 重置用户状态 - } - } - } - - private function handleClient($client) - { - // 获取客户端IP地址 - socket_getpeername($client, $clientIp); - $clientIp = $clientIp ?: 'unknown'; - - // 记录连接日志 - $this->log("客户端连接", $clientIp); - - $this->send($client, "+OK POP3 Simple Server Ready"); - - $state = 'AUTH'; // 状态:AUTH -> TRANSACTION -> UPDATE - $this->deletedEmails = []; // 重置删除列表 - - try { - while (true) { - $input = socket_read($client, 1024); - if ($input === false || trim($input) === '') { - break; - } - - $input_trimmed = trim($input); - $command = strtoupper($input_trimmed); - echo "客户端: {$input_trimmed}\n"; - - if ($state === 'AUTH') { - // 认证阶段 - if (strpos($command, 'USER ') === 0) { - $username = trim(substr($input_trimmed, 5)); // 使用原始输入,保持大小写 - if ($this->userExists($username)) { - $this->currentUser = $username; - $this->send($client, "+OK User found"); - $this->log("用户认证: USER {$username}", $clientIp); - } else { - $this->send($client, "-ERR User not found"); - $this->log("用户不存在: {$username}", $clientIp); - } - } elseif (strpos($command, 'PASS ') === 0) { - if ($this->currentUser) { - // 提取密码 - $password = substr($input, 5); // 保留原始大小写 - $password = trim($password); - - // 验证密码 - if ($this->verifyPassword($this->currentUser, $password)) { - $this->loadUserEmails(); - $this->send($client, "+OK Logged in, {$this->userEmails['count']} messages"); - $state = 'TRANSACTION'; - $this->log("登录成功: {$this->currentUser}", $clientIp, $this->currentUserId); - } else { - $this->send($client, "-ERR Invalid password"); - $this->currentUser = null; // 重置用户状态 - $this->log("密码错误: {$this->currentUser}", $clientIp); - } - } else { - $this->send($client, "-ERR USER first"); - } - } elseif ($command === 'QUIT') { - $this->send($client, "+OK Bye"); - $this->log("客户端断开连接", $clientIp); - break; - } - } elseif ($state === 'TRANSACTION') { - // 事务阶段 - if ($command === 'STAT') { - // 统计时排除已标记删除的邮件 - $activeCount = $this->userEmails['count'] - count($this->deletedEmails); - $activeSize = $this->userEmails['total_size']; - foreach ($this->deletedEmails as $deletedId) { - if (isset($this->userEmails['emails'][$deletedId])) { - $activeSize -= $this->userEmails['emails'][$deletedId]['size']; - } - } - $this->send($client, "+OK {$activeCount} {$activeSize}"); - } elseif ($command === 'LIST') { - $activeCount = $this->userEmails['count'] - count($this->deletedEmails); - $response = "+OK {$activeCount} messages\n"; - foreach ($this->userEmails['emails'] as $id => $email) { - // 跳过已标记删除的邮件 - if (!in_array($id, $this->deletedEmails)) { - $response .= "{$id} {$email['size']}\n"; - } - } - $response .= "."; - $this->send($client, $response); - } elseif (strpos($command, 'RETR ') === 0) { - $msgId = intval(substr($command, 5)); - if (isset($this->userEmails['emails'][$msgId]) && !in_array($msgId, $this->deletedEmails)) { - $email = $this->userEmails['emails'][$msgId]; - $response = "+OK {$email['size']} octets\n"; - $response .= "From: {$email['sender']}\n"; - $response .= "To: {$email['recipient']}\n"; - $response .= "Subject: {$email['subject']}\n"; - $response .= "Date: {$email['date']}\n\n"; - $response .= $email['body'] . "\n."; - $this->send($client, $response); - $this->log("读取邮件: ID {$msgId}", $clientIp, $this->currentUserId); - } else { - $this->send($client, "-ERR No such message"); - } - } elseif (strpos($command, 'DELE ') === 0) { - // 删除邮件命令 - $msgId = intval(substr($command, 5)); - if (isset($this->userEmails['emails'][$msgId]) && !in_array($msgId, $this->deletedEmails)) { - $this->deletedEmails[] = $msgId; - $this->send($client, "+OK Message {$msgId} deleted"); - $this->log("标记删除邮件: ID {$msgId}", $clientIp, $this->currentUserId); - } else { - $this->send($client, "-ERR No such message"); - } - } elseif ($command === 'QUIT') { - // 在QUIT时执行实际删除 - $this->processDeletions(); - $this->send($client, "+OK Bye"); - $this->log("客户端断开连接", $clientIp, $this->currentUserId); - $state = 'UPDATE'; - break; - } else { - $this->send($client, "-ERR Unknown command"); - } - } - } - } catch (Exception $e) { - $this->log("处理客户端错误: " . $e->getMessage(), $clientIp); - } finally { - // 重置状态 - $this->currentUser = null; - $this->currentUserId = null; - $this->deletedEmails = []; - } - } - - private function send($client, $message) - { - socket_write($client, $message . "\r\n"); - echo "服务器: {$message}\n"; - } - - private function userExists($username) - { - $stmt = $this->db->prepare("SELECT id FROM users WHERE username = ? AND is_active = 1"); - $stmt->execute([$username]); - return $stmt->rowCount() > 0; - } - - private function verifyPassword($username, $password) - { - try { - $stmt = $this->db->prepare("SELECT password_hash FROM users WHERE username = ? AND is_active = 1"); - $stmt->execute([$username]); - $user = $stmt->fetch(PDO::FETCH_ASSOC); - - if ($user && isset($user['password_hash'])) { - return password_verify($password, $user['password_hash']); - } - - return false; - } catch (Exception $e) { - echo "密码验证错误: " . $e->getMessage() . "\n"; - return false; - } - } - - private function loadUserEmails() - { - $stmt = $this->db->prepare( - "SELECT id, sender, recipient, subject, body, created_at, size_bytes - FROM emails - WHERE recipient = ? AND is_deleted = 0 - ORDER BY created_at" - ); - $stmt->execute([$this->currentUser]); - - $this->userEmails = ['count' => 0, 'total_size' => 0, 'emails' => []]; - - while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { - $id = $row['id']; - // 使用数据库中的size_bytes,如果没有则估算 - $size = $row['size_bytes'] > 0 ? $row['size_bytes'] : (strlen($row['body']) + 100); - - $this->userEmails['emails'][$id] = [ - 'sender' => $row['sender'], - 'recipient' => $row['recipient'], - 'subject' => $row['subject'], - 'body' => $row['body'], - 'date' => $row['created_at'], - 'size' => $size - ]; - - $this->userEmails['count']++; - $this->userEmails['total_size'] += $size; - } - - // 获取用户ID - $userStmt = $this->db->prepare("SELECT id FROM users WHERE username = ?"); - $userStmt->execute([$this->currentUser]); - $user = $userStmt->fetch(); - $this->currentUserId = $user ? $user['id'] : null; - - echo "为用户 {$this->currentUser} 加载了 {$this->userEmails['count']} 封邮件\n"; - } - - /** - * 处理删除操作(在QUIT时执行) - */ - private function processDeletions() - { - if (empty($this->deletedEmails)) { - return; - } - - try { - $placeholders = implode(',', array_fill(0, count($this->deletedEmails), '?')); - $stmt = $this->db->prepare( - "UPDATE emails SET is_deleted = 1 WHERE id IN ({$placeholders}) AND recipient = ?" - ); - $params = array_merge($this->deletedEmails, [$this->currentUser]); - $stmt->execute($params); - - $deletedCount = $stmt->rowCount(); - echo "删除了 {$deletedCount} 封邮件\n"; - } catch (Exception $e) { - echo "删除邮件失败: " . $e->getMessage() . "\n"; - error_log("删除邮件错误: " . $e->getMessage()); - } - } - - /** - * 记录日志到数据库 - */ - private function log($message, $clientIp = 'unknown', $userId = null) - { - try { - $stmt = $this->db->prepare( - "INSERT INTO server_logs (log_type, message, client_ip, user_id) VALUES (?, ?, ?, ?)" - ); - $stmt->execute(['POP3', $message, $clientIp, $userId]); - } catch (Exception $e) { - // 日志记录失败不影响主流程 - error_log("日志记录失败: " . $e->getMessage()); - } - } -} - -// 如果直接运行这个文件 -if (basename(__FILE__) == basename($_SERVER['PHP_SELF'])) { - $server = new SimplePop3Server(); - $server->start(); -} +connectDB(); + } + + private function connectDB() + { + try { + $this->db = new PDO( + 'mysql:host=127.0.0.1;port=3308;dbname=mail_server', + 'mail_user', + 'user123' + ); + $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + echo "数据库连接成功\n"; + } catch (PDOException $e) { + echo "数据库连接失败: " . $e->getMessage() . "\n"; + exit(1); + } + } + + public function start() + { + $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, 1); + socket_bind($this->socket, '0.0.0.0', 110); + socket_listen($this->socket, 5); + + $this->isRunning = true; + + while ($this->isRunning) { + $client = socket_accept($this->socket); + if ($client !== false) { + $this->handleClient($client); + socket_close($client); + $this->currentUser = null; // 重置用户状态 + } + } + } + + private function handleClient($client) + { + // 获取客户端IP地址 + socket_getpeername($client, $clientIp); + $clientIp = $clientIp ?: 'unknown'; + + // 记录连接日志 + $this->log("客户端连接", $clientIp); + + $this->send($client, "+OK POP3 Simple Server Ready"); + + $state = 'AUTH'; // 状态:AUTH -> TRANSACTION -> UPDATE + $this->deletedEmails = []; // 重置删除列表 + + try { + while (true) { + $input = socket_read($client, 1024); + if ($input === false || trim($input) === '') { + break; + } + + $input_trimmed = trim($input); + $command = strtoupper($input_trimmed); + echo "客户端: {$input_trimmed}\n"; + + if ($state === 'AUTH') { + // 认证阶段 + if (strpos($command, 'USER ') === 0) { + $username = trim(substr($input_trimmed, 5)); // 使用原始输入,保持大小写 + if ($this->userExists($username)) { + $this->currentUser = $username; + $this->send($client, "+OK User found"); + $this->log("用户认证: USER {$username}", $clientIp); + } else { + $this->send($client, "-ERR User not found"); + $this->log("用户不存在: {$username}", $clientIp); + } + } elseif (strpos($command, 'PASS ') === 0) { + if ($this->currentUser) { + // 提取密码 + $password = substr($input, 5); // 保留原始大小写 + $password = trim($password); + + // 验证密码 + if ($this->verifyPassword($this->currentUser, $password)) { + $this->loadUserEmails(); + $this->send($client, "+OK Logged in, {$this->userEmails['count']} messages"); + $state = 'TRANSACTION'; + $this->log("登录成功: {$this->currentUser}", $clientIp, $this->currentUserId); + } else { + $this->send($client, "-ERR Invalid password"); + $this->currentUser = null; // 重置用户状态 + $this->log("密码错误: {$this->currentUser}", $clientIp); + } + } else { + $this->send($client, "-ERR USER first"); + } + } elseif ($command === 'QUIT') { + $this->send($client, "+OK Bye"); + $this->log("客户端断开连接", $clientIp); + break; + } + } elseif ($state === 'TRANSACTION') { + // 事务阶段 + if ($command === 'STAT') { + // 统计时排除已标记删除的邮件 + $activeCount = $this->userEmails['count'] - count($this->deletedEmails); + $activeSize = $this->userEmails['total_size']; + foreach ($this->deletedEmails as $deletedId) { + if (isset($this->userEmails['emails'][$deletedId])) { + $activeSize -= $this->userEmails['emails'][$deletedId]['size']; + } + } + $this->send($client, "+OK {$activeCount} {$activeSize}"); + } elseif ($command === 'LIST') { + $activeCount = $this->userEmails['count'] - count($this->deletedEmails); + $response = "+OK {$activeCount} messages\n"; + foreach ($this->userEmails['emails'] as $id => $email) { + // 跳过已标记删除的邮件 + if (!in_array($id, $this->deletedEmails)) { + $response .= "{$id} {$email['size']}\n"; + } + } + $response .= "."; + $this->send($client, $response); + } elseif (strpos($command, 'RETR ') === 0) { + $msgId = intval(substr($command, 5)); + if (isset($this->userEmails['emails'][$msgId]) && !in_array($msgId, $this->deletedEmails)) { + $email = $this->userEmails['emails'][$msgId]; + $response = "+OK {$email['size']} octets\n"; + $response .= "From: {$email['sender']}\n"; + $response .= "To: {$email['recipient']}\n"; + $response .= "Subject: {$email['subject']}\n"; + $response .= "Date: {$email['date']}\n\n"; + $response .= $email['body'] . "\n."; + $this->send($client, $response); + $this->log("读取邮件: ID {$msgId}", $clientIp, $this->currentUserId); + } else { + $this->send($client, "-ERR No such message"); + } + } elseif (strpos($command, 'DELE ') === 0) { + // 删除邮件命令 + $msgId = intval(substr($command, 5)); + if (isset($this->userEmails['emails'][$msgId]) && !in_array($msgId, $this->deletedEmails)) { + $this->deletedEmails[] = $msgId; + $this->send($client, "+OK Message {$msgId} deleted"); + $this->log("标记删除邮件: ID {$msgId}", $clientIp, $this->currentUserId); + } else { + $this->send($client, "-ERR No such message"); + } + } elseif ($command === 'QUIT') { + // 在QUIT时执行实际删除 + $this->processDeletions(); + $this->send($client, "+OK Bye"); + $this->log("客户端断开连接", $clientIp, $this->currentUserId); + $state = 'UPDATE'; + break; + } else { + $this->send($client, "-ERR Unknown command"); + } + } + } + } catch (Exception $e) { + $this->log("处理客户端错误: " . $e->getMessage(), $clientIp); + } finally { + // 重置状态 + $this->currentUser = null; + $this->currentUserId = null; + $this->deletedEmails = []; + } + } + + private function send($client, $message) + { + socket_write($client, $message . "\r\n"); + echo "服务器: {$message}\n"; + } + + private function userExists($username) + { + $stmt = $this->db->prepare("SELECT id FROM users WHERE username = ? AND is_active = 1"); + $stmt->execute([$username]); + return $stmt->rowCount() > 0; + } + + private function verifyPassword($username, $password) + { + try { + $stmt = $this->db->prepare("SELECT password_hash FROM users WHERE username = ? AND is_active = 1"); + $stmt->execute([$username]); + $user = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($user && isset($user['password_hash'])) { + return password_verify($password, $user['password_hash']); + } + + return false; + } catch (Exception $e) { + echo "密码验证错误: " . $e->getMessage() . "\n"; + return false; + } + } + + private function loadUserEmails() + { + $stmt = $this->db->prepare( + "SELECT id, sender, recipient, subject, body, created_at, size_bytes + FROM emails + WHERE recipient = ? AND is_deleted = 0 + ORDER BY created_at" + ); + $stmt->execute([$this->currentUser]); + + $this->userEmails = ['count' => 0, 'total_size' => 0, 'emails' => []]; + + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + $id = $row['id']; + // 使用数据库中的size_bytes,如果没有则估算 + $size = $row['size_bytes'] > 0 ? $row['size_bytes'] : (strlen($row['body']) + 100); + + $this->userEmails['emails'][$id] = [ + 'sender' => $row['sender'], + 'recipient' => $row['recipient'], + 'subject' => $row['subject'], + 'body' => $row['body'], + 'date' => $row['created_at'], + 'size' => $size + ]; + + $this->userEmails['count']++; + $this->userEmails['total_size'] += $size; + } + + // 获取用户ID + $userStmt = $this->db->prepare("SELECT id FROM users WHERE username = ?"); + $userStmt->execute([$this->currentUser]); + $user = $userStmt->fetch(); + $this->currentUserId = $user ? $user['id'] : null; + + echo "为用户 {$this->currentUser} 加载了 {$this->userEmails['count']} 封邮件\n"; + } + + /** + * 处理删除操作(在QUIT时执行) + */ + private function processDeletions() + { + if (empty($this->deletedEmails)) { + return; + } + + try { + $placeholders = implode(',', array_fill(0, count($this->deletedEmails), '?')); + $stmt = $this->db->prepare( + "UPDATE emails SET is_deleted = 1 WHERE id IN ({$placeholders}) AND recipient = ?" + ); + $params = array_merge($this->deletedEmails, [$this->currentUser]); + $stmt->execute($params); + + $deletedCount = $stmt->rowCount(); + echo "删除了 {$deletedCount} 封邮件\n"; + } catch (Exception $e) { + echo "删除邮件失败: " . $e->getMessage() . "\n"; + error_log("删除邮件错误: " . $e->getMessage()); + } + } + + /** + * 记录日志到数据库 + */ + private function log($message, $clientIp = 'unknown', $userId = null) + { + try { + $stmt = $this->db->prepare( + "INSERT INTO server_logs (log_type, message, client_ip, user_id) VALUES (?, ?, ?, ?)" + ); + $stmt->execute(['POP3', $message, $clientIp, $userId]); + } catch (Exception $e) { + // 日志记录失败不影响主流程 + error_log("日志记录失败: " . $e->getMessage()); + } + } +} + +// 如果直接运行这个文件 +if (basename(__FILE__) == basename($_SERVER['PHP_SELF'])) { + $server = new SimplePop3Server(); + $server->start(); +} ?> \ No newline at end of file diff --git a/src/protocol/SmtpHandler.php b/src/protocol/SmtpHandler.php deleted file mode 100644 index 0fd042d..0000000 --- a/src/protocol/SmtpHandler.php +++ /dev/null @@ -1,220 +0,0 @@ -state) { - case 'WAIT_HELO': - return $this->handleHelo($command); - - case 'WAIT_MAIL': - return $this->handleMail($command); - - case 'WAIT_RCPT': - return $this->handleRcpt($command); - - case 'WAIT_DATA': - return $this->handleDataCommand($command); - - case 'IN_DATA': - return $this->handleDataContent($command); - - default: - return "500 Unknown state"; - } - } - - private function handleHelo($command) { - if (substr($command, 0, 4) === 'HELO' || substr($command, 0, 4) === 'EHLO') { - $this->state = 'WAIT_MAIL'; - return "250 " . SERVER_DOMAIN . " Hello"; - } elseif ($command === 'QUIT') { - return "221 Bye"; - } else { - return "503 Send HELO/EHLO first"; - } - } - - private function handleMail($command) { - if (substr($command, 0, 10) === 'MAIL FROM:') { - $this->from = $this->extractEmail($command); - - // 验证发件人是否存在(简单实现) - if ($this->validateEmail($this->from)) { - $this->state = 'WAIT_RCPT'; - $this->to = []; // 清空收件人列表 - return "250 Sender OK"; - } else { - return "550 Sender not permitted"; - } - } elseif ($command === 'QUIT') { - return "221 Bye"; - } else { - return "503 Need MAIL command"; - } - } - - private function handleRcpt($command) { - if (substr($command, 0, 8) === 'RCPT TO:') { - $recipient = $this->extractEmail($command); - - // 验证收件人是否存在 - if ($this->validateEmail($recipient)) { - $this->to[] = $recipient; - $this->state = 'WAIT_RCPT'; - return "250 Recipient OK"; - } else { - return "550 Recipient not found"; - } - } elseif (substr($command, 0, 4) === 'DATA') { - if (count($this->to) > 0) { - $this->state = 'WAIT_DATA'; - return "354 Start mail input; end with ."; - } else { - return "503 Need RCPT command"; - } - } elseif ($command === 'QUIT') { - return "221 Bye"; - } else { - return "503 Need RCPT or DATA command"; - } - } - - private function handleDataCommand($command) { - // 在WAIT_DATA状态,我们等待真正的数据内容 - // 任何命令都会进入数据处理状态 - $this->state = 'IN_DATA'; - $this->data = $command . "\r\n"; - return null; // 不发送响应,等待数据结束 - } - - private function handleDataContent($command) { - // 检查是否收到结束标记(单独一行的.) - if ($command === '.') { - // 保存邮件到数据库 - $this->saveEmail(); - - // 重置状态 - $this->state = 'WAIT_HELO'; - $this->from = ''; - $this->to = []; - $this->data = ''; - - return "250 Mail accepted for delivery"; - } else { - // 累积邮件数据 - $this->data .= $command . "\r\n"; - return null; // 不发送响应,继续接收数据 - } - } - - private function extractEmail($command) { - // 从类似 "MAIL FROM:" 中提取邮箱 - if (preg_match('/<([^>]+)>/', $command, $matches)) { - return $matches[1]; - } - return ''; - } - - private function validateEmail($email) { - // 简单验证:检查格式和是否在用户表中存在 - if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { - return false; - } - - // 查询数据库(简化实现) - try { - $db = Database::getInstance(); - $stmt = $db->prepare("SELECT id FROM users WHERE username = ? AND is_active = 1"); - $stmt->execute([$email]); - return $stmt->rowCount() > 0; - } catch (Exception $e) { - return false; - } - } - - private function saveEmail() { - try { - $db = Database::getInstance(); - - // 解析邮件数据(简化版) - $lines = explode("\r\n", $this->data); - $headers = []; - $body = ''; - $inBody = false; - - foreach ($lines as $line) { - if (!$inBody && trim($line) === '') { - $inBody = true; - continue; - } - - if ($inBody) { - $body .= $line . "\r\n"; - } else { - $headers[] = $line; - } - } - - // 获取发件人ID - $stmt = $db->prepare("SELECT id FROM users WHERE username = ?"); - $stmt->execute([$this->from]); - $sender = $stmt->fetch(); - - if (!$sender) { - return false; - } - - // 为每个收件人保存邮件 - foreach ($this->to as $recipientEmail) { - $stmt = $db->prepare("SELECT id FROM users WHERE username = ?"); - $stmt->execute([$recipientEmail]); - $recipient = $stmt->fetch(); - - if ($recipient) { - $subject = $this->extractHeader($headers, 'Subject') ?: '(No Subject)'; - - $insertStmt = $db->prepare(" - INSERT INTO emails - (sender_id, recipient_id, subject, body, headers, size_bytes) - VALUES (?, ?, ?, ?, ?, ?) - "); - - $emailData = implode("\r\n", $headers) . "\r\n\r\n" . $body; - - $insertStmt->execute([ - $sender['id'], - $recipient['id'], - $subject, - $body, - implode("\r\n", $headers), - strlen($emailData) - ]); - } - } - - return true; - } catch (Exception $e) { - error_log("保存邮件失败: " . $e->getMessage()); - return false; - } - } - - private function extractHeader($headers, $name) { - foreach ($headers as $header) { - if (stripos($header, $name . ':') === 0) { - return trim(substr($header, strlen($name) + 1)); - } - } - return null; - } -} \ No newline at end of file diff --git a/src/protocol/SmtpServer.php b/src/protocol/SmtpServer.php index 8a23f5e..7670780 100644 --- a/src/protocol/SmtpServer.php +++ b/src/protocol/SmtpServer.php @@ -1,445 +1,455 @@ -connectDB(); - - // 初始化Repository - $this->filterRepo = new FilterRepository(); - $this->mailboxRepo = new MailboxRepository(); - $this->userRepo = new UserRepository(); - } - - private function connectDB() - { - try { - $this->db = new PDO( - 'mysql:host=127.0.0.1;port=3308;dbname=mail_server', - 'mail_user', - 'user123', - [ - PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci", - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC - ] - ); - // $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - echo "数据库连接成功\n"; - } catch (PDOException $e) { - echo "数据库连接失败: " . $e->getMessage() . "\n"; - exit(1); - } - } - - public function start() - { - // 创建socket(邮局开门) - $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); - socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, 1); - socket_bind($this->socket, '0.0.0.0', 25); - socket_listen($this->socket, 5); - - $this->isRunning = true; - - while ($this->isRunning) { - // 等待客户连接(有人来寄信) - $client = socket_accept($this->socket); - if ($client !== false) { - $this->handleClient($client); - socket_close($client); - } - } - - socket_close($this->socket); - } - - private function handleClient($client) - { - // 获取客户端IP地址 - socket_getpeername($client, $clientIp); - $clientIp = $clientIp ?: 'unknown'; - - // 记录连接日志 - $this->log("客户端连接", $clientIp); - - try { - // 1. 说欢迎语 - $this->send($client, "220 mail.simple.com SMTP Ready"); - - // 2. 等待客户说 HELO - $this->waitForCommand($client, 'HELO', 'HELO或EHLO'); - - // 3. 检查IP过滤规则 - if ($this->filterRepo->isIPBlocked($clientIp)) { - $this->send($client, "550 IP address blocked"); - $this->log("IP被阻止: {$clientIp}", $clientIp); - return; - } - - $this->send($client, "250 OK - Hello"); - - // 4. 问:谁寄的? - $from = $this->waitForCommand($client, 'MAIL FROM:', '发件人邮箱'); - - // 检查发件人邮箱过滤规则 - if ($this->filterRepo->isEmailBlocked($from)) { - $this->send($client, "550 Sender email blocked"); - $this->log("发件人邮箱被阻止: {$from}", $clientIp); - return; - } - - $this->send($client, "250 Sender OK"); - - // 5. 支持多收件人 - $recipients = []; - $this->send($client, "250 Recipient OK"); - - // 循环接收多个RCPT TO命令 - while (true) { - $input = socket_read($client, 1024); - if ($input === false) break; - - $input = trim($input); - echo "客户端: {$input}\n"; - - if (stripos($input, 'RCPT TO:') === 0) { - // 提取收件人邮箱 - if (preg_match('/<(.+?)>/', $input, $matches)) { - $to = $matches[1]; - - // 检查收件人邮箱过滤规则 - if ($this->filterRepo->isEmailBlocked($to)) { - $this->send($client, "550 Recipient email blocked"); - $this->log("收件人邮箱被阻止: {$to}", $clientIp); - continue; - } - - // 初步检查收件人邮箱大小限制(使用估算值,实际检查在接收邮件内容后) - $user = $this->userRepo->findByUsername($to); - if ($user) { - $usage = $this->mailboxRepo->getUsage($user['id']); - $estimatedSize = 50000; // 估算邮件大小50KB - - if ($usage['used'] + $estimatedSize > $usage['limit']) { - $this->send($client, "552 Mailbox full"); - $this->log("收件人邮箱已满(初步检查): {$to}", $clientIp); - continue; - } - } - - $recipients[] = $to; - $this->send($client, "250 Recipient OK"); - } - } elseif (stripos($input, 'DATA') === 0) { - // 收到DATA命令,跳出循环 - break; - } elseif (strtoupper($input) === 'QUIT') { - $this->send($client, "221 Bye"); - return; - } else { - $this->send($client, "500 Error: Expected RCPT TO or DATA"); - } - } - - if (empty($recipients)) { - $this->send($client, "503 No valid recipients"); - $this->log("没有有效收件人", $clientIp); - return; - } - - // 6. 说:开始写内容吧 - $this->send($client, "354 Start mail input, end with ."); - - // 7. 接收邮件内容 - $emailContent = $this->receiveEmailContent($client); - $emailSize = strlen($emailContent); - - // 8. 再次检查邮箱大小限制(使用实际邮件大小) - $validRecipients = []; - foreach ($recipients as $to) { - $user = $this->userRepo->findByUsername($to); - if ($user) { - $usage = $this->mailboxRepo->getUsage($user['id']); - if ($usage['used'] + $emailSize > $usage['limit']) { - $this->log("收件人邮箱已满(实际检查): {$to}", $clientIp); - continue; - } - } - $validRecipients[] = $to; - } - - if (empty($validRecipients)) { - $this->send($client, "552 All recipients' mailboxes are full"); - $this->log("所有收件人邮箱已满", $clientIp); - return; - } - - // 9. 保存到数据库(支持多收件人) - $successCount = 0; - foreach ($validRecipients as $to) { - if ($this->saveEmail($from, $to, $emailContent, $clientIp, $emailSize)) { - $successCount++; - } - } - - // 10. 告诉客户:收到了 - if ($successCount > 0) { - $this->send($client, "250 Mail accepted"); - $this->log("邮件发送成功: {$from} -> " . implode(', ', $validRecipients) . " ({$successCount}个收件人)", $clientIp); - } else { - $this->send($client, "550 Mail delivery failed"); - $this->log("邮件发送失败: {$from} -> " . implode(', ', $validRecipients), $clientIp); - } - - // 11. 等客户说再见 - $this->waitForCommand($client, 'QUIT', 'QUIT'); - $this->send($client, "221 Bye"); - - echo "收到一封邮件:{$from} -> " . implode(', ', $validRecipients) . "\n"; - - } catch (Exception $e) { - $this->log("处理客户端错误: " . $e->getMessage(), $clientIp); - $this->send($client, "500 Internal server error"); - } - } - - private function send($client, $message) - { - socket_write($client, $message . "\r\n"); - } - - private function waitForCommand($client, $expected, $description) - { - while (true) { - $input = socket_read($client, 1024); - if ($input === false) break; - - $input = trim($input); - echo "客户端: {$input}\n"; - - if (stripos($input, $expected) === 0) { - // 提取邮箱地址 - if (preg_match('/<(.+?)>/', $input, $matches)) { - return $matches[1]; - } - return $input; - } - - // 如果收到QUIT,直接退出 - if (strtoupper($input) === 'QUIT') { - $this->send($client, "221 Bye"); - exit(0); - } - - $this->send($client, "500 Error: Expected {$description}"); - } - } - - private function receiveEmailContent($client) - { - $content = ""; - - $buffer = ""; - - while (true) { - $data = socket_read($client, 1024, PHP_BINARY_READ); - if ($data === false || $data === '') { - break; - } - - $buffer .= $data; - - // 按行处理 - while (($pos = strpos($buffer, "\r\n")) !== false) { - $line = substr($buffer, 0, $pos); - $buffer = substr($buffer, $pos + 2); - - // 如果遇到单独一行的 '.' 就结束 - if (trim($line) === '.') { - return $content; - } - - $content .= $line . "\r\n"; - } - } - - return $content; - } - - private function saveEmail($from, $to, $content, $clientIp = 'unknown', $emailSize = null) - { - try { - // 1. 解析邮件内容(先不转换编码) - $lines = explode("\r\n", $content); - $subject = "无主题"; - $body = ""; - $inBody = false; - - foreach ($lines as $line) { - if (!$inBody && stripos($line, 'Subject:') === 0) { - $subject = trim(substr($line, 8)); - // 解码MIME编码的主题 - $subject = $this->decodeMimeHeader($subject); - } - - if (!$inBody && trim($line) === '') { - $inBody = true; - continue; - } - - if ($inBody) { - $body .= $line . "\n"; - } - } - - // 2. 清理正文 - $body = trim($body); - - // 3. 检测当前编码 - 直接假设为UTF-8 - $detectedEncoding = 'UTF-8'; // 直接指定,不检测了 - echo "使用编码: {$detectedEncoding}\n"; - - // 4. 验证确实是UTF-8,如果不是就转换 - if (!mb_check_encoding($subject, 'UTF-8')) { - $subject = mb_convert_encoding($subject, 'UTF-8', 'auto'); - } - if (!mb_check_encoding($body, 'UTF-8')) { - $body = mb_convert_encoding($body, 'UTF-8', 'auto'); - } - - // 5. 计算邮件大小(如果未提供则计算) - if ($emailSize === null) { - $emailSize = strlen($content); - } - - // 6. 获取收件人用户ID - $user = $this->userRepo->findByUsername($to); - $recipientId = $user ? $user['id'] : null; - - // 7. 使用参数绑定,确保UTF-8传输 - $stmt = $this->db->prepare( - "INSERT INTO emails (sender, recipient, recipient_id, subject, body, size_bytes) VALUES (?, ?, ?, ?, ?, ?)" - ); - - // 绑定参数时指定字符集 - $stmt->bindValue(1, $from, PDO::PARAM_STR); - $stmt->bindValue(2, $to, PDO::PARAM_STR); - $stmt->bindValue(3, $recipientId, PDO::PARAM_INT); - $stmt->bindValue(4, $subject, PDO::PARAM_STR); - $stmt->bindValue(5, $body, PDO::PARAM_STR); - $stmt->bindValue(6, $emailSize, PDO::PARAM_INT); - - $stmt->execute(); - - echo "邮件保存成功:{$from} -> {$to}\n"; - echo " 主题: {$subject}\n"; - echo " 长度: {$emailSize} 字节\n"; - - return true; - - } catch (Exception $e) { - echo "保存邮件失败: " . $e->getMessage() . "\n"; - error_log("邮件保存错误: " . $e->getMessage() . "\n" . $e->getTraceAsString()); - $this->log("保存邮件失败: " . $e->getMessage(), $clientIp); - return false; - } - } - - /** - * 记录日志到数据库 - */ - private function log($message, $clientIp = 'unknown', $userId = null) - { - try { - $stmt = $this->db->prepare( - "INSERT INTO server_logs (log_type, message, client_ip, user_id) VALUES (?, ?, ?, ?)" - ); - $stmt->execute(['SMTP', $message, $clientIp, $userId]); - } catch (Exception $e) { - // 日志记录失败不影响主流程 - error_log("日志记录失败: " . $e->getMessage()); - } - } - - // 添加MIME头解码方法 - private function decodeMimeHeader($header) - { - // 处理 =?UTF-8?B?5L2g5aW9?= 这样的MIME编码 - $decoded = ''; - $parts = preg_split('/(=\?[^?]+\?[BQ]\?[^?]+\?=)/i', $header, -1, PREG_SPLIT_DELIM_CAPTURE); - - foreach ($parts as $part) { - if (preg_match('/=\?([^\?]+)\?([BQ])\?([^\?]+)\?=/i', $part, $matches)) { - $charset = $matches[1]; - $encoding = strtoupper($matches[2]); - $text = $matches[3]; - - if ($encoding === 'B') { - // Base64解码 - $decodedText = base64_decode($text); - } elseif ($encoding === 'Q') { - // Quoted-Printable解码 - $decodedText = quoted_printable_decode(str_replace('_', ' ', $text)); - } - - if (isset($decodedText)) { - $decoded .= mb_convert_encoding($decodedText, 'UTF-8', $charset); - } - } else { - $decoded .= $part; - } - } - - return $decoded ?: $header; - } - - public function stop() - { - $this->isRunning = false; - } -} - -// 如果直接运行这个文件 -if (basename(__FILE__) == basename($_SERVER['PHP_SELF'])) { - // 检查是否有权限监听25端口(需要sudo) - if (posix_getuid() != 0) { - echo "注意:需要sudo权限监听25端口\n"; - echo "请运行:sudo php " . __FILE__ . "\n"; - exit(1); - } - - $server = new SimpleSmtpServer(); - $server->start(); -} +connectDB(); + + // 初始化Repository + $this->filterRepo = new FilterRepository(); + $this->mailboxRepo = new MailboxRepository(); + $this->userRepo = new UserRepository(); + } + + private function connectDB() + { + try { + $this->db = new PDO( + 'mysql:host=127.0.0.1;port=3308;dbname=mail_server', + 'mail_user', + 'user123', + [ + PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci", + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC + ] + ); + // $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + echo "数据库连接成功\n"; + } catch (PDOException $e) { + echo "数据库连接失败: " . $e->getMessage() . "\n"; + exit(1); + } + } + + public function start() + { + // 创建socket(邮局开门) + $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, 1); + socket_bind($this->socket, '0.0.0.0', 25); + socket_listen($this->socket, 5); + + $this->isRunning = true; + + while ($this->isRunning) { + // 等待客户连接(有人来寄信) + $client = socket_accept($this->socket); + if ($client !== false) { + $this->handleClient($client); + socket_close($client); + } + } + + socket_close($this->socket); + } + + private function handleClient($client) + { + // 获取客户端IP地址 + socket_getpeername($client, $clientIp); + $clientIp = $clientIp ?: 'unknown'; + + // 记录连接日志 + $this->log("客户端连接", $clientIp); + + try { + // 1. 说欢迎语 + $this->send($client, "220 mail.simple.com SMTP Ready"); + + // 2. 等待客户说 HELO + $this->waitForCommand($client, 'HELO', 'HELO或EHLO'); + + // 3. 检查IP过滤规则 + if ($this->filterRepo->isIPBlocked($clientIp)) { + $this->send($client, "550 IP address blocked"); + $this->log("IP被阻止: {$clientIp}", $clientIp); + return; + } + + $this->send($client, "250 OK - Hello"); + + // 4. 问:谁寄的? + $from = $this->waitForCommand($client, 'MAIL FROM:', '发件人邮箱'); + + // 检查发件人邮箱过滤规则 + if ($this->filterRepo->isEmailBlocked($from)) { + $this->send($client, "550 Sender email blocked"); + $this->log("发件人邮箱被阻止: {$from}", $clientIp); + return; + } + + $this->send($client, "250 Sender OK"); + + // 5. 支持多收件人 + $recipients = []; + $this->send($client, "250 Recipient OK"); + + // 循环接收多个RCPT TO命令 + while (true) { + $input = socket_read($client, 1024); + if ($input === false) break; + + $input = trim($input); + echo "客户端: {$input}\n"; + + if (stripos($input, 'RCPT TO:') === 0) { + // 提取收件人邮箱 + if (preg_match('/<(.+?)>/', $input, $matches)) { + $to = $matches[1]; + + // 检查收件人邮箱过滤规则 + if ($this->filterRepo->isEmailBlocked($to)) { + $this->send($client, "550 Recipient email blocked"); + $this->log("收件人邮箱被阻止: {$to}", $clientIp); + continue; + } + + // 初步检查收件人邮箱大小限制(使用估算值,实际检查在接收邮件内容后) + $user = $this->userRepo->findByUsername($to); + if ($user) { + $usage = $this->mailboxRepo->getUsage($user['id']); + $estimatedSize = 50000; // 估算邮件大小50KB + + if ($usage['used'] + $estimatedSize > $usage['limit']) { + $this->send($client, "552 Mailbox full"); + $this->log("收件人邮箱已满(初步检查): {$to}", $clientIp); + continue; + } + } + + $recipients[] = $to; + $this->send($client, "250 Recipient OK"); + } + } elseif (stripos($input, 'DATA') === 0) { + // 收到DATA命令,跳出循环 + break; + } elseif (strtoupper($input) === 'QUIT') { + $this->send($client, "221 Bye"); + return; + } else { + $this->send($client, "500 Error: Expected RCPT TO or DATA"); + } + } + + if (empty($recipients)) { + $this->send($client, "503 No valid recipients"); + $this->log("没有有效收件人", $clientIp); + return; + } + + // 6. 说:开始写内容吧 + $this->send($client, "354 Start mail input, end with ."); + + // 7. 接收邮件内容 + $emailContent = $this->receiveEmailContent($client); + $emailSize = strlen($emailContent); + + // 8. 再次检查邮箱大小限制(使用实际邮件大小) + $validRecipients = []; + foreach ($recipients as $to) { + $user = $this->userRepo->findByUsername($to); + if ($user) { + $usage = $this->mailboxRepo->getUsage($user['id']); + if ($usage['used'] + $emailSize > $usage['limit']) { + $this->log("收件人邮箱已满(实际检查): {$to}", $clientIp); + continue; + } + } + $validRecipients[] = $to; + } + + if (empty($validRecipients)) { + $this->send($client, "552 All recipients' mailboxes are full"); + $this->log("所有收件人邮箱已满", $clientIp); + return; + } + + // 9. 保存到数据库(支持多收件人) + $successCount = 0; + foreach ($validRecipients as $to) { + if ($this->saveEmail($from, $to, $emailContent, $clientIp, $emailSize)) { + $successCount++; + } + } + + // 10. 告诉客户:收到了 + if ($successCount > 0) { + $this->send($client, "250 Mail accepted"); + $this->log("邮件发送成功: {$from} -> " . implode(', ', $validRecipients) . " ({$successCount}个收件人)", $clientIp); + } else { + $this->send($client, "550 Mail delivery failed"); + $this->log("邮件发送失败: {$from} -> " . implode(', ', $validRecipients), $clientIp); + } + + // 11. 等客户说再见 + $this->waitForCommand($client, 'QUIT', 'QUIT'); + $this->send($client, "221 Bye"); + + echo "收到一封邮件:{$from} -> " . implode(', ', $validRecipients) . "\n"; + + } catch (Exception $e) { + $this->log("处理客户端错误: " . $e->getMessage(), $clientIp); + $this->send($client, "500 Internal server error"); + } + } + + private function send($client, $message) + { + socket_write($client, $message . "\r\n"); + } + + private function waitForCommand($client, $expected, $description) + { + while (true) { + $input = socket_read($client, 1024); + if ($input === false) break; + + $input = trim($input); + echo "客户端: {$input}\n"; + + if (stripos($input, $expected) === 0) { + // 提取邮箱地址 + if (preg_match('/<(.+?)>/', $input, $matches)) { + return $matches[1]; + } + return $input; + } + + // 如果收到QUIT,直接退出 + if (strtoupper($input) === 'QUIT') { + $this->send($client, "221 Bye"); + exit(0); + } + + $this->send($client, "500 Error: Expected {$description}"); + } + } + + private function receiveEmailContent($client) + { + $content = ""; + + $buffer = ""; + + while (true) { + $data = socket_read($client, 1024, PHP_BINARY_READ); + if ($data === false || $data === '') { + break; + } + + $buffer .= $data; + + // 按行处理 + while (($pos = strpos($buffer, "\r\n")) !== false) { + $line = substr($buffer, 0, $pos); + $buffer = substr($buffer, $pos + 2); + + // 如果遇到单独一行的 '.' 就结束 + if (trim($line) === '.') { + return $content; + } + + $content .= $line . "\r\n"; + } + } + + return $content; + } + + private function saveEmail($from, $to, $content, $clientIp = 'unknown', $emailSize = null) + { + try { + // 1. 解析邮件内容(先不转换编码) + $lines = explode("\r\n", $content); + $subject = "无主题"; + $body = ""; + $inBody = false; + + foreach ($lines as $line) { + if (!$inBody && stripos($line, 'Subject:') === 0) { + $subject = trim(substr($line, 8)); + // 解码MIME编码的主题 + $subject = $this->decodeMimeHeader($subject); + } + + if (!$inBody && trim($line) === '') { + $inBody = true; + continue; + } + + if ($inBody) { + $body .= $line . "\n"; + } + } + + // 2. 清理正文 + $body = trim($body); + + // 3. 检测当前编码 - 直接假设为UTF-8 + $detectedEncoding = 'UTF-8'; // 直接指定,不检测了 + echo "使用编码: {$detectedEncoding}\n"; + + // 4. 验证确实是UTF-8,如果不是就转换 + if (!mb_check_encoding($subject, 'UTF-8')) { + $subject = mb_convert_encoding($subject, 'UTF-8', 'auto'); + } + if (!mb_check_encoding($body, 'UTF-8')) { + $body = mb_convert_encoding($body, 'UTF-8', 'auto'); + } + + // 5. 计算邮件大小(如果未提供则计算) + if ($emailSize === null) { + $emailSize = strlen($content); + } + + // 6. 获取收件人用户ID + $user = $this->userRepo->findByUsername($to); + $recipientId = $user ? $user['id'] : null; + + // 7. 使用参数绑定,确保UTF-8传输 + $stmt = $this->db->prepare( + "INSERT INTO emails (sender, recipient, recipient_id, subject, body, size_bytes) VALUES (?, ?, ?, ?, ?, ?)" + ); + + // 绑定参数时指定字符集 + $stmt->bindValue(1, $from, PDO::PARAM_STR); + $stmt->bindValue(2, $to, PDO::PARAM_STR); + $stmt->bindValue(3, $recipientId, PDO::PARAM_INT); + $stmt->bindValue(4, $subject, PDO::PARAM_STR); + $stmt->bindValue(5, $body, PDO::PARAM_STR); + $stmt->bindValue(6, $emailSize, PDO::PARAM_INT); + + $stmt->execute(); + + echo "邮件保存成功:{$from} -> {$to}\n"; + echo " 主题: {$subject}\n"; + echo " 长度: {$emailSize} 字节\n"; + + return true; + + } catch (Exception $e) { + echo "保存邮件失败: " . $e->getMessage() . "\n"; + error_log("邮件保存错误: " . $e->getMessage() . "\n" . $e->getTraceAsString()); + $this->log("保存邮件失败: " . $e->getMessage(), $clientIp); + return false; + } + } + + /** + * 记录日志到数据库 + */ + private function log($message, $clientIp = 'unknown', $userId = null) + { + try { + $stmt = $this->db->prepare( + "INSERT INTO server_logs (log_type, message, client_ip, user_id) VALUES (?, ?, ?, ?)" + ); + $stmt->execute(['SMTP', $message, $clientIp, $userId]); + } catch (Exception $e) { + // 日志记录失败不影响主流程 + error_log("日志记录失败: " . $e->getMessage()); + } + } + + // 添加MIME头解码方法 + private function decodeMimeHeader($header) + { + // 处理 =?UTF-8?B?5L2g5aW9?= 这样的MIME编码 + $decoded = ''; + $parts = preg_split('/(=\?[^?]+\?[BQ]\?[^?]+\?=)/i', $header, -1, PREG_SPLIT_DELIM_CAPTURE); + + foreach ($parts as $part) { + if (preg_match('/=\?([^\?]+)\?([BQ])\?([^\?]+)\?=/i', $part, $matches)) { + $charset = $matches[1]; + $encoding = strtoupper($matches[2]); + $text = $matches[3]; + + if ($encoding === 'B') { + // Base64解码 + $decodedText = base64_decode($text); + } elseif ($encoding === 'Q') { + // Quoted-Printable解码 + $decodedText = quoted_printable_decode(str_replace('_', ' ', $text)); + } + + if (isset($decodedText)) { + $decoded .= mb_convert_encoding($decodedText, 'UTF-8', $charset); + } + } else { + $decoded .= $part; + } + } + + return $decoded ?: $header; + } + + public function stop() + { + $this->isRunning = false; + } +} + +// 如果直接运行这个文件 +if (basename(__FILE__) == basename($_SERVER['PHP_SELF'])) { + // 检查是否有权限监听25端口(需要sudo) + if (posix_getuid() != 0) { + echo "注意:需要sudo权限监听25端口\n"; + echo "请运行:sudo php " . __FILE__ . "\n"; + exit(1); + } + + $server = new SimpleSmtpServer(); + $server->start(); +} ?> \ No newline at end of file diff --git a/src/storage/EmailRepository.php b/src/storage/EmailRepository.php index 8113c59..009e8f0 100644 --- a/src/storage/EmailRepository.php +++ b/src/storage/EmailRepository.php @@ -1,176 +1,176 @@ -db = Database::getInstance(); - } - - /** - * 根据ID查找邮件 - * @param int $id 邮件ID - * @return array|null 邮件信息或null - */ - public function findById($id) { - $stmt = $this->db->prepare(" - SELECT e.*, - u1.username as sender_username, - u2.username as recipient_username - FROM emails e - LEFT JOIN users u1 ON e.sender_id = u1.id - LEFT JOIN users u2 ON e.recipient_id = u2.id - WHERE e.id = ? AND e.is_deleted = 0 - "); - $stmt->execute([$id]); - return $stmt->fetch(); - } - - /** - * 获取用户的收件箱邮件 - * @param int $userId 用户ID - * @param int $limit 限制数量 - * @param int $offset 偏移量 - * @return array 邮件列表 - */ - public function getInbox($userId, $limit = null, $offset = 0) { - $sql = " - SELECT e.*, - COALESCE(u1.username, e.sender) as sender_name, - COALESCE(u2.username, e.recipient) as recipient_name - FROM emails e - LEFT JOIN users u1 ON e.sender_id = u1.id - LEFT JOIN users u2 ON e.recipient_id = u2.id - WHERE (e.recipient_id = ? OR e.recipient = (SELECT username FROM users WHERE id = ?)) - AND e.is_deleted = 0 - ORDER BY e.created_at DESC - "; - - if ($limit !== null) { - $sql .= " LIMIT ? OFFSET ?"; - $stmt = $this->db->prepare($sql); - $stmt->execute([$userId, $userId, $limit, $offset]); - } else { - $stmt = $this->db->prepare($sql); - $stmt->execute([$userId, $userId]); - } - - return $stmt->fetchAll(); - } - - /** - * 获取用户的发件箱邮件 - * @param int $userId 用户ID - * @param int $limit 限制数量 - * @param int $offset 偏移量 - * @return array 邮件列表 - */ - public function getSent($userId, $limit = null, $offset = 0) { - $sql = " - SELECT e.*, - COALESCE(u1.username, e.sender) as sender_name, - COALESCE(u2.username, e.recipient) as recipient_name - FROM emails e - LEFT JOIN users u1 ON e.sender_id = u1.id - LEFT JOIN users u2 ON e.recipient_id = u2.id - WHERE (e.sender_id = ? OR e.sender = (SELECT username FROM users WHERE id = ?)) - AND e.is_deleted = 0 - ORDER BY e.created_at DESC - "; - - if ($limit !== null) { - $sql .= " LIMIT ? OFFSET ?"; - $stmt = $this->db->prepare($sql); - $stmt->execute([$userId, $userId, $limit, $offset]); - } else { - $stmt = $this->db->prepare($sql); - $stmt->execute([$userId, $userId]); - } - - return $stmt->fetchAll(); - } - - /** - * 获取所有邮件(管理员功能) - * @param int $limit 限制数量 - * @param int $offset 偏移量 - * @return array 邮件列表 - */ - public function getAll($limit = null, $offset = 0) { - $sql = " - SELECT e.*, - COALESCE(u1.username, e.sender) as sender_name, - COALESCE(u2.username, e.recipient) as recipient_name - FROM emails e - LEFT JOIN users u1 ON e.sender_id = u1.id - LEFT JOIN users u2 ON e.recipient_id = u2.id - WHERE e.is_deleted = 0 - ORDER BY e.created_at DESC - "; - - if ($limit !== null) { - $sql .= " LIMIT ? OFFSET ?"; - $stmt = $this->db->prepare($sql); - $stmt->execute([$limit, $offset]); - } else { - $stmt = $this->db->query($sql); - } - - return $stmt->fetchAll(); - } - - /** - * 获取邮件总数 - * @param int|null $userId 用户ID(如果提供,只统计该用户的邮件) - * @return int 邮件总数 - */ - public function getCount($userId = null) { - if ($userId !== null) { - $stmt = $this->db->prepare(" - SELECT COUNT(*) as count FROM emails - WHERE (recipient_id = ? OR sender_id = ?) AND is_deleted = 0 - "); - $stmt->execute([$userId, $userId]); - } else { - $stmt = $this->db->query("SELECT COUNT(*) as count FROM emails WHERE is_deleted = 0"); - } - - $result = $stmt->fetch(); - return (int)$result['count']; - } - - /** - * 标记邮件为已读 - * @param int $id 邮件ID - * @return bool 是否成功 - */ - public function markAsRead($id) { - $stmt = $this->db->prepare("UPDATE emails SET is_read = 1 WHERE id = ?"); - return $stmt->execute([$id]); - } - - /** - * 删除邮件(软删除) - * @param int $id 邮件ID - * @return bool 是否成功 - */ - public function delete($id) { - $stmt = $this->db->prepare("UPDATE emails SET is_deleted = 1 WHERE id = ?"); - return $stmt->execute([$id]); - } - - /** - * 永久删除邮件 - * @param int $id 邮件ID - * @return bool 是否成功 - */ - public function permanentDelete($id) { - $stmt = $this->db->prepare("DELETE FROM emails WHERE id = ?"); - return $stmt->execute([$id]); - } -} +db = Database::getInstance(); + } + + /** + * 根据ID查找邮件 + * @param int $id 邮件ID + * @return array|null 邮件信息或null + */ + public function findById($id) { + $stmt = $this->db->prepare(" + SELECT e.*, + u1.username as sender_username, + u2.username as recipient_username + FROM emails e + LEFT JOIN users u1 ON e.sender_id = u1.id + LEFT JOIN users u2 ON e.recipient_id = u2.id + WHERE e.id = ? AND e.is_deleted = 0 + "); + $stmt->execute([$id]); + return $stmt->fetch(); + } + + /** + * 获取用户的收件箱邮件 + * @param int $userId 用户ID + * @param int $limit 限制数量 + * @param int $offset 偏移量 + * @return array 邮件列表 + */ + public function getInbox($userId, $limit = null, $offset = 0) { + $sql = " + SELECT e.*, + COALESCE(u1.username, e.sender) as sender_name, + COALESCE(u2.username, e.recipient) as recipient_name + FROM emails e + LEFT JOIN users u1 ON e.sender_id = u1.id + LEFT JOIN users u2 ON e.recipient_id = u2.id + WHERE (e.recipient_id = ? OR e.recipient = (SELECT username FROM users WHERE id = ?)) + AND e.is_deleted = 0 + ORDER BY e.created_at DESC + "; + + if ($limit !== null) { + $sql .= " LIMIT ? OFFSET ?"; + $stmt = $this->db->prepare($sql); + $stmt->execute([$userId, $userId, $limit, $offset]); + } else { + $stmt = $this->db->prepare($sql); + $stmt->execute([$userId, $userId]); + } + + return $stmt->fetchAll(); + } + + /** + * 获取用户的发件箱邮件 + * @param int $userId 用户ID + * @param int $limit 限制数量 + * @param int $offset 偏移量 + * @return array 邮件列表 + */ + public function getSent($userId, $limit = null, $offset = 0) { + $sql = " + SELECT e.*, + COALESCE(u1.username, e.sender) as sender_name, + COALESCE(u2.username, e.recipient) as recipient_name + FROM emails e + LEFT JOIN users u1 ON e.sender_id = u1.id + LEFT JOIN users u2 ON e.recipient_id = u2.id + WHERE (e.sender_id = ? OR e.sender = (SELECT username FROM users WHERE id = ?)) + AND e.is_deleted = 0 + ORDER BY e.created_at DESC + "; + + if ($limit !== null) { + $sql .= " LIMIT ? OFFSET ?"; + $stmt = $this->db->prepare($sql); + $stmt->execute([$userId, $userId, $limit, $offset]); + } else { + $stmt = $this->db->prepare($sql); + $stmt->execute([$userId, $userId]); + } + + return $stmt->fetchAll(); + } + + /** + * 获取所有邮件(管理员功能) + * @param int $limit 限制数量 + * @param int $offset 偏移量 + * @return array 邮件列表 + */ + public function getAll($limit = null, $offset = 0) { + $sql = " + SELECT e.*, + COALESCE(u1.username, e.sender) as sender_name, + COALESCE(u2.username, e.recipient) as recipient_name + FROM emails e + LEFT JOIN users u1 ON e.sender_id = u1.id + LEFT JOIN users u2 ON e.recipient_id = u2.id + WHERE e.is_deleted = 0 + ORDER BY e.created_at DESC + "; + + if ($limit !== null) { + $sql .= " LIMIT ? OFFSET ?"; + $stmt = $this->db->prepare($sql); + $stmt->execute([$limit, $offset]); + } else { + $stmt = $this->db->query($sql); + } + + return $stmt->fetchAll(); + } + + /** + * 获取邮件总数 + * @param int|null $userId 用户ID(如果提供,只统计该用户的邮件) + * @return int 邮件总数 + */ + public function getCount($userId = null) { + if ($userId !== null) { + $stmt = $this->db->prepare(" + SELECT COUNT(*) as count FROM emails + WHERE (recipient_id = ? OR sender_id = ?) AND is_deleted = 0 + "); + $stmt->execute([$userId, $userId]); + } else { + $stmt = $this->db->query("SELECT COUNT(*) as count FROM emails WHERE is_deleted = 0"); + } + + $result = $stmt->fetch(); + return (int)$result['count']; + } + + /** + * 标记邮件为已读 + * @param int $id 邮件ID + * @return bool 是否成功 + */ + public function markAsRead($id) { + $stmt = $this->db->prepare("UPDATE emails SET is_read = 1 WHERE id = ?"); + return $stmt->execute([$id]); + } + + /** + * 删除邮件(软删除) + * @param int $id 邮件ID + * @return bool 是否成功 + */ + public function delete($id) { + $stmt = $this->db->prepare("UPDATE emails SET is_deleted = 1 WHERE id = ?"); + return $stmt->execute([$id]); + } + + /** + * 永久删除邮件 + * @param int $id 邮件ID + * @return bool 是否成功 + */ + public function permanentDelete($id) { + $stmt = $this->db->prepare("DELETE FROM emails WHERE id = ?"); + return $stmt->execute([$id]); + } +} diff --git a/src/storage/FilterRepository.php b/src/storage/FilterRepository.php index e4f198e..f9bc04d 100644 --- a/src/storage/FilterRepository.php +++ b/src/storage/FilterRepository.php @@ -1,114 +1,114 @@ -db = Database::getInstance(); - } - - /** - * 创建过滤规则 - * @param string $type 规则类型 ('email' 或 'ip') - * @param string $value 规则值 - * @param string $action 动作 ('block' 或 'allow') - * @param string $description 描述 - * @return bool 是否成功 - */ - public function create($type, $value, $action = 'block', $description = '') { - $stmt = $this->db->prepare(" - INSERT INTO filter_rules (rule_type, rule_value, action, description, is_active) - VALUES (?, ?, ?, ?, 1) - "); - return $stmt->execute([$type, $value, $action, $description]); - } - - /** - * 获取所有过滤规则 - * @return array 规则列表 - */ - public function getAll() { - $stmt = $this->db->query(" - SELECT * FROM filter_rules - ORDER BY rule_type, created_at DESC - "); - return $stmt->fetchAll(); - } - - /** - * 获取激活的过滤规则 - * @return array 规则列表 - */ - public function getActive() { - $stmt = $this->db->query(" - SELECT * FROM filter_rules - WHERE is_active = 1 - ORDER BY rule_type, created_at DESC - "); - return $stmt->fetchAll(); - } - - /** - * 检查邮箱是否被过滤 - * @param string $email 邮箱地址 - * @return bool 是否被阻止 - */ - public function isEmailBlocked($email) { - $stmt = $this->db->prepare(" - SELECT action FROM filter_rules - WHERE rule_type = 'email' - AND is_active = 1 - AND (rule_value = ? OR rule_value LIKE ?) - ORDER BY action DESC - LIMIT 1 - "); - $stmt->execute([$email, $email]); - $result = $stmt->fetch(); - return $result && $result['action'] === 'block'; - } - - /** - * 检查IP是否被过滤 - * @param string $ip IP地址 - * @return bool 是否被阻止 - */ - public function isIPBlocked($ip) { - $stmt = $this->db->prepare(" - SELECT action FROM filter_rules - WHERE rule_type = 'ip' - AND is_active = 1 - AND rule_value = ? - ORDER BY action DESC - LIMIT 1 - "); - $stmt->execute([$ip]); - $result = $stmt->fetch(); - return $result && $result['action'] === 'block'; - } - - /** - * 删除规则 - * @param int $id 规则ID - * @return bool 是否成功 - */ - public function delete($id) { - $stmt = $this->db->prepare("DELETE FROM filter_rules WHERE id = ?"); - return $stmt->execute([$id]); - } - - /** - * 更新规则状态 - * @param int $id 规则ID - * @param bool $isActive 是否激活 - * @return bool 是否成功 - */ - public function updateStatus($id, $isActive) { - $stmt = $this->db->prepare("UPDATE filter_rules SET is_active = ? WHERE id = ?"); - return $stmt->execute([$isActive ? 1 : 0, $id]); - } -} - +db = Database::getInstance(); + } + + /** + * 创建过滤规则 + * @param string $type 规则类型 ('email' 或 'ip') + * @param string $value 规则值 + * @param string $action 动作 ('block' 或 'allow') + * @param string $description 描述 + * @return bool 是否成功 + */ + public function create($type, $value, $action = 'block', $description = '') { + $stmt = $this->db->prepare(" + INSERT INTO filter_rules (rule_type, rule_value, action, description, is_active) + VALUES (?, ?, ?, ?, 1) + "); + return $stmt->execute([$type, $value, $action, $description]); + } + + /** + * 获取所有过滤规则 + * @return array 规则列表 + */ + public function getAll() { + $stmt = $this->db->query(" + SELECT * FROM filter_rules + ORDER BY rule_type, created_at DESC + "); + return $stmt->fetchAll(); + } + + /** + * 获取激活的过滤规则 + * @return array 规则列表 + */ + public function getActive() { + $stmt = $this->db->query(" + SELECT * FROM filter_rules + WHERE is_active = 1 + ORDER BY rule_type, created_at DESC + "); + return $stmt->fetchAll(); + } + + /** + * 检查邮箱是否被过滤 + * @param string $email 邮箱地址 + * @return bool 是否被阻止 + */ + public function isEmailBlocked($email) { + $stmt = $this->db->prepare(" + SELECT action FROM filter_rules + WHERE rule_type = 'email' + AND is_active = 1 + AND (rule_value = ? OR rule_value LIKE ?) + ORDER BY action DESC + LIMIT 1 + "); + $stmt->execute([$email, $email]); + $result = $stmt->fetch(); + return $result && $result['action'] === 'block'; + } + + /** + * 检查IP是否被过滤 + * @param string $ip IP地址 + * @return bool 是否被阻止 + */ + public function isIPBlocked($ip) { + $stmt = $this->db->prepare(" + SELECT action FROM filter_rules + WHERE rule_type = 'ip' + AND is_active = 1 + AND rule_value = ? + ORDER BY action DESC + LIMIT 1 + "); + $stmt->execute([$ip]); + $result = $stmt->fetch(); + return $result && $result['action'] === 'block'; + } + + /** + * 删除规则 + * @param int $id 规则ID + * @return bool 是否成功 + */ + public function delete($id) { + $stmt = $this->db->prepare("DELETE FROM filter_rules WHERE id = ?"); + return $stmt->execute([$id]); + } + + /** + * 更新规则状态 + * @param int $id 规则ID + * @param bool $isActive 是否激活 + * @return bool 是否成功 + */ + public function updateStatus($id, $isActive) { + $stmt = $this->db->prepare("UPDATE filter_rules SET is_active = ? WHERE id = ?"); + return $stmt->execute([$isActive ? 1 : 0, $id]); + } +} + diff --git a/src/storage/MailboxRepository.php b/src/storage/MailboxRepository.php index 8e00b9b..2c7b8b5 100644 --- a/src/storage/MailboxRepository.php +++ b/src/storage/MailboxRepository.php @@ -1,84 +1,84 @@ -db = Database::getInstance(); - } - - /** - * 获取用户邮箱大小限制 - * @param int $userId 用户ID - * @return int 大小限制(字节) - */ - public function getSizeLimit($userId) { - $stmt = $this->db->prepare(" - SELECT size_limit_bytes FROM user_mailbox_limits WHERE user_id = ? - "); - $stmt->execute([$userId]); - $result = $stmt->fetch(); - - if ($result) { - return (int)$result['size_limit_bytes']; - } - - // 返回默认值 - return 104857600; // 100MB - } - - /** - * 设置用户邮箱大小限制 - * @param int $userId 用户ID - * @param int $sizeBytes 大小限制(字节) - * @return bool 是否成功 - */ - public function setSizeLimit($userId, $sizeBytes) { - $stmt = $this->db->prepare(" - INSERT INTO user_mailbox_limits (user_id, size_limit_bytes) - VALUES (?, ?) - ON DUPLICATE KEY UPDATE size_limit_bytes = VALUES(size_limit_bytes) - "); - return $stmt->execute([$userId, $sizeBytes]); - } - - /** - * 获取用户当前邮箱使用大小 - * @param int $userId 用户ID - * @return int 已使用大小(字节) - */ - public function getUsedSize($userId) { - $stmt = $this->db->prepare(" - SELECT COALESCE(SUM(size_bytes), 0) as total_size - FROM emails - WHERE recipient_id = ? AND is_deleted = 0 - "); - $stmt->execute([$userId]); - $result = $stmt->fetch(); - return (int)($result['total_size'] ?? 0); - } - - /** - * 获取用户邮箱使用情况 - * @param int $userId 用户ID - * @return array ['limit' => int, 'used' => int, 'percentage' => float] - */ - public function getUsage($userId) { - $limit = $this->getSizeLimit($userId); - $used = $this->getUsedSize($userId); - $percentage = $limit > 0 ? ($used / $limit) * 100 : 0; - - return [ - 'limit' => $limit, - 'used' => $used, - 'percentage' => round($percentage, 2) - ]; - } -} - +db = Database::getInstance(); + } + + /** + * 获取用户邮箱大小限制 + * @param int $userId 用户ID + * @return int 大小限制(字节) + */ + public function getSizeLimit($userId) { + $stmt = $this->db->prepare(" + SELECT size_limit_bytes FROM user_mailbox_limits WHERE user_id = ? + "); + $stmt->execute([$userId]); + $result = $stmt->fetch(); + + if ($result) { + return (int)$result['size_limit_bytes']; + } + + // 返回默认值 + return 104857600; // 100MB + } + + /** + * 设置用户邮箱大小限制 + * @param int $userId 用户ID + * @param int $sizeBytes 大小限制(字节) + * @return bool 是否成功 + */ + public function setSizeLimit($userId, $sizeBytes) { + $stmt = $this->db->prepare(" + INSERT INTO user_mailbox_limits (user_id, size_limit_bytes) + VALUES (?, ?) + ON DUPLICATE KEY UPDATE size_limit_bytes = VALUES(size_limit_bytes) + "); + return $stmt->execute([$userId, $sizeBytes]); + } + + /** + * 获取用户当前邮箱使用大小 + * @param int $userId 用户ID + * @return int 已使用大小(字节) + */ + public function getUsedSize($userId) { + $stmt = $this->db->prepare(" + SELECT COALESCE(SUM(size_bytes), 0) as total_size + FROM emails + WHERE recipient_id = ? AND is_deleted = 0 + "); + $stmt->execute([$userId]); + $result = $stmt->fetch(); + return (int)($result['total_size'] ?? 0); + } + + /** + * 获取用户邮箱使用情况 + * @param int $userId 用户ID + * @return array ['limit' => int, 'used' => int, 'percentage' => float] + */ + public function getUsage($userId) { + $limit = $this->getSizeLimit($userId); + $used = $this->getUsedSize($userId); + $percentage = $limit > 0 ? ($used / $limit) * 100 : 0; + + return [ + 'limit' => $limit, + 'used' => $used, + 'percentage' => round($percentage, 2) + ]; + } +} + diff --git a/src/storage/ServiceRepository.php b/src/storage/ServiceRepository.php index 83eb754..bd04d94 100644 --- a/src/storage/ServiceRepository.php +++ b/src/storage/ServiceRepository.php @@ -1,79 +1,79 @@ -db = Database::getInstance(); - } - - /** - * 获取服务状态 - * @param string $serviceName 服务名称 ('smtp' 或 'pop3') - * @return array|null 服务状态或null - */ - public function getStatus($serviceName) { - $stmt = $this->db->prepare("SELECT * FROM service_status WHERE service_name = ?"); - $stmt->execute([$serviceName]); - return $stmt->fetch(); - } - - /** - * 更新服务状态 - * @param string $serviceName 服务名称 - * @param bool $isRunning 是否运行 - * @param int|null $pid 进程ID - * @return bool 是否成功 - */ - public function updateStatus($serviceName, $isRunning, $pid = null) { - $stmt = $this->db->prepare(" - INSERT INTO service_status (service_name, is_running, pid, last_started_at, last_stopped_at) - VALUES (?, ?, ?, NOW(), NULL) - ON DUPLICATE KEY UPDATE - is_running = VALUES(is_running), - pid = VALUES(pid), - last_started_at = IF(VALUES(is_running) = 1, NOW(), last_started_at), - last_stopped_at = IF(VALUES(is_running) = 0, NOW(), last_stopped_at) - "); - return $stmt->execute([$serviceName, $isRunning ? 1 : 0, $pid]); - } - - /** - * 获取所有服务状态 - * @return array 服务状态列表 - */ - public function getAllStatus() { - $stmt = $this->db->query("SELECT * FROM service_status ORDER BY service_name"); - return $stmt->fetchAll(); - } - - /** - * 检查服务是否运行 - * @param string $serviceName 服务名称 - * @return bool 是否运行 - */ - public function isRunning($serviceName) { - $status = $this->getStatus($serviceName); - if (!$status) { - return false; - } - - // 检查进程是否真的在运行 - if ($status['pid'] && $status['is_running']) { - // 检查进程是否存在 - $result = shell_exec("ps -p {$status['pid']} -o pid= 2>/dev/null"); - if (empty(trim($result))) { - // 进程不存在,更新状态 - $this->updateStatus($serviceName, false, null); - return false; - } - } - - return (bool)$status['is_running']; - } -} - +db = Database::getInstance(); + } + + /** + * 获取服务状态 + * @param string $serviceName 服务名称 ('smtp' 或 'pop3') + * @return array|null 服务状态或null + */ + public function getStatus($serviceName) { + $stmt = $this->db->prepare("SELECT * FROM service_status WHERE service_name = ?"); + $stmt->execute([$serviceName]); + return $stmt->fetch(); + } + + /** + * 更新服务状态 + * @param string $serviceName 服务名称 + * @param bool $isRunning 是否运行 + * @param int|null $pid 进程ID + * @return bool 是否成功 + */ + public function updateStatus($serviceName, $isRunning, $pid = null) { + $stmt = $this->db->prepare(" + INSERT INTO service_status (service_name, is_running, pid, last_started_at, last_stopped_at) + VALUES (?, ?, ?, NOW(), NULL) + ON DUPLICATE KEY UPDATE + is_running = VALUES(is_running), + pid = VALUES(pid), + last_started_at = IF(VALUES(is_running) = 1, NOW(), last_started_at), + last_stopped_at = IF(VALUES(is_running) = 0, NOW(), last_stopped_at) + "); + return $stmt->execute([$serviceName, $isRunning ? 1 : 0, $pid]); + } + + /** + * 获取所有服务状态 + * @return array 服务状态列表 + */ + public function getAllStatus() { + $stmt = $this->db->query("SELECT * FROM service_status ORDER BY service_name"); + return $stmt->fetchAll(); + } + + /** + * 检查服务是否运行 + * @param string $serviceName 服务名称 + * @return bool 是否运行 + */ + public function isRunning($serviceName) { + $status = $this->getStatus($serviceName); + if (!$status) { + return false; + } + + // 检查进程是否真的在运行 + if ($status['pid'] && $status['is_running']) { + // 检查进程是否存在 + $result = shell_exec("ps -p {$status['pid']} -o pid= 2>/dev/null"); + if (empty(trim($result))) { + // 进程不存在,更新状态 + $this->updateStatus($serviceName, false, null); + return false; + } + } + + return (bool)$status['is_running']; + } +} + diff --git a/src/storage/SystemSettingsRepository.php b/src/storage/SystemSettingsRepository.php index 69bfe60..e67ed9b 100644 --- a/src/storage/SystemSettingsRepository.php +++ b/src/storage/SystemSettingsRepository.php @@ -1,56 +1,56 @@ -db = Database::getInstance(); - } - - /** - * 获取设置值 - * @param string $key 设置键 - * @param mixed $default 默认值 - * @return mixed 设置值 - */ - public function get($key, $default = null) { - $stmt = $this->db->prepare("SELECT setting_value FROM system_settings WHERE setting_key = ?"); - $stmt->execute([$key]); - $result = $stmt->fetch(); - return $result ? $result['setting_value'] : $default; - } - - /** - * 设置值 - * @param string $key 设置键 - * @param mixed $value 设置值 - * @return bool 是否成功 - */ - public function set($key, $value) { - $stmt = $this->db->prepare(" - INSERT INTO system_settings (setting_key, setting_value) - VALUES (?, ?) - ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value) - "); - return $stmt->execute([$key, $value]); - } - - /** - * 获取所有设置 - * @return array 所有设置 - */ - public function getAll() { - $stmt = $this->db->query("SELECT setting_key, setting_value FROM system_settings"); - $results = $stmt->fetchAll(); - $settings = []; - foreach ($results as $row) { - $settings[$row['setting_key']] = $row['setting_value']; - } - return $settings; - } -} - +db = Database::getInstance(); + } + + /** + * 获取设置值 + * @param string $key 设置键 + * @param mixed $default 默认值 + * @return mixed 设置值 + */ + public function get($key, $default = null) { + $stmt = $this->db->prepare("SELECT setting_value FROM system_settings WHERE setting_key = ?"); + $stmt->execute([$key]); + $result = $stmt->fetch(); + return $result ? $result['setting_value'] : $default; + } + + /** + * 设置值 + * @param string $key 设置键 + * @param mixed $value 设置值 + * @return bool 是否成功 + */ + public function set($key, $value) { + $stmt = $this->db->prepare(" + INSERT INTO system_settings (setting_key, setting_value) + VALUES (?, ?) + ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value) + "); + return $stmt->execute([$key, $value]); + } + + /** + * 获取所有设置 + * @return array 所有设置 + */ + public function getAll() { + $stmt = $this->db->query("SELECT setting_key, setting_value FROM system_settings"); + $results = $stmt->fetchAll(); + $settings = []; + foreach ($results as $row) { + $settings[$row['setting_key']] = $row['setting_value']; + } + return $settings; + } +} + diff --git a/src/storage/UserRepository.php b/src/storage/UserRepository.php index fcdc4cf..8ec2195 100644 --- a/src/storage/UserRepository.php +++ b/src/storage/UserRepository.php @@ -1,173 +1,173 @@ -db = Database::getInstance(); - } - - /** - * 根据用户名查找用户 - * @param string $username 用户名(邮箱) - * @return array|null 用户信息或null - */ - public function findByUsername($username) { - $stmt = $this->db->prepare("SELECT * FROM users WHERE username = ?"); - $stmt->execute([$username]); - return $stmt->fetch(); - } - - /** - * 根据ID查找用户 - * @param int $id 用户ID - * @return array|null 用户信息或null - */ - public function findById($id) { - $stmt = $this->db->prepare("SELECT * FROM users WHERE id = ?"); - $stmt->execute([$id]); - return $stmt->fetch(); - } - - /** - * 检查用户名是否已存在 - * @param string $username 用户名 - * @return bool 是否存在 - */ - public function usernameExists($username) { - $user = $this->findByUsername($username); - return $user !== false; - } - - /** - * 创建新用户 - * @param string $username 用户名(邮箱) - * @param string $password 明文密码 - * @param bool $isAdmin 是否为管理员 - * @param bool $isActive 是否激活 - * @return array 创建的用户信息 - * @throws Exception 如果创建失败 - */ - public function create($username, $password, $isAdmin = false, $isActive = true) { - // 检查用户名是否已存在 - if ($this->usernameExists($username)) { - throw new Exception("用户名已存在"); - } - - // 加密密码 - $passwordHash = Security::hashPassword($password); - - // 插入数据库 - $stmt = $this->db->prepare(" - INSERT INTO users (username, password_hash, is_admin, is_active, created_at) - VALUES (?, ?, ?, ?, NOW()) - "); - - $stmt->execute([$username, $passwordHash, $isAdmin ? 1 : 0, $isActive ? 1 : 0]); - - // 返回创建的用户信息 - $userId = $this->db->lastInsertId(); - return $this->findById($userId); - } - - /** - * 更新用户信息 - * @param int $id 用户ID - * @param array $data 要更新的数据 ['password' => string, 'is_admin' => bool, 'is_active' => bool] - * @return bool 是否成功 - */ - public function update($id, $data) { - $updates = []; - $params = []; - - if (isset($data['password'])) { - $updates[] = "password_hash = ?"; - $params[] = Security::hashPassword($data['password']); - } - - if (isset($data['is_admin'])) { - $updates[] = "is_admin = ?"; - $params[] = $data['is_admin'] ? 1 : 0; - } - - if (isset($data['is_active'])) { - $updates[] = "is_active = ?"; - $params[] = $data['is_active'] ? 1 : 0; - } - - if (empty($updates)) { - return false; - } - - $params[] = $id; - $sql = "UPDATE users SET " . implode(", ", $updates) . " WHERE id = ?"; - $stmt = $this->db->prepare($sql); - return $stmt->execute($params); - } - - /** - * 删除用户 - * @param int $id 用户ID - * @return bool 是否成功 - */ - public function delete($id) { - $stmt = $this->db->prepare("DELETE FROM users WHERE id = ?"); - return $stmt->execute([$id]); - } - - /** - * 获取所有用户列表 - * @param int $limit 限制数量 - * @param int $offset 偏移量 - * @return array 用户列表 - */ - public function getAll($limit = null, $offset = 0) { - $sql = "SELECT id, username, is_admin, is_active, created_at FROM users ORDER BY created_at DESC"; - - if ($limit !== null) { - $sql .= " LIMIT ? OFFSET ?"; - $stmt = $this->db->prepare($sql); - $stmt->execute([$limit, $offset]); - } else { - $stmt = $this->db->query($sql); - } - - return $stmt->fetchAll(); - } - - /** - * 获取用户总数 - * @return int 用户总数 - */ - public function getCount() { - $stmt = $this->db->query("SELECT COUNT(*) as count FROM users"); - $result = $stmt->fetch(); - return (int)$result['count']; - } - - /** - * 验证用户密码 - * @param string $username 用户名 - * @param string $password 明文密码 - * @return array|null 用户信息或null(如果验证失败) - */ - public function verifyPassword($username, $password) { - $user = $this->findByUsername($username); - - if (!$user) { - return null; - } - - if (!Security::verifyPassword($password, $user['password_hash'])) { - return null; - } - - return $user; - } -} +db = Database::getInstance(); + } + + /** + * 根据用户名查找用户 + * @param string $username 用户名(邮箱) + * @return array|null 用户信息或null + */ + public function findByUsername($username) { + $stmt = $this->db->prepare("SELECT * FROM users WHERE username = ?"); + $stmt->execute([$username]); + return $stmt->fetch(); + } + + /** + * 根据ID查找用户 + * @param int $id 用户ID + * @return array|null 用户信息或null + */ + public function findById($id) { + $stmt = $this->db->prepare("SELECT * FROM users WHERE id = ?"); + $stmt->execute([$id]); + return $stmt->fetch(); + } + + /** + * 检查用户名是否已存在 + * @param string $username 用户名 + * @return bool 是否存在 + */ + public function usernameExists($username) { + $user = $this->findByUsername($username); + return $user !== false; + } + + /** + * 创建新用户 + * @param string $username 用户名(邮箱) + * @param string $password 明文密码 + * @param bool $isAdmin 是否为管理员 + * @param bool $isActive 是否激活 + * @return array 创建的用户信息 + * @throws Exception 如果创建失败 + */ + public function create($username, $password, $isAdmin = false, $isActive = true) { + // 检查用户名是否已存在 + if ($this->usernameExists($username)) { + throw new Exception("用户名已存在"); + } + + // 加密密码 + $passwordHash = Security::hashPassword($password); + + // 插入数据库 + $stmt = $this->db->prepare(" + INSERT INTO users (username, password_hash, is_admin, is_active, created_at) + VALUES (?, ?, ?, ?, NOW()) + "); + + $stmt->execute([$username, $passwordHash, $isAdmin ? 1 : 0, $isActive ? 1 : 0]); + + // 返回创建的用户信息 + $userId = $this->db->lastInsertId(); + return $this->findById($userId); + } + + /** + * 更新用户信息 + * @param int $id 用户ID + * @param array $data 要更新的数据 ['password' => string, 'is_admin' => bool, 'is_active' => bool] + * @return bool 是否成功 + */ + public function update($id, $data) { + $updates = []; + $params = []; + + if (isset($data['password'])) { + $updates[] = "password_hash = ?"; + $params[] = Security::hashPassword($data['password']); + } + + if (isset($data['is_admin'])) { + $updates[] = "is_admin = ?"; + $params[] = $data['is_admin'] ? 1 : 0; + } + + if (isset($data['is_active'])) { + $updates[] = "is_active = ?"; + $params[] = $data['is_active'] ? 1 : 0; + } + + if (empty($updates)) { + return false; + } + + $params[] = $id; + $sql = "UPDATE users SET " . implode(", ", $updates) . " WHERE id = ?"; + $stmt = $this->db->prepare($sql); + return $stmt->execute($params); + } + + /** + * 删除用户 + * @param int $id 用户ID + * @return bool 是否成功 + */ + public function delete($id) { + $stmt = $this->db->prepare("DELETE FROM users WHERE id = ?"); + return $stmt->execute([$id]); + } + + /** + * 获取所有用户列表 + * @param int $limit 限制数量 + * @param int $offset 偏移量 + * @return array 用户列表 + */ + public function getAll($limit = null, $offset = 0) { + $sql = "SELECT id, username, is_admin, is_active, created_at FROM users ORDER BY created_at DESC"; + + if ($limit !== null) { + $sql .= " LIMIT ? OFFSET ?"; + $stmt = $this->db->prepare($sql); + $stmt->execute([$limit, $offset]); + } else { + $stmt = $this->db->query($sql); + } + + return $stmt->fetchAll(); + } + + /** + * 获取用户总数 + * @return int 用户总数 + */ + public function getCount() { + $stmt = $this->db->query("SELECT COUNT(*) as count FROM users"); + $result = $stmt->fetch(); + return (int)$result['count']; + } + + /** + * 验证用户密码 + * @param string $username 用户名 + * @param string $password 明文密码 + * @return array|null 用户信息或null(如果验证失败) + */ + public function verifyPassword($username, $password) { + $user = $this->findByUsername($username); + + if (!$user) { + return null; + } + + if (!Security::verifyPassword($password, $user['password_hash'])) { + return null; + } + + return $user; + } +} diff --git a/src/utils/Security.php b/src/utils/Security.php index 13b0b7d..55dfbc6 100644 --- a/src/utils/Security.php +++ b/src/utils/Security.php @@ -1,129 +1,129 @@ - 10]); - } - - /** - * 验证密码 - * @param string $password 明文密码 - * @param string $hash 密码哈希 - * @return bool 是否匹配 - */ - public static function verifyPassword($password, $hash) { - return password_verify($password, $hash); - } - - /** - * 清理输入,防止XSS攻击 - * @param string $input 用户输入 - * @return string 清理后的字符串 - */ - public static function sanitizeInput($input) { - return htmlspecialchars(strip_tags(trim($input)), ENT_QUOTES, 'UTF-8'); - } - - /** - * 生成CSRF令牌 - * @return string CSRF令牌 - */ - public static function generateCSRFToken() { - if (!isset($_SESSION['csrf_token'])) { - $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); - } - return $_SESSION['csrf_token']; - } - - /** - * 验证CSRF令牌 - * @param string $token 待验证的令牌 - * @return bool 是否有效 - */ - public static function verifyCSRFToken($token) { - return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token); - } - - /** - * 获取客户端IP地址 - * @return string IP地址 - */ - public static function getClientIP() { - $ipKeys = ['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR']; - foreach ($ipKeys as $key) { - if (array_key_exists($key, $_SERVER) === true) { - foreach (explode(',', $_SERVER[$key]) as $ip) { - $ip = trim($ip); - if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) { - return $ip; - } - } - } - } - return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; - } - - /** - * 防止暴力破解:检查登录尝试次数 - * @param string $username 用户名 - * @param int $maxAttempts 最大尝试次数 - * @param int $lockoutTime 锁定时间(秒) - * @return bool 是否允许登录 - */ - public static function checkLoginAttempts($username, $maxAttempts = 5, $lockoutTime = 300) { - $key = 'login_attempts_' . md5($username); - - if (!isset($_SESSION[$key])) { - $_SESSION[$key] = ['count' => 0, 'time' => time()]; - return true; - } - - $attempts = $_SESSION[$key]; - - // 如果超过锁定时间,重置计数 - if (time() - $attempts['time'] > $lockoutTime) { - $_SESSION[$key] = ['count' => 0, 'time' => time()]; - return true; - } - - // 检查是否超过最大尝试次数 - if ($attempts['count'] >= $maxAttempts) { - return false; - } - - return true; - } - - /** - * 记录登录失败尝试 - * @param string $username 用户名 - */ - public static function recordLoginAttempt($username) { - $key = 'login_attempts_' . md5($username); - - if (!isset($_SESSION[$key])) { - $_SESSION[$key] = ['count' => 1, 'time' => time()]; - } else { - $_SESSION[$key]['count']++; - $_SESSION[$key]['time'] = time(); - } - } - - /** - * 清除登录尝试记录 - * @param string $username 用户名 - */ - public static function clearLoginAttempts($username) { - $key = 'login_attempts_' . md5($username); - unset($_SESSION[$key]); - } -} + 10]); + } + + /** + * 验证密码 + * @param string $password 明文密码 + * @param string $hash 密码哈希 + * @return bool 是否匹配 + */ + public static function verifyPassword($password, $hash) { + return password_verify($password, $hash); + } + + /** + * 清理输入,防止XSS攻击 + * @param string $input 用户输入 + * @return string 清理后的字符串 + */ + public static function sanitizeInput($input) { + return htmlspecialchars(strip_tags(trim($input)), ENT_QUOTES, 'UTF-8'); + } + + /** + * 生成CSRF令牌 + * @return string CSRF令牌 + */ + public static function generateCSRFToken() { + if (!isset($_SESSION['csrf_token'])) { + $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); + } + return $_SESSION['csrf_token']; + } + + /** + * 验证CSRF令牌 + * @param string $token 待验证的令牌 + * @return bool 是否有效 + */ + public static function verifyCSRFToken($token) { + return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token); + } + + /** + * 获取客户端IP地址 + * @return string IP地址 + */ + public static function getClientIP() { + $ipKeys = ['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR']; + foreach ($ipKeys as $key) { + if (array_key_exists($key, $_SERVER) === true) { + foreach (explode(',', $_SERVER[$key]) as $ip) { + $ip = trim($ip); + if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) { + return $ip; + } + } + } + } + return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; + } + + /** + * 防止暴力破解:检查登录尝试次数 + * @param string $username 用户名 + * @param int $maxAttempts 最大尝试次数 + * @param int $lockoutTime 锁定时间(秒) + * @return bool 是否允许登录 + */ + public static function checkLoginAttempts($username, $maxAttempts = 5, $lockoutTime = 300) { + $key = 'login_attempts_' . md5($username); + + if (!isset($_SESSION[$key])) { + $_SESSION[$key] = ['count' => 0, 'time' => time()]; + return true; + } + + $attempts = $_SESSION[$key]; + + // 如果超过锁定时间,重置计数 + if (time() - $attempts['time'] > $lockoutTime) { + $_SESSION[$key] = ['count' => 0, 'time' => time()]; + return true; + } + + // 检查是否超过最大尝试次数 + if ($attempts['count'] >= $maxAttempts) { + return false; + } + + return true; + } + + /** + * 记录登录失败尝试 + * @param string $username 用户名 + */ + public static function recordLoginAttempt($username) { + $key = 'login_attempts_' . md5($username); + + if (!isset($_SESSION[$key])) { + $_SESSION[$key] = ['count' => 1, 'time' => time()]; + } else { + $_SESSION[$key]['count']++; + $_SESSION[$key]['time'] = time(); + } + } + + /** + * 清除登录尝试记录 + * @param string $username 用户名 + */ + public static function clearLoginAttempts($username) { + $key = 'login_attempts_' . md5($username); + unset($_SESSION[$key]); + } +} diff --git a/src/utils/Validator.php b/src/utils/Validator.php index 8a4bf63..0979e0c 100644 --- a/src/utils/Validator.php +++ b/src/utils/Validator.php @@ -1,157 +1,157 @@ - bool, 'errors' => array] 验证结果和错误信息 - */ - public static function validatePassword($password, $minLength = 6) { - $errors = []; - - if (strlen($password) < $minLength) { - $errors[] = "密码长度至少需要 {$minLength} 个字符"; - } - - if (preg_match('/^[a-zA-Z0-9]+$/', $password) && strlen($password) < 8) { - // 如果密码只包含字母和数字,且长度小于8,建议使用更复杂的密码 - // 但不强制要求 - } - - return [ - 'valid' => empty($errors), - 'errors' => $errors - ]; - } - - /** - * 验证用户名格式 - * @param string $username 用户名(邮箱格式) - * @return array ['valid' => bool, 'errors' => array] 验证结果和错误信息 - */ - public static function validateUsername($username) { - $errors = []; - - if (empty($username)) { - $errors[] = "用户名不能为空"; - } elseif (!self::validateEmail($username)) { - $errors[] = "用户名必须是有效的邮箱格式"; - } - - return [ - 'valid' => empty($errors), - 'errors' => $errors - ]; - } - - /** - * 验证IP地址格式 - * @param string $ip IP地址 - * @return bool 是否有效 - */ - public static function validateIP($ip) { - return filter_var($ip, FILTER_VALIDATE_IP) !== false; - } - - /** - * 验证端口号 - * @param int $port 端口号 - * @return bool 是否有效(1-65535) - */ - public static function validatePort($port) { - return is_numeric($port) && $port >= 1 && $port <= 65535; - } - - /** - * 验证非空字符串 - * @param string $value 待验证的值 - * @param string $fieldName 字段名称(用于错误提示) - * @return array ['valid' => bool, 'errors' => array] 验证结果和错误信息 - */ - public static function validateRequired($value, $fieldName = '字段') { - $errors = []; - - if (empty(trim($value))) { - $errors[] = "{$fieldName}不能为空"; - } - - return [ - 'valid' => empty($errors), - 'errors' => $errors - ]; - } - - /** - * 验证字符串长度 - * @param string $value 待验证的值 - * @param int $min 最小长度 - * @param int $max 最大长度 - * @param string $fieldName 字段名称 - * @return array ['valid' => bool, 'errors' => array] 验证结果和错误信息 - */ - public static function validateLength($value, $min, $max, $fieldName = '字段') { - $errors = []; - $length = mb_strlen($value, 'UTF-8'); - - if ($length < $min) { - $errors[] = "{$fieldName}长度不能少于 {$min} 个字符"; - } - - if ($length > $max) { - $errors[] = "{$fieldName}长度不能超过 {$max} 个字符"; - } - - return [ - 'valid' => empty($errors), - 'errors' => $errors - ]; - } - - /** - * 验证两个密码是否匹配 - * @param string $password 密码 - * @param string $confirmPassword 确认密码 - * @return array ['valid' => bool, 'errors' => array] 验证结果和错误信息 - */ - public static function validatePasswordMatch($password, $confirmPassword) { - $errors = []; - - if ($password !== $confirmPassword) { - $errors[] = "两次输入的密码不一致"; - } - - return [ - 'valid' => empty($errors), - 'errors' => $errors - ]; - } -} + bool, 'errors' => array] 验证结果和错误信息 + */ + public static function validatePassword($password, $minLength = 6) { + $errors = []; + + if (strlen($password) < $minLength) { + $errors[] = "密码长度至少需要 {$minLength} 个字符"; + } + + if (preg_match('/^[a-zA-Z0-9]+$/', $password) && strlen($password) < 8) { + // 如果密码只包含字母和数字,且长度小于8,建议使用更复杂的密码 + // 但不强制要求 + } + + return [ + 'valid' => empty($errors), + 'errors' => $errors + ]; + } + + /** + * 验证用户名格式 + * @param string $username 用户名(邮箱格式) + * @return array ['valid' => bool, 'errors' => array] 验证结果和错误信息 + */ + public static function validateUsername($username) { + $errors = []; + + if (empty($username)) { + $errors[] = "用户名不能为空"; + } elseif (!self::validateEmail($username)) { + $errors[] = "用户名必须是有效的邮箱格式"; + } + + return [ + 'valid' => empty($errors), + 'errors' => $errors + ]; + } + + /** + * 验证IP地址格式 + * @param string $ip IP地址 + * @return bool 是否有效 + */ + public static function validateIP($ip) { + return filter_var($ip, FILTER_VALIDATE_IP) !== false; + } + + /** + * 验证端口号 + * @param int $port 端口号 + * @return bool 是否有效(1-65535) + */ + public static function validatePort($port) { + return is_numeric($port) && $port >= 1 && $port <= 65535; + } + + /** + * 验证非空字符串 + * @param string $value 待验证的值 + * @param string $fieldName 字段名称(用于错误提示) + * @return array ['valid' => bool, 'errors' => array] 验证结果和错误信息 + */ + public static function validateRequired($value, $fieldName = '字段') { + $errors = []; + + if (empty(trim($value))) { + $errors[] = "{$fieldName}不能为空"; + } + + return [ + 'valid' => empty($errors), + 'errors' => $errors + ]; + } + + /** + * 验证字符串长度 + * @param string $value 待验证的值 + * @param int $min 最小长度 + * @param int $max 最大长度 + * @param string $fieldName 字段名称 + * @return array ['valid' => bool, 'errors' => array] 验证结果和错误信息 + */ + public static function validateLength($value, $min, $max, $fieldName = '字段') { + $errors = []; + $length = mb_strlen($value, 'UTF-8'); + + if ($length < $min) { + $errors[] = "{$fieldName}长度不能少于 {$min} 个字符"; + } + + if ($length > $max) { + $errors[] = "{$fieldName}长度不能超过 {$max} 个字符"; + } + + return [ + 'valid' => empty($errors), + 'errors' => $errors + ]; + } + + /** + * 验证两个密码是否匹配 + * @param string $password 密码 + * @param string $confirmPassword 确认密码 + * @return array ['valid' => bool, 'errors' => array] 验证结果和错误信息 + */ + public static function validatePasswordMatch($password, $confirmPassword) { + $errors = []; + + if ($password !== $confirmPassword) { + $errors[] = "两次输入的密码不一致"; + } + + return [ + 'valid' => empty($errors), + 'errors' => $errors + ]; + } +} diff --git a/test_smtp.sh b/test_smtp.sh index 6cb3802..50e8ca5 100644 --- a/test_smtp.sh +++ b/test_smtp.sh @@ -1,30 +1,30 @@ -#!/bin/bash -echo "测试SMTP服务器..." - -# 启动服务器(后台运行) -sudo php scripts/start_smtp.php & -SERVER_PID=$! -sleep 3 # 等待更长时间确保服务器启动 - -echo "发送测试邮件..." -# 使用timeout防止telnet无限等待 -timeout 5 telnet localhost 25 << 'EOF' -HELO test -MAIL FROM: -RCPT TO: -DATA -Subject: 自动测试邮件 -From: user1@test.com -To: admin@test.com - -这是自动发送的测试邮件 -. -QUIT -EOF - -# 等待服务器处理完成 -sleep 2 - - -kill $SERVER_PID 2>/dev/null +#!/bin/bash +echo "测试SMTP服务器..." + +# 启动服务器(后台运行) +sudo php scripts/start_smtp.php & +SERVER_PID=$! +sleep 3 # 等待更长时间确保服务器启动 + +echo "发送测试邮件..." +# 使用timeout防止telnet无限等待 +timeout 5 telnet localhost 25 << 'EOF' +HELO test +MAIL FROM: +RCPT TO: +DATA +Subject: 自动测试邮件 +From: user1@test.com +To: admin@test.com + +这是自动发送的测试邮件 +. +QUIT +EOF + +# 等待服务器处理完成 +sleep 2 + + +kill $SERVER_PID 2>/dev/null echo "测试完成" \ No newline at end of file