Compare commits

..

1 Commits

@ -1,3 +0,0 @@
{
"remote.autoForwardPortsFallback": 0
}

@ -1,322 +1,460 @@
# 邮件服务器项目 # 邮件服务器项目
基于POP3和SMTP协议的邮件服务端实现 基于POP3和SMTP协议的邮件服务端实现
## 环境要求 ## 环境要求
- Docker & Docker Compose - Docker & Docker Compose
- PHP 7.4+ (需要扩展: php-mysql, php-sockets) - PHP 7.4+ (需要扩展: php-mysql, php-sockets)
- WSL2 (Windows环境) - WSL2 (Windows环境)
- netstat
## 快速开始
## 快速开始
### 1. 安装PHP扩展如果未安装
### 1. 安装PHP扩展如果未安装
```bash
```bash sudo apt update
sudo apt update sudo apt install php php-cli php-mysql php-sockets -y
sudo apt install php php-cli php-mysql php-sockets -y ```
```
### 2. 启动数据库
### 2. 启动数据库
```bash
```bash cd /mnt/d/mailserver/mailserver
cd /mnt/d/mailserver/mailserver
# 首次启动或重置数据库
# 首次启动或重置数据库 docker-compose down -v
docker-compose down -v docker-compose up -d
docker-compose up -d
# 等待10-15秒让数据库初始化完成
# 等待10-15秒让数据库初始化完成 sleep 15
sleep 15 ```
```
### 3. 初始化管理功能数据库表(首次使用)
### 3. 初始化管理功能数据库表(首次使用)
```bash
```bash # 执行管理功能相关的数据库表创建
# 执行管理功能相关的数据库表创建 docker-compose exec mysql mysql -umail_user -puser123 mail_server < scripts/create_admin_tables.sql
docker-compose exec mysql mysql -umail_user -puser123 mail_server < scripts/create_admin_tables.sql ```
```
### 4. 查看数据库phpMyAdmin
### 4. 查看数据库phpMyAdmin
- 访问http://localhost:8088
- 访问http://localhost:8088 - 登录信息:
- 登录信息: - 服务器:`mysql`(或留空)
- 服务器:`mysql`(或留空) - 用户名:`root`
- 用户名:`root` - 密码:`root123`
- 密码:`root123`
### 5. 测试账号
### 5. 测试账号
- 管理员:`admin@test.com` / `123456`
- 管理员:`admin@test.com` / `123456` - 普通用户:`user1@test.com` / `123456`
- 普通用户:`user1@test.com` / `123456`
## 端口说明
## 端口说明
- **25** - SMTP服务器发送邮件
- **25** - SMTP服务器发送邮件 - **110** - POP3服务器接收邮件
- **110** - POP3服务器接收邮件 - **3308** - MySQL数据库
- **3308** - MySQL数据库 - **8080** - Web管理后台
- **8080** - Web管理后台 - **8088** - phpMyAdmin管理界面
- **8088** - phpMyAdmin管理界面
## 启动服务器
### SMTP服务器发送邮件
## Web管理后台
```bash
### 启动Web服务器 sudo php scripts/start_smtp.php
```
**方式1推荐从public目录启动**
```bash ### POP3服务器接收邮件
cd /mnt/d/mailserver/mailserver/public
php -S localhost:8080 ```bash
#如果打不开可以尝试换端口为8888或者别的 sudo php scripts/start_pop3.php
``` ```
**方式2从项目根目录启动** **注意**两个服务器需要分别在两个终端运行都需要sudo权限因为使用25和110端口
```bash
cd /mnt/d/mailserver/mailserver ## Web管理后台
php -S localhost:8080 -t public
``` ### 启动Web服务器
### 访问管理后台 **方式1推荐从public目录启动**
```bash
- 访问http://localhost:8080 cd /mnt/d/mailserver/mailserver/public
- 登录账号: php -S localhost:8080
- 管理员:`admin@test.com` / `123456` ```
- 普通用户:`user1@test.com` / `123456`
**方式2从项目根目录启动**
```bash
cd /mnt/d/mailserver/mailserver
## 测试服务器方法 php -S localhost:8080 -t public
```
### 测试SMTP发送邮件
### 访问管理后台
**终端1启动SMTP服务器**
```bash - 访问http://localhost:8080
sudo php scripts/start_smtp.php - 登录账号:
``` - 管理员:`admin@test.com` / `123456`
- 普通用户:`user1@test.com` / `123456`
**终端2连接测试**
```bash ### 功能模块
telnet localhost 25
``` #### 1. 用户注册
- **页面**`register.php`
**输入命令:** - **功能**:新用户注册,邮箱域名限制为 @test.com
```
HELO test #### 2. 用户管理(管理员)
MAIL FROM: <user1@test.com> - **页面**`users.php`
RCPT TO: <admin@test.com> - **功能**
DATA - 创建新用户(设置密码、管理员权限、激活状态)
Subject: 测试邮件 - 编辑用户信息(修改密码、权限、状态)
From: user1@test.com - 删除用户账号
To: admin@test.com - 查看用户列表
这是一封测试邮件! #### 3. 邮件管理
. - **页面**`emails.php`
QUIT - **功能**
``` - 查看邮件(管理员查看全部,普通用户查看自己的收件箱)
- 查看邮件详情
### 测试POP3接收邮件 - 标记邮件为已读
- 删除邮件
**终端1启动POP3服务器** - 分页浏览
```bash
sudo php scripts/start_pop3.php #### 4. 群发邮件(管理员)
``` - **页面**`broadcast.php`
- **功能**
**终端2连接测试** - 发送给所有用户
```bash - 发送给指定用户列表
telnet localhost 110 - 自定义邮件主题和内容
```
#### 5. 过滤规则
**输入命令:** - **页面**`filters.php`
``` - **功能**
USER admin@test.com - 创建邮箱过滤规则(阻止/允许特定邮箱)
PASS 123456 - 创建IP地址过滤规则阻止/允许特定IP
STAT - 启用/禁用过滤规则
LIST - 删除过滤规则
RETR 1
QUIT #### 6. 系统设置(管理员)
``` - **页面**`settings.php`
- **功能**
## 查看数据 - 设置SMTP端口默认25
- 设置POP3端口默认110
### 方法1phpMyAdmin推荐 - 设置服务器域名默认test.com
访问 http://localhost:8088选择 `mail_server` 数据库 - 设置用户邮箱大小限制
- 设置日志存储路径和最大大小
### 方法2命令行 - 修改管理员密码
```bash
# 查看用户 #### 7. 服务管理(管理员)
docker-compose exec mysql mysql -umail_user -puser123 mailserver -e "SELECT * FROM users;" - **页面**`services.php`
- **功能**
# 查看邮件 - 查看SMTP服务状态
docker-compose exec mysql mysql -umail_user -puser123 mailserver -e "SELECT id, sender, recipient, subject, created_at FROM emails ORDER BY id DESC;" - 查看POP3服务状态
``` - 启动/停止服务(状态管理)
## 重置数据库 #### 8. 日志管理
- **页面**`logs.php`
```bash - **功能**
docker-compose down -v - 查看所有日志
docker-compose up -d - 按类型过滤SMTP/POP3
sleep 15 - 查看日志统计信息
# 重新执行初始化脚本 - 清除日志(管理员)
docker-compose exec mysql mysql -umail_user -puser123 mail_server < scripts/create_admin_tables.sql
``` #### 9. 帮助
- **页面**`help.php`
## 常见问题 - **功能**:提供系统使用帮助文档
### web页面启停服务器显示端口未成功监听
- 确保web用户有进入目录执行开始脚本的能力可用下面命令测试观察输出。 ## 测试方法
```bash ### 测试SMTP发送邮件
#模拟www-data用户执行start_smtp.php脚本
sudo -u www-data php /home/clumxc/projects/mailserver/scripts/start_smtp.php **终端1启动SMTP服务器**
``` ```bash
- 需要sudo权限的低端口25、110无法通过Web页面开启需要先改为25252、1100等其它端口 sudo php scripts/start_smtp.php
或给 /usr/bin/php 这个可执行文件贴一张“特许证”,以后不管谁运行 php都能绑低端口不需要 root。 ```
一步一步做:
```bash **终端2连接测试**
#给 PHP 贴特许证,打开终端,执行: ```bash
sudo setcap 'cap_net_bind_service=+ep' /usr/bin/php telnet localhost 25
``` ```
```bash
#确认贴上了 **输入命令:**
getcap /usr/bin/php ```
#看到输出 HELO test
/usr/bin/php = cap_net_bind_service+ep MAIL FROM: <user1@test.com>
``` RCPT TO: <admin@test.com>
```bash DATA
#重启你的 Web 服务(让新能力生效) Subject: 测试邮件
#如果你用 Apache From: user1@test.com
sudo systemctl restart apache2 To: admin@test.com
#如果你用 Nginx + PHP-FPM
sudo systemctl restart php-fpm 这是一封测试邮件!
``` .
### 端口被占用 QUIT
```
**SMTP服务器启动失败25端口**
```bash ### 测试POP3接收邮件
# 检查端口占用
sudo netstat -tlnp | grep 25 **终端1启动POP3服务器**
```bash
# 或使用 sudo php scripts/start_pop3.php
sudo lsof -i :25 ```
```
**终端2连接测试**
**POP3服务器启动失败110端口** ```bash
```bash telnet localhost 110
sudo netstat -tlnp | grep 110 ```
```
**输入命令:**
### 数据库连接失败 ```
USER admin@test.com
```bash PASS 123456
# 检查Docker容器状态 STAT
docker-compose ps LIST
RETR 1
# 查看数据库日志 QUIT
docker-compose logs mysql ```
# 重启数据库 ### 测试Web管理后台
docker-compose restart mysql
``` 1. **用户注册测试**
- 访问http://localhost:8080/register.php
### telnet连接失败 - 注册新用户:`newuser@test.com` / `123456`
- 预期:注册成功,跳转到登录页
```bash
# 安装telnet如果未安装 2. **用户管理测试(管理员)**
sudo apt install telnet - 登录管理员账号
- 访问http://localhost:8080/users.php
# 或使用nc替代 - 创建、编辑、删除用户
nc localhost 25
``` 3. **群发邮件测试(管理员)**
- 访问http://localhost:8080/broadcast.php
### 密码验证失败 - 选择"发送给所有用户"或"发送给指定用户"
- 填写主题和内容,发送
- 确认使用正确的测试账号:`admin@test.com` / `123456` - 预期:显示成功发送数量
- 如果重置了数据库,密码会恢复为 `123456`
4. **系统设置测试(管理员)**
## 项目结构 - 访问http://localhost:8080/settings.php
- 修改端口、域名、邮箱大小等设置
``` - 修改管理员密码
mailserver/
├── scripts/ # 启动脚本和SQL 5. **过滤规则测试**
│ ├── start_smtp.php # 启动SMTP服务器 - 访问http://localhost:8080/filters.php
│ ├── start_pop3.php # 启动POP3服务器 - 创建邮箱过滤规则和IP过滤规则
│ ├── create_tables.sql # 数据库初始化脚本 - 测试启用/禁用功能
│ └── create_admin_tables.sql # 管理功能数据库表
├── src/ # 源代码 6. **日志管理测试**
│ ├── protocol/ # SMTP/POP3协议实现 - 访问http://localhost:8080/logs.php
│ │ ├── SmtpServer.php - 查看日志列表,按类型过滤
│ │ └── Pop3Server.php - 测试清除日志功能(管理员)
│ ├── storage/ # 数据存储层
│ │ ├── Database.php ## 查看数据
│ │ ├── UserRepository.php
│ │ ├── EmailRepository.php ### 方法1phpMyAdmin推荐
│ │ ├── SystemSettingsRepository.php 访问 http://localhost:8088选择 `mail_server` 数据库
│ │ ├── FilterRepository.php
│ │ ├── ServiceRepository.php ### 方法2命令行
│ │ └── MailboxRepository.php ```bash
│ ├── admin/ # 管理后台逻辑 # 查看用户
│ │ └── BroadcastService.php docker-compose exec mysql mysql -umail_user -puser123 mail_server -e "SELECT * FROM users;"
│ └── utils/ # 工具类
│ ├── Security.php # 查看邮件
│ └── Validator.php docker-compose exec mysql mysql -umail_user -puser123 mail_server -e "SELECT id, sender, recipient, subject, created_at FROM emails ORDER BY id DESC;"
├── public/ # Web管理界面 ```
│ ├── index.php # 主页面(登录+仪表盘)
│ ├── register.php # 用户注册 ## 重置数据库
│ ├── logout.php # 退出登录
│ ├── users.php # 用户管理 ```bash
│ ├── emails.php # 邮件管理 docker-compose down -v
│ ├── broadcast.php # 群发邮件 docker-compose up -d
│ ├── filters.php # 过滤规则 sleep 15
│ ├── settings.php # 系统设置 # 重新执行初始化脚本
│ ├── services.php # 服务管理 docker-compose exec mysql mysql -umail_user -puser123 mail_server < scripts/create_admin_tables.sql
│ ├── logs.php # 日志管理 ```
│ └── help.php # 帮助
├── config/ # 配置文件 ## 常见问题
│ ├── database.php # 数据库配置
│ └── constants.php # 常量定义 ### 端口被占用
└── docker-compose.yml # Docker配置
``` **SMTP服务器启动失败25端口**
```bash
## 功能完成情况 # 检查端口占用
sudo netstat -tlnp | grep 25
### ✅ 服务器端功能(已完成)
# 或使用
根据课程设计说明书要求,服务器端功能已全部实现: sudo lsof -i :25
```
1. **✅ 邮箱管理**
- 设置用户邮箱大小限制 **POP3服务器启动失败110端口**
- 查看用户邮箱使用情况 ```bash
sudo netstat -tlnp | grep 110
2. **✅ 客户管理** ```
- 创建新客户账号和密码
- 设置用户权限(管理员/普通用户) ### 数据库连接失败
- 启用/禁用用户
- 删除客户账号 ```bash
- 编辑用户信息 # 检查Docker容器状态
docker-compose ps
3. **✅ 服务起停**
- SMTP服务状态管理 # 查看数据库日志
- POP3服务状态管理 docker-compose logs mysql
- 服务启动/停止控制
# 重启数据库
4. **✅ 系统设置** docker-compose restart mysql
- SMTP端口设置默认25 ```
- POP3端口设置默认110
- 服务器域名设置默认test.com ### telnet连接失败
- 管理员密码修改
- 邮件过滤(账号过滤) ```bash
- IP地址过滤 # 安装telnet如果未安装
sudo apt install telnet
5. **✅ 日志管理**
- SMTP日志查看 # 或使用nc替代
- POP3日志查看 nc localhost 25
- 日志清除功能 ```
- 日志存储位置设置
- 日志文件大小管理 ### 密码验证失败
6. **✅ 日常管理** - 确认使用正确的测试账号:`admin@test.com` / `123456`
- 群发邮件功能(发送给所有用户或指定用户) - 如果重置了数据库,密码会恢复为 `123456`
7. **✅ 帮助** ## 项目结构
- 系统使用帮助文档
```
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

@ -1,21 +1,21 @@
<?php <?php
/** /**
* 数据库配置文件 * 数据库配置文件
* 使用环境变量或默认配置 * 使用环境变量或默认配置
*/ */
return [ return [
'host' => getenv('DB_HOST') ?: '127.0.0.1', // 使用127.0.0.1连接映射端口 'host' => getenv('DB_HOST') ?: '127.0.0.1', // 使用127.0.0.1连接映射端口
'port' => getenv('DB_PORT') ?: '3308', // Docker映射端口 'port' => getenv('DB_PORT') ?: '3308', // Docker映射端口
'database' => getenv('DB_DATABASE') ?: 'mail_server', 'database' => getenv('DB_DATABASE') ?: 'mail_server',
'username' => getenv('DB_USERNAME') ?: 'mail_user', 'username' => getenv('DB_USERNAME') ?: 'mail_user',
'password' => getenv('DB_PASSWORD') ?: 'user123', 'password' => getenv('DB_PASSWORD') ?: 'user123',
'charset' => 'utf8mb4', 'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci', 'collation' => 'utf8mb4_unicode_ci',
'options' => [ 'options' => [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false, PDO::ATTR_EMULATE_PREPARES => false,
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci" PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci"
] ]
]; ];

@ -1,44 +1,44 @@
services: services:
mysql: mysql:
image: mysql:8.0 image: mysql:8.0
container_name: mail-mysql container_name: mail-mysql
restart: unless-stopped restart: unless-stopped
environment: environment:
MYSQL_ROOT_PASSWORD: root123 MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: mail_server MYSQL_DATABASE: mail_server
MYSQL_USER: mail_user MYSQL_USER: mail_user
MYSQL_PASSWORD: user123 MYSQL_PASSWORD: user123
ports: ports:
- "3308:3306" - "3308:3306"
volumes: volumes:
- mysql-data:/var/lib/mysql - mysql-data:/var/lib/mysql
- ./scripts/create_all_tables.sql:/docker-entrypoint-initdb.d/init.sql - ./scripts/create_all_tables.sql:/docker-entrypoint-initdb.d/init.sql
command: command:
--character-set-server=utf8mb4 --character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci --collation-server=utf8mb4_unicode_ci
--default-authentication-plugin=mysql_native_password --default-authentication-plugin=mysql_native_password
healthcheck: healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-proot123"] test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-proot123"]
interval: 5s interval: 5s
timeout: 3s timeout: 3s
retries: 10 retries: 10
phpmyadmin: phpmyadmin:
image: phpmyadmin/phpmyadmin image: phpmyadmin/phpmyadmin
container_name: mail-phpmyadmin container_name: mail-phpmyadmin
restart: unless-stopped restart: unless-stopped
environment: environment:
PMA_HOST: mysql PMA_HOST: mysql
PMA_PORT: 3306 PMA_PORT: 3306
PMA_USER: root PMA_USER: root
PMA_PASSWORD: root123 PMA_PASSWORD: root123
UPLOAD_LIMIT: 64M UPLOAD_LIMIT: 64M
ports: ports:
- "8088:80" - "8088:80"
depends_on: depends_on:
mysql: mysql:
condition: service_healthy condition: service_healthy
volumes: volumes:
mysql-data: mysql-data:

@ -1,20 +0,0 @@
启动最简POP3邮件服务器
==============================
POP3服务器启动在 0.0.0.0:1100
按 Ctrl+C 停止
数据库连接成功
服务器: +OK POP3 Simple Server Ready
服务器: +OK POP3 Simple Server Ready
服务器: +OK POP3 Simple Server Ready
服务器: +OK POP3 Simple Server Ready
服务器: +OK POP3 Simple Server Ready
服务器: +OK POP3 Simple Server Ready
服务器: +OK POP3 Simple Server Ready
服务器: +OK POP3 Simple Server Ready
服务器: +OK POP3 Simple Server Ready
服务器: +OK POP3 Simple Server Ready
服务器: +OK POP3 Simple Server Ready
服务器: +OK POP3 Simple Server Ready
服务器: +OK POP3 Simple Server Ready

@ -1,7 +0,0 @@
启动最简SMTP邮件服务器
==============================
SMTP服务器启动在 0.0.0.0:25
按 Ctrl+C 停止
数据库连接成功

@ -1,194 +1,193 @@
<?php <?php
require_once __DIR__ . '/../config/database.php'; require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../src/storage/Database.php'; require_once __DIR__ . '/../src/storage/Database.php';
require_once __DIR__ . '/../src/storage/UserRepository.php'; require_once __DIR__ . '/../src/storage/UserRepository.php';
require_once __DIR__ . '/../src/storage/EmailRepository.php'; require_once __DIR__ . '/../src/storage/EmailRepository.php';
require_once __DIR__ . '/../src/admin/BroadcastService.php'; require_once __DIR__ . '/../src/admin/BroadcastService.php';
require_once __DIR__ . '/../src/utils/Security.php'; require_once __DIR__ . '/../src/utils/Security.php';
session_start(); session_start();
// 身份验证 // 身份验证
if (!isset($_SESSION['user_id'])) { if (!isset($_SESSION['user_id'])) {
header('Location: index.php'); header('Location: index.php');
exit; exit;
} }
// 检查管理员权限 // 检查管理员权限
if (!$_SESSION['is_admin']) { if (!$_SESSION['is_admin']) {
die('权限不足:只有管理员可以访问此页面'); die('权限不足:只有管理员可以访问此页面');
} }
$broadcastService = new BroadcastService(); $broadcastService = new BroadcastService();
$userRepo = new UserRepository(); $userRepo = new UserRepository();
$message = ''; $message = '';
$error = ''; $error = '';
// 处理群发邮件 // 处理群发邮件
if (isset($_POST['send_broadcast'])) { if (isset($_POST['send_broadcast'])) {
$subject = trim($_POST['subject'] ?? ''); $subject = trim($_POST['subject'] ?? '');
$body = trim($_POST['body'] ?? ''); $body = trim($_POST['body'] ?? '');
$broadcastType = $_POST['broadcast_type'] ?? 'all'; $broadcastType = $_POST['broadcast_type'] ?? 'all';
$recipients = trim($_POST['recipients'] ?? ''); $recipients = trim($_POST['recipients'] ?? '');
if (empty($subject)) { if (empty($subject)) {
$error = "主题不能为空"; $error = "主题不能为空";
} elseif (empty($body)) { } elseif (empty($body)) {
$error = "邮件内容不能为空"; $error = "邮件内容不能为空";
} else { } else {
$senderEmail = $_SESSION['username']; $senderEmail = $_SESSION['username'];
try { try {
if ($broadcastType === 'all') { if ($broadcastType === 'all') {
// 群发给所有用户 // 群发给所有用户
$result = $broadcastService->broadcastToAll($senderEmail, $subject, $body); $result = $broadcastService->broadcastToAll($senderEmail, $subject, $body);
} else { } else {
// 群发给指定用户 // 群发给指定用户
$recipientList = array_filter(array_map('trim', explode(',', $recipients))); $recipientList = array_filter(array_map('trim', explode(',', $recipients)));
if (empty($recipientList)) { if (empty($recipientList)) {
$error = "请指定收件人"; $error = "请指定收件人";
} else { } else {
$result = $broadcastService->broadcastToUsers($senderEmail, $recipientList, $subject, $body); $result = $broadcastService->broadcastToUsers($senderEmail, $recipientList, $subject, $body);
} }
} }
if (isset($result)) { if (isset($result)) {
if ($result['success'] > 0) { if ($result['success'] > 0) {
$message = "群发成功!成功发送 {$result['success']} 封邮件"; $message = "群发成功!成功发送 {$result['success']} 封邮件";
if ($result['failed'] > 0) { if ($result['failed'] > 0) {
$message .= ",失败 {$result['failed']} 封"; $message .= ",失败 {$result['failed']} 封";
} }
if (!empty($result['errors'])) { if (!empty($result['errors'])) {
$error = "部分失败:" . implode('<br>', array_slice($result['errors'], 0, 5)); $error = "部分失败:" . implode('<br>', array_slice($result['errors'], 0, 5));
if (count($result['errors']) > 5) { if (count($result['errors']) > 5) {
$error .= "<br>... 还有 " . (count($result['errors']) - 5) . " 个错误"; $error .= "<br>... 还有 " . (count($result['errors']) - 5) . " 个错误";
} }
} }
} else { } else {
$error = "群发失败:" . implode('<br>', $result['errors']); $error = "群发失败:" . implode('<br>', $result['errors']);
} }
} }
} catch (Exception $e) { } catch (Exception $e) {
$error = "群发失败: " . $e->getMessage(); $error = "群发失败: " . $e->getMessage();
} }
} }
} }
// 获取所有用户列表(用于选择收件人) // 获取所有用户列表(用于选择收件人)
$allUsers = $userRepo->getAll(); $allUsers = $userRepo->getAll();
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>群发邮件 - 邮件服务器</title> <title>群发邮件 - 邮件服务器</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<style> <style>
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; } body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
.header { background: #007bff; color: white; padding: 15px; margin: -20px -20px 20px -20px; } .header { background: #007bff; color: white; padding: 15px; margin: -20px -20px 20px -20px; }
.menu { background: white; padding: 10px; margin-bottom: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .menu { background: white; padding: 10px; margin-bottom: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.menu a { margin-right: 15px; text-decoration: none; color: #007bff; } .menu a { margin-right: 15px; text-decoration: none; color: #007bff; }
.container { background: white; padding: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); max-width: 800px; } .container { background: white; padding: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); max-width: 800px; }
.message { background: #d4edda; color: #155724; padding: 12px; border-radius: 5px; margin-bottom: 20px; } .message { background: #d4edda; color: #155724; padding: 12px; border-radius: 5px; margin-bottom: 20px; }
.error { background: #f8d7da; color: #721c24; padding: 12px; border-radius: 5px; margin-bottom: 20px; } .error { background: #f8d7da; color: #721c24; padding: 12px; border-radius: 5px; margin-bottom: 20px; }
.form-group { margin-bottom: 20px; } .form-group { margin-bottom: 20px; }
.form-group label { display: block; margin-bottom: 8px; font-weight: 500; } .form-group label { display: block; margin-bottom: 8px; font-weight: 500; }
.form-group input, .form-group textarea, .form-group select { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; } .form-group input, .form-group textarea, .form-group select { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; }
.form-group textarea { min-height: 200px; font-family: monospace; } .form-group textarea { min-height: 200px; font-family: monospace; }
.form-group small { color: #666; font-size: 12px; } .form-group small { color: #666; font-size: 12px; }
.btn { padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; } .btn { padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; }
.btn-primary { background: #007bff; color: white; } .btn-primary { background: #007bff; color: white; }
.btn-primary:hover { background: #0056b3; } .btn-primary:hover { background: #0056b3; }
.radio-group { display: flex; gap: 20px; margin-bottom: 15px; } .radio-group { display: flex; gap: 20px; margin-bottom: 15px; }
.radio-group label { display: flex; align-items: center; cursor: pointer; } .radio-group label { display: flex; align-items: center; cursor: pointer; }
.radio-group input[type="radio"] { width: auto; margin-right: 5px; } .radio-group input[type="radio"] { width: auto; margin-right: 5px; }
.user-list { max-height: 200px; overflow-y: auto; border: 1px solid #ddd; padding: 10px; border-radius: 4px; background: #f8f9fa; } .user-list { max-height: 200px; overflow-y: auto; border: 1px solid #ddd; padding: 10px; border-radius: 4px; background: #f8f9fa; }
.user-list-item { padding: 5px; } .user-list-item { padding: 5px; }
</style> </style>
</head> </head>
<body> <body>
<div class="header"> <div class="header">
<h1>邮件服务器管理后台</h1> <h1>邮件服务器管理后台</h1>
<div>欢迎, <?php echo htmlspecialchars($_SESSION['username']); ?> <div>欢迎, <?php echo htmlspecialchars($_SESSION['username']); ?>
(<a href="logout.php" style="color: white;">退出</a>) (<a href="logout.php" style="color: white;">退出</a>)
</div> </div>
</div> </div>
<div class="menu"> <div class="menu">
<a href="index.php">仪表盘</a> <a href="index.php">仪表盘</a>
<a href="users.php">用户管理</a> <a href="users.php">用户管理</a>
<a href="broadcast.php">群发邮件</a> <a href="emails.php">邮件管理</a>
<a href="filters.php">过滤规则</a> <a href="broadcast.php">群发邮件</a>
<a href="logs.php">系统日志</a> <a href="filters.php">过滤规则</a>
<a href="services.php">服务管理</a> <a href="logs.php">系统日志</a>
<a href="settings.php">系统设置</a> <a href="settings.php">系统设置</a>
<a href="help.php">帮助</a> </div>
</div>
<div class="container">
<div class="container"> <h2>群发邮件</h2>
<h2>群发邮件</h2>
<?php if ($message): ?>
<?php if ($message): ?> <div class="message"><?php echo $message; ?></div>
<div class="message"><?php echo $message; ?></div> <?php endif; ?>
<?php endif; ?>
<?php if ($error): ?>
<?php if ($error): ?> <div class="error"><?php echo $error; ?></div>
<div class="error"><?php echo $error; ?></div> <?php endif; ?>
<?php endif; ?>
<form method="POST">
<form method="POST"> <div class="form-group">
<div class="form-group"> <label>发送方式</label>
<label>发送方式</label> <div class="radio-group">
<div class="radio-group"> <label>
<label> <input type="radio" name="broadcast_type" value="all" checked onchange="toggleRecipients()">
<input type="radio" name="broadcast_type" value="all" checked onchange="toggleRecipients()"> 发送给所有用户
发送给所有用户 </label>
</label> <label>
<label> <input type="radio" name="broadcast_type" value="selected" onchange="toggleRecipients()">
<input type="radio" name="broadcast_type" value="selected" onchange="toggleRecipients()"> 发送给指定用户
发送给指定用户 </label>
</label> </div>
</div> </div>
</div>
<div class="form-group" id="recipients-group" style="display: none;">
<div class="form-group" id="recipients-group" style="display: none;"> <label>收件人(多个邮箱用逗号分隔)</label>
<label>收件人(多个邮箱用逗号分隔)</label> <input type="text" name="recipients" placeholder="user1@test.com, user2@test.com">
<input type="text" name="recipients" placeholder="user1@test.com, user2@test.com"> <small>请输入邮箱地址,多个邮箱用逗号分隔</small>
<small>请输入邮箱地址,多个邮箱用逗号分隔</small> <div class="user-list">
<div class="user-list"> <strong>可用用户列表:</strong>
<strong>可用用户列表:</strong> <?php foreach ($allUsers as $user): ?>
<?php foreach ($allUsers as $user): ?> <?php if ($user['is_active'] && $user['username'] !== $_SESSION['username']): ?>
<?php if ($user['is_active'] && $user['username'] !== $_SESSION['username']): ?> <div class="user-list-item"><?php echo htmlspecialchars($user['username']); ?></div>
<div class="user-list-item"><?php echo htmlspecialchars($user['username']); ?></div> <?php endif; ?>
<?php endif; ?> <?php endforeach; ?>
<?php endforeach; ?> </div>
</div> </div>
</div>
<div class="form-group">
<div class="form-group"> <label>邮件主题 *</label>
<label>邮件主题 *</label> <input type="text" name="subject" required placeholder="请输入邮件主题" value="<?php echo htmlspecialchars($_POST['subject'] ?? ''); ?>">
<input type="text" name="subject" required placeholder="请输入邮件主题" value="<?php echo htmlspecialchars($_POST['subject'] ?? ''); ?>"> </div>
</div>
<div class="form-group">
<div class="form-group"> <label>邮件内容 *</label>
<label>邮件内容 *</label> <textarea name="body" required placeholder="请输入邮件内容"><?php echo htmlspecialchars($_POST['body'] ?? ''); ?></textarea>
<textarea name="body" required placeholder="请输入邮件内容"><?php echo htmlspecialchars($_POST['body'] ?? ''); ?></textarea> </div>
</div>
<button type="submit" name="send_broadcast" class="btn btn-primary">发送群发邮件</button>
<button type="submit" name="send_broadcast" class="btn btn-primary">发送群发邮件</button> </form>
</form> </div>
</div>
<script>
<script> function toggleRecipients() {
function toggleRecipients() { const broadcastType = document.querySelector('input[name="broadcast_type"]:checked').value;
const broadcastType = document.querySelector('input[name="broadcast_type"]:checked').value; const recipientsGroup = document.getElementById('recipients-group');
const recipientsGroup = document.getElementById('recipients-group'); if (broadcastType === 'selected') {
if (broadcastType === 'selected') { recipientsGroup.style.display = 'block';
recipientsGroup.style.display = 'block'; } else {
} else { recipientsGroup.style.display = 'none';
recipientsGroup.style.display = 'none'; }
} }
} </script>
</script> </body>
</body> </html>
</html>

@ -1,238 +1,240 @@
<?php <?php
require_once __DIR__ . '/../config/database.php'; require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../src/storage/Database.php'; require_once __DIR__ . '/../src/storage/Database.php';
require_once __DIR__ . '/../src/storage/EmailRepository.php'; require_once __DIR__ . '/../src/storage/EmailRepository.php';
require_once __DIR__ . '/../src/utils/Security.php'; require_once __DIR__ . '/../src/utils/Security.php';
session_start(); session_start();
// 身份验证 // 身份验证
if (!isset($_SESSION['user_id'])) { if (!isset($_SESSION['user_id'])) {
header('Location: index.php'); header('Location: index.php');
exit; exit;
} }
$emailRepo = new EmailRepository(); $emailRepo = new EmailRepository();
$message = ''; $message = '';
$error = ''; $error = '';
// 处理删除邮件 // 处理删除邮件
if (isset($_GET['delete'])) { if (isset($_GET['delete'])) {
$emailId = (int)$_GET['delete']; $emailId = (int)$_GET['delete'];
if ($emailRepo->delete($emailId)) { if ($emailRepo->delete($emailId)) {
$message = "邮件删除成功"; $message = "邮件删除成功";
} else { } else {
$error = "删除失败"; $error = "删除失败";
} }
} }
// 处理标记已读 // 处理标记已读
if (isset($_GET['mark_read'])) { if (isset($_GET['mark_read'])) {
$emailId = (int)$_GET['mark_read']; $emailId = (int)$_GET['mark_read'];
if ($emailRepo->markAsRead($emailId)) { if ($emailRepo->markAsRead($emailId)) {
$message = "邮件已标记为已读"; $message = "邮件已标记为已读";
} }
} }
// 获取邮件列表 // 获取邮件列表
$isAdmin = $_SESSION['is_admin'] ?? false; $isAdmin = $_SESSION['is_admin'] ?? false;
$userId = $_SESSION['user_id']; $userId = $_SESSION['user_id'];
// 分页参数 // 分页参数
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1; $page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$perPage = 20; $perPage = 20;
$offset = ($page - 1) * $perPage; $offset = ($page - 1) * $perPage;
// 获取邮件 // 获取邮件
if ($isAdmin) { if ($isAdmin) {
$emails = $emailRepo->getAll($perPage, $offset); $emails = $emailRepo->getAll($perPage, $offset);
$totalEmails = $emailRepo->getCount(); $totalEmails = $emailRepo->getCount();
} else { } else {
$emails = $emailRepo->getInbox($userId, $perPage, $offset); $emails = $emailRepo->getInbox($userId, $perPage, $offset);
$totalEmails = $emailRepo->getCount($userId); $totalEmails = $emailRepo->getCount($userId);
} }
$totalPages = ceil($totalEmails / $perPage); $totalPages = ceil($totalEmails / $perPage);
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>邮件管理 - 邮件服务器</title> <title>邮件管理 - 邮件服务器</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<style> <style>
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; } body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
.header { background: #007bff; color: white; padding: 15px; margin: -20px -20px 20px -20px; } .header { background: #007bff; color: white; padding: 15px; margin: -20px -20px 20px -20px; }
.menu { background: white; padding: 10px; margin-bottom: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .menu { background: white; padding: 10px; margin-bottom: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.menu a { margin-right: 15px; text-decoration: none; color: #007bff; } .menu a { margin-right: 15px; text-decoration: none; color: #007bff; }
.container { background: white; padding: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .container { background: white; padding: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.message { background: #d4edda; color: #155724; padding: 12px; border-radius: 5px; margin-bottom: 20px; } .message { background: #d4edda; color: #155724; padding: 12px; border-radius: 5px; margin-bottom: 20px; }
.error { background: #f8d7da; color: #721c24; padding: 12px; border-radius: 5px; margin-bottom: 20px; } .error { background: #f8d7da; color: #721c24; padding: 12px; border-radius: 5px; margin-bottom: 20px; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; } table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; } th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
th { background: #f8f9fa; font-weight: 600; } th { background: #f8f9fa; font-weight: 600; }
tr:hover { background: #f8f9fa; } tr:hover { background: #f8f9fa; }
.btn { padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; text-decoration: none; display: inline-block; } .btn { padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; text-decoration: none; display: inline-block; }
.btn-primary { background: #007bff; color: white; } .btn-primary { background: #007bff; color: white; }
.btn-danger { background: #dc3545; color: white; } .btn-danger { background: #dc3545; color: white; }
.btn-success { background: #28a745; color: white; } .btn-success { background: #28a745; color: white; }
.btn-small { padding: 4px 8px; font-size: 12px; } .btn-small { padding: 4px 8px; font-size: 12px; }
.badge { padding: 4px 8px; border-radius: 3px; font-size: 12px; font-weight: 500; } .badge { padding: 4px 8px; border-radius: 3px; font-size: 12px; font-weight: 500; }
.badge-read { background: #6c757d; color: white; } .badge-read { background: #6c757d; color: white; }
.badge-unread { background: #007bff; color: white; } .badge-unread { background: #007bff; color: white; }
.email-unread { font-weight: bold; } .email-unread { font-weight: bold; }
.pagination { margin-top: 20px; text-align: center; } .pagination { margin-top: 20px; text-align: center; }
.pagination a { display: inline-block; padding: 8px 12px; margin: 0 4px; text-decoration: none; border: 1px solid #ddd; border-radius: 4px; } .pagination a { display: inline-block; padding: 8px 12px; margin: 0 4px; text-decoration: none; border: 1px solid #ddd; border-radius: 4px; }
.pagination a:hover { background: #f8f9fa; } .pagination a:hover { background: #f8f9fa; }
.pagination .current { background: #007bff; color: white; border-color: #007bff; } .pagination .current { background: #007bff; color: white; border-color: #007bff; }
.email-preview { max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .email-preview { max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); overflow: auto; } .modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); overflow: auto; }
.modal-content { background: white; margin: 50px auto; padding: 20px; width: 80%; max-width: 800px; border-radius: 5px; } .modal-content { background: white; margin: 50px auto; padding: 20px; width: 80%; max-width: 800px; border-radius: 5px; }
.close { float: right; font-size: 28px; font-weight: bold; cursor: pointer; } .close { float: right; font-size: 28px; font-weight: bold; cursor: pointer; }
.email-body { white-space: pre-wrap; background: #f8f9fa; padding: 15px; border-radius: 5px; margin-top: 10px; } .email-body { white-space: pre-wrap; background: #f8f9fa; padding: 15px; border-radius: 5px; margin-top: 10px; }
</style> </style>
</head> </head>
<body> <body>
<div class="header"> <div class="header">
<h1>邮件服务器管理后台</h1> <h1>邮件服务器管理后台</h1>
<div>欢迎, <?php echo htmlspecialchars($_SESSION['username']); ?> <div>欢迎, <?php echo htmlspecialchars($_SESSION['username']); ?>
(<a href="logout.php" style="color: white;">退出</a>) (<a href="logout.php" style="color: white;">退出</a>)
</div> </div>
</div> </div>
<div class="menu"> <div class="menu">
<a href="index.php">仪表盘</a> <a href="index.php">仪表盘</a>
<a href="users.php">用户管理</a> <?php if ($isAdmin): ?>
<a href="broadcast.php">群发邮件</a> <a href="users.php">用户管理</a>
<a href="filters.php">过滤规则</a> <?php endif; ?>
<a href="logs.php">系统日志</a> <a href="emails.php">邮件管理</a>
<a href="services.php">服务管理</a> <a href="filters.php">过滤规则</a>
<a href="settings.php">系统设置</a> <a href="logs.php">系统日志</a>
<a href="help.php">帮助</a> <?php if ($isAdmin): ?>
</div> <a href="settings.php">系统设置</a>
<?php endif; ?>
<div class="container"> </div>
<h2>邮箱管理 <?php if ($isAdmin): ?>(全部邮件)<?php else: ?>(我的收件箱)<?php endif; ?></h2>
<div class="container">
<?php if ($message): ?> <h2>邮件管理 <?php if ($isAdmin): ?>(全部邮件)<?php else: ?>(我的收件箱)<?php endif; ?></h2>
<div class="message"><?php echo $message; ?></div>
<?php endif; ?> <?php if ($message): ?>
<div class="message"><?php echo $message; ?></div>
<?php if ($error): ?> <?php endif; ?>
<div class="error"><?php echo $error; ?></div>
<?php endif; ?> <?php if ($error): ?>
<div class="error"><?php echo $error; ?></div>
<p><?php echo $totalEmails; ?> 封邮件</p> <?php endif; ?>
<table> <p><?php echo $totalEmails; ?> 封邮件</p>
<thead>
<tr> <table>
<th>ID</th> <thead>
<th>发件人</th> <tr>
<th>收件人</th> <th>ID</th>
<th>主题</th> <th>发件人</th>
<th>状态</th> <th>收件人</th>
<th>时间</th> <th>主题</th>
<th>操作</th> <th>状态</th>
</tr> <th>时间</th>
</thead> <th>操作</th>
<tbody> </tr>
<?php if (empty($emails)): ?> </thead>
<tr> <tbody>
<td colspan="7" style="text-align: center; padding: 40px;"> <?php if (empty($emails)): ?>
暂无邮件 <tr>
</td> <td colspan="7" style="text-align: center; padding: 40px;">
</tr> 暂无邮件
<?php else: ?> </td>
<?php foreach ($emails as $email): ?> </tr>
<tr class="<?php echo $email['is_read'] ? '' : 'email-unread'; ?>"> <?php else: ?>
<td><?php echo $email['id']; ?></td> <?php foreach ($emails as $email): ?>
<td><?php echo htmlspecialchars($email['sender_name'] ?? $email['sender'] ?? '未知'); ?></td> <tr class="<?php echo $email['is_read'] ? '' : 'email-unread'; ?>">
<td><?php echo htmlspecialchars($email['recipient_name'] ?? $email['recipient'] ?? '未知'); ?></td> <td><?php echo $email['id']; ?></td>
<td class="email-preview"> <td><?php echo htmlspecialchars($email['sender_name'] ?? $email['sender'] ?? '未知'); ?></td>
<a href="#" onclick="viewEmail(<?php echo htmlspecialchars(json_encode($email)); ?>); return false;"> <td><?php echo htmlspecialchars($email['recipient_name'] ?? $email['recipient'] ?? '未知'); ?></td>
<?php echo htmlspecialchars($email['subject'] ?? '(无主题)'); ?> <td class="email-preview">
</a> <a href="#" onclick="viewEmail(<?php echo htmlspecialchars(json_encode($email)); ?>); return false;">
</td> <?php echo htmlspecialchars($email['subject'] ?? '(无主题)'); ?>
<td> </a>
<?php if ($email['is_read']): ?> </td>
<span class="badge badge-read">已读</span> <td>
<?php else: ?> <?php if ($email['is_read']): ?>
<span class="badge badge-unread">未读</span> <span class="badge badge-read">已读</span>
<?php endif; ?> <?php else: ?>
</td> <span class="badge badge-unread">未读</span>
<td><?php echo $email['created_at']; ?></td> <?php endif; ?>
<td> </td>
<a href="#" onclick="viewEmail(<?php echo htmlspecialchars(json_encode($email)); ?>); return false;" class="btn btn-primary btn-small">查看</a> <td><?php echo $email['created_at']; ?></td>
<?php if (!$email['is_read']): ?> <td>
<a href="?mark_read=<?php echo $email['id']; ?>" class="btn btn-success btn-small">标记已读</a> <a href="#" onclick="viewEmail(<?php echo htmlspecialchars(json_encode($email)); ?>); return false;" class="btn btn-primary btn-small">查看</a>
<?php endif; ?> <?php if (!$email['is_read']): ?>
<a href="?delete=<?php echo $email['id']; ?>" class="btn btn-danger btn-small" onclick="return confirm('确定要删除此邮件吗?');">删除</a> <a href="?mark_read=<?php echo $email['id']; ?>" class="btn btn-success btn-small">标记已读</a>
</td> <?php endif; ?>
</tr> <a href="?delete=<?php echo $email['id']; ?>" class="btn btn-danger btn-small" onclick="return confirm('确定要删除此邮件吗?');">删除</a>
<?php endforeach; ?> </td>
<?php endif; ?> </tr>
</tbody> <?php endforeach; ?>
</table> <?php endif; ?>
</tbody>
<!-- 分页 --> </table>
<?php if ($totalPages > 1): ?>
<div class="pagination"> <!-- 分页 -->
<?php if ($page > 1): ?> <?php if ($totalPages > 1): ?>
<a href="?page=<?php echo $page - 1; ?>">上一页</a> <div class="pagination">
<?php endif; ?> <?php if ($page > 1): ?>
<a href="?page=<?php echo $page - 1; ?>">上一页</a>
<?php for ($i = 1; $i <= $totalPages; $i++): ?> <?php endif; ?>
<?php if ($i == $page): ?>
<span class="current"><?php echo $i; ?></span> <?php for ($i = 1; $i <= $totalPages; $i++): ?>
<?php else: ?> <?php if ($i == $page): ?>
<a href="?page=<?php echo $i; ?>"><?php echo $i; ?></a> <span class="current"><?php echo $i; ?></span>
<?php endif; ?> <?php else: ?>
<?php endfor; ?> <a href="?page=<?php echo $i; ?>"><?php echo $i; ?></a>
<?php endif; ?>
<?php if ($page < $totalPages): ?> <?php endfor; ?>
<a href="?page=<?php echo $page + 1; ?>">下一页</a>
<?php endif; ?> <?php if ($page < $totalPages): ?>
</div> <a href="?page=<?php echo $page + 1; ?>">下一页</a>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php endif; ?>
<!-- 查看邮件模态框 --> </div>
<div id="emailModal" class="modal">
<div class="modal-content"> <!-- 查看邮件模态框 -->
<span class="close" onclick="closeEmailModal()">&times;</span> <div id="emailModal" class="modal">
<h3 id="email-subject">邮件详情</h3> <div class="modal-content">
<div> <span class="close" onclick="closeEmailModal()">&times;</span>
<strong>发件人:</strong><span id="email-sender"></span><br> <h3 id="email-subject">邮件详情</h3>
<strong>收件人:</strong><span id="email-recipient"></span><br> <div>
<strong>时间:</strong><span id="email-time"></span><br> <strong>发件人:</strong><span id="email-sender"></span><br>
<strong>主题:</strong><span id="email-subject-text"></span> <strong>收件人:</strong><span id="email-recipient"></span><br>
</div> <strong>时间:</strong><span id="email-time"></span><br>
<div class="email-body" id="email-body"></div> <strong>主题:</strong><span id="email-subject-text"></span>
</div> </div>
</div> <div class="email-body" id="email-body"></div>
</div>
<script> </div>
function viewEmail(email) {
document.getElementById('email-subject').textContent = email.subject || '(无主题)'; <script>
document.getElementById('email-subject-text').textContent = email.subject || '(无主题)'; function viewEmail(email) {
document.getElementById('email-sender').textContent = email.sender_name || email.sender || '未知'; document.getElementById('email-subject').textContent = email.subject || '(无主题)';
document.getElementById('email-recipient').textContent = email.recipient_name || email.recipient || '未知'; document.getElementById('email-subject-text').textContent = email.subject || '(无主题)';
document.getElementById('email-time').textContent = email.created_at; document.getElementById('email-sender').textContent = email.sender_name || email.sender || '未知';
document.getElementById('email-body').textContent = email.body || '(无内容)'; document.getElementById('email-recipient').textContent = email.recipient_name || email.recipient || '未知';
document.getElementById('emailModal').style.display = 'block'; document.getElementById('email-time').textContent = email.created_at;
} document.getElementById('email-body').textContent = email.body || '(无内容)';
document.getElementById('emailModal').style.display = 'block';
function closeEmailModal() { }
document.getElementById('emailModal').style.display = 'none';
} function closeEmailModal() {
document.getElementById('emailModal').style.display = 'none';
window.onclick = function(event) { }
var modal = document.getElementById('emailModal');
if (event.target == modal) { window.onclick = function(event) {
closeEmailModal(); var modal = document.getElementById('emailModal');
} if (event.target == modal) {
} closeEmailModal();
</script> }
</body> }
</html> </script>
</body>
</html>

@ -1,257 +1,243 @@
<?php <?php
require_once __DIR__ . '/../config/database.php'; require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../src/storage/Database.php'; require_once __DIR__ . '/../src/storage/Database.php';
require_once __DIR__ . '/../src/storage/FilterRepository.php'; require_once __DIR__ . '/../src/storage/FilterRepository.php';
require_once __DIR__ . '/../src/utils/Validator.php'; require_once __DIR__ . '/../src/utils/Validator.php';
require_once __DIR__ . '/../src/utils/Security.php'; require_once __DIR__ . '/../src/utils/Security.php';
//开启所有错误日志报告 session_start();
error_reporting(E_ALL);
ini_set('display_errors', 1); // 身份验证
if (!isset($_SESSION['user_id'])) {
session_start(); header('Location: index.php');
exit;
// 身份验证 }
if (!isset($_SESSION['user_id'])) {
header('Location: index.php'); $filterRepo = new FilterRepository();
exit; $message = '';
} $error = '';
$filterRepo = new FilterRepository(); // 处理创建过滤规则
$message = ''; if (isset($_POST['create_filter'])) {
$error = ''; $ruleType = $_POST['rule_type'] ?? '';
$ruleValue = trim($_POST['rule_value'] ?? '');
if (isset($_POST['toggle_id'])) { $action = $_POST['action'] ?? 'block';
$id = (int)$_POST['toggle_id']; $description = trim($_POST['description'] ?? '');
$row = $filterRepo->getById($id);
if ($row) { if (empty($ruleValue)) {
$filterRepo->updateStatus($id, !(bool)$row['is_active']); $error = "规则值不能为空";
} } else {
header('Location: filters.php'); // 302 跳回干净地址 if ($ruleType === 'email') {
exit; if (!Validator::validateEmail($ruleValue)) {
} $error = "邮箱格式无效";
}
} elseif ($ruleType === 'ip') {
if (!Validator::validateIP($ruleValue)) {
// 处理创建过滤规则 $error = "IP地址格式无效";
if (isset($_POST['create_filter'])) { }
$ruleType = $_POST['rule_type'] ?? ''; } else {
$ruleValue = trim($_POST['rule_value'] ?? ''); $error = "规则类型无效";
$action = $_POST['action'] ?? 'block'; }
$description = trim($_POST['description'] ?? '');
if (empty($error)) {
if (empty($ruleValue)) { try {
$error = "规则值不能为空"; if ($filterRepo->create($ruleType, $ruleValue, $action, $description)) {
} else { $message = "过滤规则创建成功";
if ($ruleType === 'email') { } else {
if (!Validator::validateEmail($ruleValue)) { $error = "创建失败,可能已存在相同规则";
$error = "邮箱格式无效"; }
} } catch (Exception $e) {
} elseif ($ruleType === 'ip') { $error = "创建失败: " . $e->getMessage();
if (!Validator::validateIP($ruleValue)) { }
$error = "IP地址格式无效"; }
} }
} else { }
$error = "规则类型无效";
} // 处理删除规则
if (isset($_GET['delete'])) {
if (empty($error)) { $id = (int)$_GET['delete'];
try { if ($filterRepo->delete($id)) {
if ($filterRepo->create($ruleType, $ruleValue, $action, $description)) { $message = "规则删除成功";
$message = "过滤规则创建成功"; } else {
} else { $error = "删除失败";
$error = "创建失败,可能已存在相同规则"; }
} }
} catch (Exception $e) {
$error = "创建失败: " . $e->getMessage(); // 处理切换规则状态
} if (isset($_GET['toggle'])) {
} $id = (int)$_GET['toggle'];
} $rule = $filterRepo->getAll();
} $currentRule = null;
foreach ($rule as $r) {
// 处理删除规则 if ($r['id'] == $id) {
if (isset($_GET['delete'])) { $currentRule = $r;
$id = (int)$_GET['delete']; break;
if ($filterRepo->delete($id)) { }
$message = "规则删除成功"; }
} else { if ($currentRule) {
$error = "删除失败"; $newStatus = !$currentRule['is_active'];
} if ($filterRepo->updateStatus($id, $newStatus)) {
} $message = "规则状态已更新";
} else {
/*------------------------------------ $error = "更新失败";
// 处理切换规则状态 }
if (isset($_GET['toggle'])) { }
$id = (int)$_GET['toggle']; }
$row = $filterRepo->getById($id); // 改用 public 方法
if ($row) { // 获取所有规则
$newStatus = !(bool)$row['is_active']; $rules = $filterRepo->getAll();
if ($filterRepo->updateStatus($id, $newStatus)) { ?>
$message = "规则状态已更新"; <!DOCTYPE html>
} else { <html>
$error = "更新失败"; <head>
} <title>过滤规则 - 邮件服务器</title>
} else { <meta charset="UTF-8">
$error = "规则不存在"; <style>
} body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
} .header { background: #007bff; color: white; padding: 15px; margin: -20px -20px 20px -20px; }
------------------------------------ */ .menu { background: white; padding: 10px; margin-bottom: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
// 获取所有规则 .menu a { margin-right: 15px; text-decoration: none; color: #007bff; }
$rules = $filterRepo->getAll(); .container { background: white; padding: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.message { background: #d4edda; color: #155724; padding: 12px; border-radius: 5px; margin-bottom: 20px; }
?> .error { background: #f8d7da; color: #721c24; padding: 12px; border-radius: 5px; margin-bottom: 20px; }
<!DOCTYPE html> .form-group { margin-bottom: 15px; }
<html> .form-group label { display: block; margin-bottom: 5px; font-weight: 500; }
<head> .form-group input, .form-group select, .form-group textarea { width: 100%; max-width: 500px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
<title>过滤规则 - 邮件服务器</title> .form-inline { display: flex; gap: 10px; align-items: flex-end; }
<meta charset="UTF-8"> .form-inline .form-group { flex: 1; margin-bottom: 0; }
<style> .btn { padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; text-decoration: none; display: inline-block; }
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; } .btn-primary { background: #007bff; color: white; }
.header { background: #007bff; color: white; padding: 15px; margin: -20px -20px 20px -20px; } .btn-danger { background: #dc3545; color: white; }
.menu { background: white; padding: 10px; margin-bottom: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .btn-success { background: #28a745; color: white; }
.menu a { margin-right: 15px; text-decoration: none; color: #007bff; } .btn-warning { background: #ffc107; color: #000; }
.container { background: white; padding: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } table { width: 100%; border-collapse: collapse; margin-top: 20px; }
.message { background: #d4edda; color: #155724; padding: 12px; border-radius: 5px; margin-bottom: 20px; } th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
.error { background: #f8d7da; color: #721c24; padding: 12px; border-radius: 5px; margin-bottom: 20px; } th { background: #f8f9fa; }
.form-group { margin-bottom: 15px; } .badge { padding: 4px 8px; border-radius: 3px; font-size: 12px; font-weight: 500; }
.form-group label { display: block; margin-bottom: 5px; font-weight: 500; } .badge-email { background: #17a2b8; color: white; }
.form-group input, .form-group select, .form-group textarea { width: 100%; max-width: 500px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; } .badge-ip { background: #6c757d; color: white; }
.form-inline { display: flex; gap: 10px; align-items: flex-end; } .badge-block { background: #dc3545; color: white; }
.form-inline .form-group { flex: 1; margin-bottom: 0; } .badge-allow { background: #28a745; color: white; }
.btn { padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; text-decoration: none; display: inline-block; } .badge-active { background: #28a745; color: white; }
.btn-primary { background: #007bff; color: white; } .badge-inactive { background: #6c757d; color: white; }
.btn-danger { background: #dc3545; color: white; } </style>
.btn-success { background: #28a745; color: white; } </head>
.btn-warning { background: #ffc107; color: #000; } <body>
table { width: 100%; border-collapse: collapse; margin-top: 20px; } <div class="header">
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; } <h1>邮件服务器管理后台</h1>
th { background: #f8f9fa; } <div>欢迎, <?php echo htmlspecialchars($_SESSION['username']); ?>
.badge { padding: 4px 8px; border-radius: 3px; font-size: 12px; font-weight: 500; } (<a href="logout.php" style="color: white;">退出</a>)
.badge-email { background: #17a2b8; color: white; } </div>
.badge-ip { background: #6c757d; color: white; } </div>
.badge-block { background: #dc3545; color: white; }
.badge-allow { background: #28a745; color: white; } <div class="menu">
.badge-active { background: #28a745; color: white; } <a href="index.php">仪表盘</a>
.badge-inactive { background: #6c757d; color: white; } <?php if ($_SESSION['is_admin'] ?? false): ?>
</style> <a href="users.php">用户管理</a>
</head> <?php endif; ?>
<body> <a href="emails.php">邮件管理</a>
<div class="header"> <a href="filters.php">过滤规则</a>
<h1>邮件服务器管理后台</h1> <a href="logs.php">系统日志</a>
<div>欢迎, <?php echo htmlspecialchars($_SESSION['username']); ?> <?php if ($_SESSION['is_admin'] ?? false): ?>
(<a href="logout.php" style="color: white;">退出</a>) <a href="settings.php">系统设置</a>
</div> <?php endif; ?>
</div> </div>
<div class="menu"> <div class="container">
<a href="index.php">仪表盘</a> <h2>过滤规则管理</h2>
<a href="users.php">用户管理</a>
<a href="broadcast.php">群发邮件</a> <?php if ($message): ?>
<a href="filters.php">过滤规则</a> <div class="message"><?php echo $message; ?></div>
<a href="logs.php">系统日志</a> <?php endif; ?>
<a href="services.php">服务管理</a>
<a href="settings.php">系统设置</a> <?php if ($error): ?>
<a href="help.php">帮助</a> <div class="error"><?php echo $error; ?></div>
</div> <?php endif; ?>
<div class="container"> <!-- 创建过滤规则 -->
<h2>过滤规则管理</h2> <h3>创建过滤规则</h3>
<form method="POST" class="form-inline">
<?php if ($message): ?> <div class="form-group">
<div class="message"><?php echo $message; ?></div> <label>规则类型</label>
<?php endif; ?> <select name="rule_type" required>
<option value="email">邮箱过滤</option>
<?php if ($error): ?> <option value="ip">IP地址过滤</option>
<div class="error"><?php echo $error; ?></div> </select>
<?php endif; ?> </div>
<div class="form-group">
<!-- 创建过滤规则 --> <label>规则值</label>
<h3>创建过滤规则</h3> <input type="text" name="rule_value" placeholder="邮箱或IP地址" required>
<form method="POST" class="form-inline"> </div>
<div class="form-group"> <div class="form-group">
<label>规则类型</label> <label>动作</label>
<select name="rule_type" required> <select name="action" required>
<option value="email">邮箱过滤</option> <option value="block">阻止</option>
<option value="ip">IP地址过滤</option> <option value="allow">允许</option>
</select> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>规则值</label> <label>描述</label>
<input type="text" name="rule_value" placeholder="邮箱或IP地址" required> <input type="text" name="description" placeholder="规则描述(可选)">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>动作</label> <button type="submit" name="create_filter" class="btn btn-primary">创建规则</button>
<select name="action" required> </div>
<option value="block">阻止</option> </form>
<option value="allow">允许</option>
</select> <!-- 规则列表 -->
</div> <h3>过滤规则列表 (<?php echo count($rules); ?>)</h3>
<div class="form-group"> <table>
<label>描述</label> <thead>
<input type="text" name="description" placeholder="规则描述(可选)"> <tr>
</div> <th>ID</th>
<div class="form-group"> <th>类型</th>
<button type="submit" name="create_filter" class="btn btn-primary">创建规则</button> <th>规则值</th>
</div> <th>动作</th>
</form> <th>描述</th>
<th>状态</th>
<!-- 规则列表 --> <th>创建时间</th>
<h3>过滤规则列表 (<?php echo count($rules); ?>)</h3> <th>操作</th>
<table> </tr>
<thead> </thead>
<tr> <tbody>
<th>ID</th> <?php if (empty($rules)): ?>
<th>类型</th> <tr>
<th>规则值</th> <td colspan="8" style="text-align: center; padding: 40px;">暂无过滤规则</td>
<th>动作</th> </tr>
<th>描述</th> <?php else: ?>
<th>状态</th> <?php foreach ($rules as $rule): ?>
<th>创建时间</th> <tr>
<th>操作</th> <td><?php echo $rule['id']; ?></td>
</tr> <td>
</thead> <span class="badge badge-<?php echo $rule['rule_type']; ?>">
<tbody> <?php echo $rule['rule_type'] === 'email' ? '邮箱' : 'IP'; ?>
<?php if (empty($rules)): ?> </span>
<tr> </td>
<td colspan="8" style="text-align: center; padding: 40px;">暂无过滤规则</td> <td><?php echo htmlspecialchars($rule['rule_value']); ?></td>
</tr> <td>
<?php else: ?> <span class="badge badge-<?php echo $rule['action']; ?>">
<?php foreach ($rules as $rule): ?> <?php echo $rule['action'] === 'block' ? '阻止' : '允许'; ?>
<tr> </span>
<td><?php echo $rule['id']; ?></td> </td>
<td> <td><?php echo htmlspecialchars($rule['description'] ?? '-'); ?></td>
<span class="badge badge-<?php echo $rule['rule_type']; ?>"> <td>
<?php echo $rule['rule_type'] === 'email' ? '邮箱' : 'IP'; ?> <span class="badge badge-<?php echo $rule['is_active'] ? 'active' : 'inactive'; ?>">
</span> <?php echo $rule['is_active'] ? '激活' : '禁用'; ?>
</td> </span>
<td><?php echo htmlspecialchars($rule['rule_value']); ?></td> </td>
<td> <td><?php echo $rule['created_at']; ?></td>
<span class="badge badge-<?php echo $rule['action']; ?>"> <td>
<?php echo $rule['action'] === 'block' ? '阻止' : '允许'; ?> <a href="?toggle=<?php echo $rule['id']; ?>" class="btn btn-warning">
</span> <?php echo $rule['is_active'] ? '禁用' : '启用'; ?>
</td> </a>
<td><?php echo htmlspecialchars($rule['description'] ?? '-'); ?></td> <a href="?delete=<?php echo $rule['id']; ?>" class="btn btn-danger" onclick="return confirm('确定要删除此规则吗?');">删除</a>
<td> </td>
<span class="badge badge-<?php echo $rule['is_active'] ? 'active' : 'inactive'; ?>"> </tr>
<?php echo $rule['is_active'] ? '激活' : '禁用'; ?> <?php endforeach; ?>
</span> <?php endif; ?>
</td> </tbody>
<td><?php echo $rule['created_at']; ?></td> </table>
<td> </div>
<form method="post" style="display:inline;"> </body>
<input type="hidden" name="toggle_id" value="<?php echo $rule['id']; ?>"> </html>
<button type="submit" class="btn btn-warning">
<?php echo $rule['is_active'] ? '禁用' : '启用'; ?>
</button>
</form>
<a href="?delete=<?php echo $rule['id']; ?>" class="btn btn-danger" onclick="return confirm('确定要删除此规则吗?');">删除</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</body>
</html>

@ -1,186 +1,196 @@
<?php <?php
require_once __DIR__ . '/../config/database.php'; require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../src/storage/Database.php'; require_once __DIR__ . '/../src/storage/Database.php';
session_start(); session_start();
// 身份验证 // 身份验证
if (!isset($_SESSION['user_id'])) { if (!isset($_SESSION['user_id'])) {
header('Location: index.php'); header('Location: index.php');
exit; exit;
} }
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>帮助 - 邮件服务器</title> <title>帮助 - 邮件服务器</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<style> <style>
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; } body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
.header { background: #007bff; color: white; padding: 15px; margin: -20px -20px 20px -20px; } .header { background: #007bff; color: white; padding: 15px; margin: -20px -20px 20px -20px; }
.menu { background: white; padding: 10px; margin-bottom: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .menu { background: white; padding: 10px; margin-bottom: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.menu a { margin-right: 15px; text-decoration: none; color: #007bff; } .menu a { margin-right: 15px; text-decoration: none; color: #007bff; }
.container { background: white; padding: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); max-width: 900px; } .container { background: white; padding: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); max-width: 900px; }
.section { margin-bottom: 30px; } .section { margin-bottom: 30px; }
.section h3 { color: #007bff; border-bottom: 2px solid #007bff; padding-bottom: 10px; } .section h3 { color: #007bff; border-bottom: 2px solid #007bff; padding-bottom: 10px; }
.section h4 { color: #333; margin-top: 20px; } .section h4 { color: #333; margin-top: 20px; }
.code-block { background: #f8f9fa; padding: 15px; border-radius: 5px; border-left: 4px solid #007bff; margin: 10px 0; font-family: monospace; } .code-block { background: #f8f9fa; padding: 15px; border-radius: 5px; border-left: 4px solid #007bff; margin: 10px 0; font-family: monospace; }
ul, ol { line-height: 1.8; } ul, ol { line-height: 1.8; }
.highlight { background: #fff3cd; padding: 2px 4px; border-radius: 3px; } .highlight { background: #fff3cd; padding: 2px 4px; border-radius: 3px; }
</style> </style>
</head> </head>
<body> <body>
<div class="header"> <div class="header">
<h1>邮件服务器管理后台</h1> <h1>邮件服务器管理后台</h1>
<div>欢迎, <?php echo htmlspecialchars($_SESSION['username']); ?> <div>欢迎, <?php echo htmlspecialchars($_SESSION['username']); ?>
(<a href="logout.php" style="color: white;">退出</a>) (<a href="logout.php" style="color: white;">退出</a>)
</div> </div>
</div> </div>
<div class="menu"> <div class="menu">
<a href="index.php">仪表盘</a> <a href="index.php">仪表盘</a>
<a href="users.php">用户管理</a> <?php if ($_SESSION['is_admin'] ?? false): ?>
<a href="broadcast.php">群发邮件</a> <a href="users.php">用户管理</a>
<a href="filters.php">过滤规则</a> <?php endif; ?>
<a href="logs.php">系统日志</a> <a href="emails.php">邮件管理</a>
<a href="services.php">服务管理</a> <a href="filters.php">过滤规则</a>
<a href="settings.php">系统设置</a> <a href="logs.php">系统日志</a>
<a href="help.php">帮助</a> <?php if ($_SESSION['is_admin'] ?? false): ?>
</div> <a href="settings.php">系统设置</a>
<?php endif; ?>
<div class="container"> <a href="help.php">帮助</a>
<h2>使用帮助</h2> </div>
<div class="section"> <div class="container">
<h3>系统概述</h3> <h2>使用帮助</h2>
<p>这是一个基于POP3和SMTP协议的邮件服务器管理系统支持用户注册、邮件收发、系统管理等功能。</p>
</div> <div class="section">
<h3>系统概述</h3>
<div class="section"> <p>这是一个基于POP3和SMTP协议的邮件服务器管理系统支持用户注册、邮件收发、系统管理等功能。</p>
<h3>功能模块</h3> </div>
<h4>1. 用户管理</h4> <div class="section">
<ul> <h3>功能模块</h3>
<li><strong>创建用户:</strong>可以创建新的用户账号,设置密码、管理员权限和激活状态</li>
<li><strong>编辑用户:</strong>可以修改用户密码、权限和状态</li> <h4>1. 用户管理(管理员功能)</h4>
<li><strong>删除用户:</strong>可以删除用户账号(不能删除自己)</li> <ul>
<li><strong>用户列表:</strong>查看所有注册用户及其状态</li> <li><strong>创建用户:</strong>可以创建新的用户账号,设置密码、管理员权限和激活状态</li>
</ul> <li><strong>编辑用户:</strong>可以修改用户密码、权限和状态</li>
<li><strong>删除用户:</strong>可以删除用户账号(不能删除自己)</li>
<li><strong>用户列表:</strong>查看所有注册用户及其状态</li>
<h4>2. 群发邮件</h4> </ul>
<ul>
<li><strong>发送给所有用户:</strong>可以一次性向所有激活用户发送通知邮件</li> <h4>2. 邮件管理</h4>
<li><strong>发送给指定用户:</strong>可以选择特定用户进行群发</li> <ul>
<li><strong>邮件内容:</strong>支持自定义主题和内容</li> <li><strong>查看邮件:</strong>管理员可以查看所有邮件,普通用户只能查看自己的收件箱</li>
</ul> <li><strong>标记已读:</strong>将未读邮件标记为已读</li>
<li><strong>删除邮件:</strong>删除不需要的邮件(软删除)</li>
<h4>3. 过滤规则</h4> <li><strong>邮件详情:</strong>点击邮件主题查看完整内容</li>
<ul> </ul>
<li><strong>邮箱过滤:</strong>可以阻止或允许特定邮箱地址</li>
<li><strong>IP过滤</strong>可以阻止或允许特定IP地址</li> <h4>3. 群发邮件(管理员功能)</h4>
<li><strong>规则管理:</strong>可以启用、禁用或删除过滤规则</li> <ul>
</ul> <li><strong>发送给所有用户:</strong>可以一次性向所有激活用户发送通知邮件</li>
<li><strong>发送给指定用户:</strong>可以选择特定用户进行群发</li>
<h4>4. 系统设置</h4> <li><strong>邮件内容:</strong>支持自定义主题和内容</li>
<ul> </ul>
<li><strong>端口设置:</strong>配置SMTP端口默认25和POP3端口默认110</li>
<li><strong>域名设置:</strong>设置邮件服务器域名默认test.com</li> <h4>4. 过滤规则</h4>
<li><strong>邮箱管理:</strong>设置用户邮箱大小限制</li> <ul>
<li><strong>日志设置:</strong>配置日志存储路径和最大大小</li> <li><strong>邮箱过滤:</strong>可以阻止或允许特定邮箱地址</li>
<li><strong>密码修改:</strong>管理员可以修改自己的密码</li> <li><strong>IP过滤</strong>可以阻止或允许特定IP地址</li>
</ul> <li><strong>规则管理:</strong>可以启用、禁用或删除过滤规则</li>
</ul>
<h4>5. 服务管理</h4>
<ul> <h4>5. 系统设置(管理员功能)</h4>
<li><strong>SMTP服务</strong>查看和管理SMTP服务状态</li> <ul>
<li><strong>POP3服务</strong>查看和管理POP3服务状态</li> <li><strong>端口设置:</strong>配置SMTP端口默认25和POP3端口默认110</li>
<li><strong>服务起停:</strong>启动或停止邮件服务</li> <li><strong>域名设置:</strong>设置邮件服务器域名默认test.com</li>
</ul> <li><strong>邮箱管理:</strong>设置用户邮箱大小限制</li>
<li><strong>日志设置:</strong>配置日志存储路径和最大大小</li>
<h4>6. 日志管理</h4> <li><strong>密码修改:</strong>管理员可以修改自己的密码</li>
<ul> </ul>
<li><strong>查看日志:</strong>查看SMTP和POP3服务器日志</li>
<li><strong>日志过滤:</strong>按类型过滤日志(全部/SMTP/POP3</li> <h4>6. 服务管理(管理员功能)</h4>
<li><strong>清除日志:</strong>管理员可以清除日志记录</li> <ul>
</ul> <li><strong>SMTP服务</strong>查看和管理SMTP服务状态</li>
</div> <li><strong>POP3服务</strong>查看和管理POP3服务状态</li>
<li><strong>服务起停:</strong>启动或停止邮件服务</li>
<div class="section"> </ul>
<h3>启动服务器</h3>
<p>要启动邮件服务器,需要在命令行执行以下命令:</p> <h4>7. 日志管理</h4>
<div class="code-block"> <ul>
# 启动SMTP服务器需要sudo权限<br> <li><strong>查看日志:</strong>查看SMTP和POP3服务器日志</li>
sudo php scripts/start_smtp.php<br><br> <li><strong>日志过滤:</strong>按类型过滤日志(全部/SMTP/POP3</li>
# 启动POP3服务器需要sudo权限<br> <li><strong>清除日志:</strong>管理员可以清除日志记录</li>
sudo php scripts/start_pop3.php </ul>
</div> </div>
<p><span class="highlight">注意:</span>两个服务器需要分别在两个终端运行。</p>
</div> <div class="section">
<h3>启动服务器</h3>
<div class="section"> <p>要启动邮件服务器,需要在命令行执行以下命令:</p>
<h3>测试邮件服务器</h3> <div class="code-block">
<h4>测试SMTP发送邮件</h4> # 启动SMTP服务器需要sudo权限<br>
<div class="code-block"> sudo php scripts/start_smtp.php<br><br>
telnet localhost 25<br> # 启动POP3服务器需要sudo权限<br>
HELO test<br> sudo php scripts/start_pop3.php
MAIL FROM: &lt;user1@test.com&gt;<br> </div>
RCPT TO: &lt;admin@test.com&gt;<br> <p><span class="highlight">注意:</span>两个服务器需要分别在两个终端运行。</p>
DATA<br> </div>
Subject: 测试邮件<br>
From: user1@test.com<br> <div class="section">
To: admin@test.com<br><br> <h3>测试邮件服务器</h3>
这是一封测试邮件!<br> <h4>测试SMTP发送邮件</h4>
.<br> <div class="code-block">
QUIT telnet localhost 25<br>
</div> HELO test<br>
MAIL FROM: &lt;user1@test.com&gt;<br>
<h4>测试POP3接收邮件</h4> RCPT TO: &lt;admin@test.com&gt;<br>
<div class="code-block"> DATA<br>
telnet localhost 110<br> Subject: 测试邮件<br>
USER admin@test.com<br> From: user1@test.com<br>
PASS 123456<br> To: admin@test.com<br><br>
STAT<br> 这是一封测试邮件!<br>
LIST<br> .<br>
RETR 1<br> QUIT
QUIT </div>
</div>
</div> <h4>测试POP3接收邮件</h4>
<div class="code-block">
<div class="section"> telnet localhost 110<br>
<h3>常见问题</h3> USER admin@test.com<br>
PASS 123456<br>
<h4>Q: 端口被占用怎么办?</h4> STAT<br>
<p>A: 检查端口占用情况:</p> LIST<br>
<div class="code-block"> RETR 1<br>
sudo netstat -tlnp | grep 25 # 检查SMTP端口<br> QUIT
sudo netstat -tlnp | grep 110 # 检查POP3端口 </div>
</div> </div>
<h4>Q: 数据库连接失败?</h4> <div class="section">
<p>A: 确保Docker容器正在运行</p> <h3>常见问题</h3>
<div class="code-block">
docker-compose ps<br> <h4>Q: 端口被占用怎么办?</h4>
docker-compose up -d mysql <p>A: 检查端口占用情况:</p>
</div> <div class="code-block">
sudo netstat -tlnp | grep 25 # 检查SMTP端口<br>
<h4>Q: 如何重置数据库?</h4> sudo netstat -tlnp | grep 110 # 检查POP3端口
<p>A: 执行以下命令:</p> </div>
<div class="code-block">
docker-compose down -v<br> <h4>Q: 数据库连接失败?</h4>
docker-compose up -d<br> <p>A: 确保Docker容器正在运行</p>
sleep 15 <div class="code-block">
</div> docker-compose ps<br>
</div> docker-compose up -d mysql
</div>
<div class="section">
<h3>默认账号</h3> <h4>Q: 如何重置数据库?</h4>
<ul> <p>A: 执行以下命令:</p>
<li><strong>管理员:</strong>admin@test.com / 123456</li> <div class="code-block">
<li><strong>普通用户:</strong>user1@test.com / 123456</li> docker-compose down -v<br>
</ul> docker-compose up -d<br>
</div> sleep 15
</div> </div>
</body> </div>
</html>
<div class="section">
<h3>默认账号</h3>
<ul>
<li><strong>管理员:</strong>admin@test.com / 123456</li>
<li><strong>普通用户:</strong>user1@test.com / 123456</li>
</ul>
</div>
</div>
</body>
</html>

@ -1,217 +1,214 @@
<?php <?php
require_once __DIR__ . '/../config/database.php'; require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../src/storage/Database.php'; require_once __DIR__ . '/../src/storage/Database.php';
require_once __DIR__ . '/../src/storage/UserRepository.php'; require_once __DIR__ . '/../src/storage/UserRepository.php';
require_once __DIR__ . '/../src/utils/Security.php'; require_once __DIR__ . '/../src/utils/Security.php';
session_start(); session_start();
// 简单身份验证,检查用户是否已登录。如果未登录($_SESSION['user_id']不存在),则重定向到登录页面。 // 简单身份验证
function requireAuth() { function requireAuth() {
if (!isset($_SESSION['user_id'])) { if (!isset($_SESSION['user_id'])) {
if (basename($_SERVER['PHP_SELF']) !== 'index.php') { header('Location: index.php');
header('Location: index.php'); exit;
exit; }
} }
}
} // 登录检查
if (isset($_POST['login'])) {
// 登录检查 $username = trim($_POST['username'] ?? '');
if (isset($_POST['login'])) { $password = $_POST['password'] ?? '';
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? ''; try {
// 检查登录尝试次数(防止暴力破解)
try { if (!Security::checkLoginAttempts($username)) {
// 检查登录尝试次数(防止暴力破解) $error = "登录失败次数过多请5分钟后再试";
if (!Security::checkLoginAttempts($username)) { } else {
$error = "登录失败次数过多请5分钟后再试"; $userRepo = new UserRepository();
} else { $user = $userRepo->verifyPassword($username, $password);
$userRepo = new UserRepository();
$user = $userRepo->verifyPassword($username, $password); if ($user && $user['is_active']) {
// 登录成功,清除尝试记录
if ($user && $user['is_active'] && $user['is_admin']) { Security::clearLoginAttempts($username);
// 登录成功,清除尝试记录
Security::clearLoginAttempts($username); $_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['user_id'] = $user['id']; $_SESSION['is_admin'] = $user['is_admin'];
$_SESSION['username'] = $user['username']; header('Location: index.php');
$_SESSION['is_admin'] = $user['is_admin']; exit;
header('Location: index.php'); } else {
exit; // 登录失败,记录尝试
}else if($user && !$user['is_active']){ Security::recordLoginAttempt($username);
$error = "用户被禁用"; $error = "用户名或密码错误";
}else if($user && $user['is_active'] && !$user['is_admin']){ }
$error = "没有权限"; }
}else{ } catch (Exception $e) {
// 登录失败,记录尝试 $error = "登录失败: " . $e->getMessage();
Security::recordLoginAttempt($username); }
$error = "用户名或密码错误"; }
}
} // 如果是登录页面
} catch (Exception $e) { if (basename($_SERVER['PHP_SELF']) === 'index.php' && !isset($_SESSION['user_id'])) {
$error = "登录失败: " . $e->getMessage(); ?>
} <!DOCTYPE html>
} <html>
<head>
// 如果是登录页面 <title>邮件服务器管理后台 - 登录</title>
if (basename($_SERVER['PHP_SELF']) === 'index.php' && !isset($_SESSION['user_id'])) { <style>
?> body { font-family: Arial, sans-serif; max-width: 400px; margin: 50px auto; padding: 20px; }
<!DOCTYPE html> .login-box { border: 1px solid #ddd; padding: 20px; border-radius: 5px; }
<html> input { width: 100%; padding: 8px; margin: 5px 0 15px 0; }
<head> button { background: #007bff; color: white; padding: 10px; border: none; width: 100%; }
<meta charset="UTF-8"> .error { color: red; margin-bottom: 15px; }
<meta name="viewport" content="width=device-width, initial-scale=1.0"> </style>
<title>邮件服务器管理后台 - 登录</title> </head>
<style> <body>
body { font-family: Arial, sans-serif; max-width: 400px; margin: 50px auto; padding: 20px; } <div class="login-box">
.login-box { border: 1px solid #ddd; padding: 20px; border-radius: 5px; } <h2>邮件服务器管理后台</h2>
input { width: 100%; padding: 8px; margin: 5px 0 15px 0; } <?php if (isset($error)) echo "<div class='error'>$error</div>"; ?>
button { background: #007bff; color: white; padding: 10px; border: none; width: 100%; } <form method="POST">
.error { color: red; margin-bottom: 15px; } <div>
</style> <label>用户名:</label>
</head> <input type="text" name="username" value="admin@test.com" required>
<body> </div>
<div class="login-box"> <div>
<h2>邮件服务器管理后台</h2> <label>密码:</label>
<?php if (isset($error)) echo "<div class='error'>$error</div>"; ?> <input type="password" name="password" value="123456" required>
<form method="POST"> </div>
<div> <button type="submit" name="login">登录</button>
<label>用户名:</label> </form>
<input type="text" name="username" value="admin@test.com" required> <p style="margin-top: 20px; font-size: 12px; color: #666; text-align: center;">
</div> 还没有账号?<a href="register.php" style="color: #007bff; text-decoration: none;">立即注册</a>
<div> </p>
<label>密码:</label> <p style="margin-top: 10px; font-size: 12px; color: #666;">
<input type="password" name="password" value="123456" required> 测试账号: admin@test.com / 123456<br>
</div> 普通账号: user1@test.com / 123456
<button type="submit" name="login">登录</button> </p>
</form> </div>
<p style="margin-top: 10px; font-size: 12px; color: #666;"> </body>
测试账号: admin@test.com / 123456<br> </html>
普通账号: user1@test.com / 123456 <?php
</p> exit;
</div> }
</body>
</html> requireAuth();
<?php ?>
exit; <!DOCTYPE html>
} <html>
<head>
requireAuth(); <title>邮件服务器管理后台</title>
?> <style>
<!DOCTYPE html> body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }
<html> .header { background: #007bff; color: white; padding: 15px; margin: -20px -20px 20px -20px; }
<head> .menu { background: #f8f9fa; padding: 10px; margin-bottom: 20px; }
<title>邮件服务器管理后台</title> .menu a { margin-right: 15px; text-decoration: none; color: #007bff; }
<style> .stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 20px; }
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; } .stat-box { border: 1px solid #ddd; padding: 15px; text-align: center; }
.header { background: #007bff; color: white; padding: 15px; margin: -20px -20px 20px -20px; } table { width: 100%; border-collapse: collapse; }
.menu { background: #f8f9fa; padding: 10px; margin-bottom: 20px; } th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
.menu a { margin-right: 15px; text-decoration: none; color: #007bff; } th { background: #f8f9fa; }
.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 20px; } </style>
.stat-box { border: 1px solid #ddd; padding: 15px; text-align: center; } </head>
table { width: 100%; border-collapse: collapse; } <body>
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; } <div class="header">
th { background: #f8f9fa; } <h1>邮件服务器管理后台</h1>
</style> <div>欢迎, <?php echo htmlspecialchars($_SESSION['username']); ?>
</head> (<a href="logout.php" style="color: white;">退出</a>)
<body> </div>
<div class="header"> </div>
<h1>邮件服务器管理后台</h1>
<div>欢迎, <?php echo htmlspecialchars($_SESSION['username']); ?> <div class="menu">
(<a href="logout.php" style="color: white;">退出</a>) <a href="index.php">仪表盘</a>
</div> <?php if ($_SESSION['is_admin']) { ?>
</div> <a href="users.php">用户管理</a>
<?php } ?>
<div class="menu"> <a href="emails.php">邮件管理</a>
<a href="index.php">仪表盘</a> <?php if ($_SESSION['is_admin']) { ?>
<?php?> <a href="broadcast.php">群发邮件</a>
<a href="users.php">用户管理</a> <?php } ?>
<?php ?> <a href="filters.php">过滤规则</a>
<a href="broadcast.php">群发邮件</a> <a href="logs.php">系统日志</a>
<?php ?> <?php if ($_SESSION['is_admin']) { ?>
<a href="filters.php">过滤规则</a> <a href="services.php">服务管理</a>
<a href="logs.php">系统日志</a> <a href="settings.php">系统设置</a>
<?php ?> <?php } ?>
<a href="services.php">服务管理</a> <a href="help.php">帮助</a>
<a href="settings.php">系统设置</a> </div>
<?php ?>
<a href="help.php">帮助</a> <div class="stats">
</div> <?php
$db = Database::getInstance();
<div class="stats">
<?php // 统计用户数
$db = Database::getInstance(); $stmt = $db->query("SELECT COUNT(*) as count FROM users");
$userCount = $stmt->fetch()['count'];
// 统计用户数
$stmt = $db->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 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'];
// 统计今日日志
$stmt = $db->query("SELECT COUNT(*) as count FROM server_logs WHERE DATE(created_at) = CURDATE()"); // 统计活跃会话(简化版)
$logCount = $stmt->fetch()['count']; $activeConnections = 0;
?>
// 统计活跃会话(简化版)
$activeConnections = 0; <div class="stat-box">
?> <h3><?php echo $userCount; ?></h3>
<p>注册用户</p>
<div class="stat-box"> </div>
<h3><?php echo $userCount; ?></h3> <div class="stat-box">
<p>注册用户</p> <h3><?php echo $emailCount; ?></h3>
</div> <p>总邮件数</p>
<div class="stat-box"> </div>
<h3><?php echo $emailCount; ?></h3> <div class="stat-box">
<p>总邮件数</p> <h3><?php echo $logCount; ?></h3>
</div> <p>今日日志</p>
<div class="stat-box"> </div>
<h3><?php echo $logCount; ?></h3> <div class="stat-box">
<p>今日日志</p> <h3><?php echo $activeConnections; ?></h3>
</div> <p>活跃连接</p>
<div class="stat-box"> </div>
<h3><?php echo $activeConnections; ?></h3> </div>
<p>活跃连接</p>
</div> <h2>最近邮件</h2>
</div> <table>
<thead>
<h2>最近邮件</h2> <tr>
<table> <th>ID</th>
<thead> <th>发件人</th>
<tr> <th>收件人</th>
<th>ID</th> <th>主题</th>
<th>发件人</th> <th>时间</th>
<th>收件人</th> </tr>
<th>主题</th> </thead>
<th>时间</th> <tbody>
</tr> <?php
</thead> $stmt = $db->query("
<tbody> SELECT e.*,
<?php COALESCE(u1.username, e.sender) as sender_name,
$stmt = $db->query(" COALESCE(u2.username, e.recipient) as recipient_name
SELECT e.*, FROM emails e
COALESCE(u1.username, e.sender) as sender_name, LEFT JOIN users u1 ON e.sender_id = u1.id
COALESCE(u2.username, e.recipient) as recipient_name LEFT JOIN users u2 ON e.recipient_id = u2.id
FROM emails e WHERE e.is_deleted = 0
LEFT JOIN users u1 ON e.sender_id = u1.id ORDER BY e.created_at DESC
LEFT JOIN users u2 ON e.recipient_id = u2.id LIMIT 10
WHERE e.is_deleted = 0 ");
ORDER BY e.created_at DESC
LIMIT 10 while ($email = $stmt->fetch()) {
"); echo "<tr>";
echo "<td>{$email['id']}</td>";
while ($email = $stmt->fetch()) { echo "<td>" . htmlspecialchars($email['sender_name'] ?? '未知') . "</td>";
echo "<tr>"; echo "<td>" . htmlspecialchars($email['recipient_name'] ?? '未知') . "</td>";
echo "<td>{$email['id']}</td>"; echo "<td>" . htmlspecialchars($email['subject'] ?? '(无主题)') . "</td>";
echo "<td>" . htmlspecialchars($email['sender_name'] ?? '未知') . "</td>"; echo "<td>{$email['created_at']}</td>";
echo "<td>" . htmlspecialchars($email['recipient_name'] ?? '未知') . "</td>"; echo "</tr>";
echo "<td>" . htmlspecialchars($email['subject'] ?? '(无主题)') . "</td>"; }
echo "<td>{$email['created_at']}</td>"; ?>
echo "</tr>"; </tbody>
} </table>
?> </body>
</tbody>
</table>
</body>
</html> </html>

@ -1,428 +0,0 @@
<?php
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../src/storage/Database.php';
require_once __DIR__ . '/../src/storage/UserRepository.php';
require_once __DIR__ . '/../src/utils/Security.php';
session_start();
// 简单身份验证,检查用户是否已登录。如果未登录($_SESSION['user_id']不存在),则重定向到登录页面。
function requireAuth() {
if (!isset($_SESSION['user_id'])) {
header('Location: index.php');
exit;
}
}
// 登录检查
if (isset($_POST['login'])) {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
try {
// 检查登录尝试次数(防止暴力破解)
if (!Security::checkLoginAttempts($username)) {
$error = "登录失败次数过多请5分钟后再试";
} else {
$userRepo = new UserRepository();
$user = $userRepo->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'])) {
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>邮件服务器管理后台 - 登录</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.login-container {
width: 100%;
max-width: 420px;
animation: fadeIn 0.5s ease;
}
.login-header {
text-align: center;
margin-bottom: 30px;
}
.login-header h1 {
color: #007bff;
font-size: 28px;
font-weight: 600;
margin-bottom: 8px;
}
.login-header p {
color: #6c757d;
font-size: 14px;
}
.login-card {
background: white;
border-radius: 12px;
box-shadow: 0 8px 30px rgba(0, 123, 255, 0.15);
padding: 40px;
border: 1px solid rgba(0, 123, 255, 0.1);
}
.error-message {
background-color: #f8d7da;
color: #721c24;
padding: 12px;
border-radius: 6px;
margin-bottom: 20px;
border-left: 4px solid #dc3545;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
}
.error-message:before {
content: "⚠";
font-size: 16px;
}
.form-group {
margin-bottom: 24px;
}
.form-group label {
display: block;
color: #495057;
font-weight: 500;
margin-bottom: 8px;
font-size: 14px;
}
.form-control {
width: 100%;
padding: 12px 16px;
border: 2px solid #e9ecef;
border-radius: 8px;
font-size: 15px;
transition: all 0.3s ease;
background-color: #f8f9fa;
}
.form-control:focus {
outline: none;
border-color: #007bff;
background-color: white;
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
}
.form-control:hover {
border-color: #ced4da;
}
.login-btn {
width: 100%;
padding: 14px;
background: linear-gradient(to right, #007bff, #0056b3);
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 10px;
}
.login-btn:hover {
background: linear-gradient(to right, #0069d9, #004085);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.2);
}
.login-btn:active {
transform: translateY(0);
}
.test-accounts {
margin-top: 25px;
padding-top: 20px;
border-top: 1px solid #e9ecef;
text-align: center;
}
.test-accounts p {
color: #6c757d;
font-size: 13px;
line-height: 1.5;
margin-bottom: 5px;
}
.test-accounts strong {
color: #495057;
font-weight: 600;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 480px) {
.login-card {
padding: 30px 25px;
}
.login-header h1 {
font-size: 24px;
}
body {
padding: 15px;
}
}
</style>
</head>
<body>
<div class="login-container">
<div class="login-header">
<h1>邮件服务器管理</h1>
<p>安全登录到管理后台</p>
</div>
<div class="login-card">
<?php if (isset($error)): ?>
<div class="error-message">
<?php echo htmlspecialchars($error); ?>
</div>
<?php endif; ?>
<form method="POST" action="" id="loginForm">
<div class="form-group">
<label for="username">用户名</label>
<input type="text"
id="username"
name="username"
class="form-control"
value="admin@test.com"
required
placeholder="请输入用户名或邮箱">
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password"
id="password"
name="password"
class="form-control"
value="123456"
required
placeholder="请输入密码">
</div>
<button type="submit" name="login" class="login-btn">
登录系统
</button>
</form>
<div class="test-accounts">
<p><strong>测试账号</strong></p>
<p>管理员: admin@test.com / 123456</p>
<p>普通用户: user1@test.com / 123456</p>
</div>
</div>
</div>
<script>
// 简单的表单验证增强
document.getElementById('loginForm').addEventListener('submit', function(e) {
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value;
if (!username) {
e.preventDefault();
alert('请输入用户名');
return false;
}
if (!password) {
e.preventDefault();
alert('请输入密码');
return false;
}
// 登录按钮状态变化
const submitBtn = e.target.querySelector('button[type="submit"]');
submitBtn.disabled = true;
submitBtn.innerHTML = '登录中...';
submitBtn.style.opacity = '0.8';
});
</script>
</body>
</html>
<?php
exit;
}
requireAuth();
?>
<!DOCTYPE html>
<html>
<head>
<title>邮件服务器管理后台</title>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }
.header { background: #007bff; color: white; padding: 15px; margin: -20px -20px 20px -20px; }
.menu { background: #f8f9fa; padding: 10px; margin-bottom: 20px; }
.menu a { margin-right: 15px; text-decoration: none; color: #007bff; }
.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 20px; }
.stat-box { border: 1px solid #ddd; padding: 15px; text-align: center; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background: #f8f9fa; }
</style>
</head>
<body>
<div class="header">
<h1>邮件服务器管理后台</h1>
<div>欢迎, <?php echo htmlspecialchars($_SESSION['username']); ?>
(<a href="logout.php" style="color: white;">退出</a>)
</div>
</div>
<div class="menu">
<a href="index.php">仪表盘</a>
<?php if ($_SESSION['is_admin']) { ?>
<a href="users.php">用户管理</a>
<?php } ?>
<a href="emails.php">邮件管理</a>
<?php if ($_SESSION['is_admin']) { ?>
<a href="broadcast.php">群发邮件</a>
<?php } ?>
<a href="filters.php">过滤规则</a>
<a href="logs.php">系统日志</a>
<?php if ($_SESSION['is_admin']) { ?>
<a href="services.php">服务管理</a>
<a href="settings.php">系统设置</a>
<?php } ?>
<a href="help.php">帮助</a>
</div>
<div class="stats">
<?php
$db = Database::getInstance();
// 统计用户数
$stmt = $db->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;
?>
<div class="stat-box">
<h3><?php echo $userCount; ?></h3>
<p>注册用户</p>
</div>
<div class="stat-box">
<h3><?php echo $emailCount; ?></h3>
<p>总邮件数</p>
</div>
<div class="stat-box">
<h3><?php echo $logCount; ?></h3>
<p>今日日志</p>
</div>
<div class="stat-box">
<h3><?php echo $activeConnections; ?></h3>
<p>活跃连接</p>
</div>
</div>
<h2>最近邮件</h2>
<table>
<thead>
<tr>
<th>ID</th>
<th>发件人</th>
<th>收件人</th>
<th>主题</th>
<th>时间</th>
</tr>
</thead>
<tbody>
<?php
$stmt = $db->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 "<tr>";
echo "<td>{$email['id']}</td>";
echo "<td>" . htmlspecialchars($email['sender_name'] ?? '未知') . "</td>";
echo "<td>" . htmlspecialchars($email['recipient_name'] ?? '未知') . "</td>";
echo "<td>" . htmlspecialchars($email['subject'] ?? '(无主题)') . "</td>";
echo "<td>{$email['created_at']}</td>";
echo "</tr>";
}
?>
</tbody>
</table>
</body>
</html>

@ -1,18 +1,18 @@
<?php <?php
session_start(); session_start();
// 清除所有会话数据 // 清除所有会话数据
$_SESSION = array(); $_SESSION = array();
// 销毁会话cookie // 销毁会话cookie
if (isset($_COOKIE[session_name()])) { if (isset($_COOKIE[session_name()])) {
setcookie(session_name(), '', time()-3600, '/'); setcookie(session_name(), '', time()-3600, '/');
} }
// 销毁会话 // 销毁会话
session_destroy(); session_destroy();
// 重定向到登录页面 // 重定向到登录页面
header('Location: index.php'); header('Location: index.php');
exit; exit;

@ -1,257 +1,256 @@
<?php <?php
require_once __DIR__ . '/../config/database.php'; require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../src/storage/Database.php'; require_once __DIR__ . '/../src/storage/Database.php';
require_once __DIR__ . '/../src/storage/SystemSettingsRepository.php'; require_once __DIR__ . '/../src/storage/SystemSettingsRepository.php';
//
error_reporting(E_ALL); session_start();
ini_set('display_errors', 1);
// 身份验证
session_start(); if (!isset($_SESSION['user_id'])) {
header('Location: index.php');
// 身份验证 exit;
if (!isset($_SESSION['user_id'])) { }
header('Location: index.php');
exit; $db = Database::getInstance();
} $settingsRepo = new SystemSettingsRepository();
$message = '';
$db = Database::getInstance(); $error = '';
$settingsRepo = new SystemSettingsRepository();
$message = ''; // 处理清除日志
$error = ''; if (isset($_GET['clear_logs'])) {
$logType = $_GET['clear_logs'] ?? '';
// 处理清除日志
if (isset($_GET['clear_logs'])) { if ($logType === 'all') {
$logType = $_GET['clear_logs'] ?? ''; $stmt = $db->prepare("DELETE FROM server_logs");
$stmt->execute();
if ($logType === 'all') { $message = "所有日志已清除";
$stmt = $db->prepare("DELETE FROM server_logs"); } elseif ($logType === 'smtp') {
$stmt->execute(); $stmt = $db->prepare("DELETE FROM server_logs WHERE log_type = 'SMTP'");
$message = "所有日志已清除"; $stmt->execute();
} elseif ($logType === 'smtp') { $message = "SMTP日志已清除";
$stmt = $db->prepare("DELETE FROM server_logs WHERE log_type = 'SMTP'"); } elseif ($logType === 'pop3') {
$stmt->execute(); $stmt = $db->prepare("DELETE FROM server_logs WHERE log_type = 'POP3'");
$message = "SMTP日志已清除"; $stmt->execute();
} elseif ($logType === 'pop3') { $message = "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"); $stmt = $db->query("SELECT COUNT(*) as count FROM server_logs WHERE log_type = 'SMTP'");
$totalLogs = $stmt->fetch()['count']; $smtpLogs = $stmt->fetch()['count'];
$stmt = $db->query("SELECT COUNT(*) as count FROM server_logs WHERE log_type = 'SMTP'"); $stmt = $db->query("SELECT COUNT(*) as count FROM server_logs WHERE log_type = 'POP3'");
$smtpLogs = $stmt->fetch()['count']; $pop3Logs = $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;
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$perPage = 50; // 过滤参数
$offset = ($page - 1) * $perPage; $filterType = $_GET['type'] ?? 'all';
// 过滤参数 // 获取日志列表
$filterType = $_GET['type'] ?? 'all'; if ($filterType === 'smtp') {
$stmt = $db->prepare("
// 获取日志列表 SELECT l.*, u.username
if ($filterType === 'smtp') { FROM server_logs l
$stmt = $db->prepare(" LEFT JOIN users u ON l.user_id = u.id
SELECT l.*, u.username WHERE l.log_type = 'SMTP'
FROM server_logs l ORDER BY l.created_at DESC
LEFT JOIN users u ON l.user_id = u.id LIMIT ? OFFSET ?
WHERE l.log_type = 'SMTP' ");
ORDER BY l.created_at DESC $stmt->execute([$perPage, $offset]);
LIMIT ? OFFSET ?
"); $countStmt = $db->prepare("SELECT COUNT(*) as count FROM server_logs WHERE log_type = 'SMTP'");
$stmt->execute([$perPage, $offset]); $countStmt->execute();
$totalLogs = $countStmt->fetch()['count'];
$countStmt = $db->prepare("SELECT COUNT(*) as count FROM server_logs WHERE log_type = 'SMTP'"); } elseif ($filterType === 'pop3') {
$countStmt->execute(); $stmt = $db->prepare("
$totalLogs = $countStmt->fetch()['count']; SELECT l.*, u.username
} elseif ($filterType === 'pop3') { FROM server_logs l
$stmt = $db->prepare(" LEFT JOIN users u ON l.user_id = u.id
SELECT l.*, u.username WHERE l.log_type = 'POP3'
FROM server_logs l ORDER BY l.created_at DESC
LEFT JOIN users u ON l.user_id = u.id LIMIT ? OFFSET ?
WHERE l.log_type = 'POP3' ");
ORDER BY l.created_at DESC $stmt->execute([$perPage, $offset]);
LIMIT ? OFFSET ?
"); $countStmt = $db->prepare("SELECT COUNT(*) as count FROM server_logs WHERE log_type = 'POP3'");
$stmt->execute([$perPage, $offset]); $countStmt->execute();
$totalLogs = $countStmt->fetch()['count'];
$countStmt = $db->prepare("SELECT COUNT(*) as count FROM server_logs WHERE log_type = 'POP3'"); } else {
$countStmt->execute(); $stmt = $db->prepare("
$totalLogs = $countStmt->fetch()['count']; SELECT l.*, u.username
} else { FROM server_logs l
$stmt = $db->prepare(" LEFT JOIN users u ON l.user_id = u.id
SELECT l.*, u.username ORDER BY l.created_at DESC
FROM server_logs l LIMIT ? OFFSET ?
LEFT JOIN users u ON l.user_id = u.id ");
ORDER BY l.created_at DESC $stmt->execute([$perPage, $offset]);
LIMIT ? OFFSET ? }
");
$stmt->execute([$perPage, $offset]); $logs = $stmt->fetchAll();
} $totalPages = ceil($totalLogs / $perPage);
$logs = $stmt->fetchAll(); // 获取日志设置
$totalPages = ceil($totalLogs / $perPage); $logPath = $settingsRepo->get('log_path', '/var/log/mailserver');
$logMaxSize = $settingsRepo->get('log_max_size', 10485760);
// 获取日志设置 ?>
$logPath = $settingsRepo->get('log_path', '/var/log/mailserver'); <!DOCTYPE html>
$logMaxSize = $settingsRepo->get('log_max_size', 10485760); <html>
?> <head>
<!DOCTYPE html> <title>系统日志 - 邮件服务器</title>
<html> <meta charset="UTF-8">
<head> <style>
<title>系统日志 - 邮件服务器</title> body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
<meta charset="UTF-8"> .header { background: #007bff; color: white; padding: 15px; margin: -20px -20px 20px -20px; }
<style> .menu { background: white; padding: 10px; margin-bottom: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; } .menu a { margin-right: 15px; text-decoration: none; color: #007bff; }
.header { background: #007bff; color: white; padding: 15px; margin: -20px -20px 20px -20px; } .container { background: white; padding: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.menu { background: white; padding: 10px; margin-bottom: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .message { background: #d4edda; color: #155724; padding: 12px; border-radius: 5px; margin-bottom: 20px; }
.menu a { margin-right: 15px; text-decoration: none; color: #007bff; } .error { background: #f8d7da; color: #721c24; padding: 12px; border-radius: 5px; margin-bottom: 20px; }
.container { background: white; padding: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin-bottom: 20px; }
.message { background: #d4edda; color: #155724; padding: 12px; border-radius: 5px; margin-bottom: 20px; } .stat-box { border: 1px solid #ddd; padding: 15px; text-align: center; border-radius: 5px; }
.error { background: #f8d7da; color: #721c24; padding: 12px; border-radius: 5px; margin-bottom: 20px; } .stat-box h3 { margin: 0; font-size: 24px; color: #007bff; }
.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin-bottom: 20px; } .stat-box p { margin: 5px 0 0 0; color: #666; }
.stat-box { border: 1px solid #ddd; padding: 15px; text-align: center; border-radius: 5px; } .filters { margin-bottom: 20px; }
.stat-box h3 { margin: 0; font-size: 24px; color: #007bff; } .filters a { display: inline-block; padding: 8px 16px; margin-right: 10px; text-decoration: none; border: 1px solid #ddd; border-radius: 4px; }
.stat-box p { margin: 5px 0 0 0; color: #666; } .filters a.active { background: #007bff; color: white; border-color: #007bff; }
.filters { margin-bottom: 20px; } table { width: 100%; border-collapse: collapse; margin-top: 20px; }
.filters a { display: inline-block; padding: 8px 16px; margin-right: 10px; text-decoration: none; border: 1px solid #ddd; border-radius: 4px; } th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
.filters a.active { background: #007bff; color: white; border-color: #007bff; } th { background: #f8f9fa; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; } .btn { padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; text-decoration: none; display: inline-block; }
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; } .btn-danger { background: #dc3545; color: white; }
th { background: #f8f9fa; } .pagination { margin-top: 20px; text-align: center; }
.btn { padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; text-decoration: none; display: inline-block; } .pagination a { display: inline-block; padding: 8px 12px; margin: 0 4px; text-decoration: none; border: 1px solid #ddd; border-radius: 4px; }
.btn-danger { background: #dc3545; color: white; } .pagination a:hover { background: #f8f9fa; }
.pagination { margin-top: 20px; text-align: center; } .pagination .current { background: #007bff; color: white; border-color: #007bff; }
.pagination a { display: inline-block; padding: 8px 12px; margin: 0 4px; text-decoration: none; border: 1px solid #ddd; border-radius: 4px; } .log-info { background: #f8f9fa; padding: 15px; border-radius: 5px; margin-bottom: 20px; }
.pagination a:hover { background: #f8f9fa; } </style>
.pagination .current { background: #007bff; color: white; border-color: #007bff; } </head>
.log-info { background: #f8f9fa; padding: 15px; border-radius: 5px; margin-bottom: 20px; } <body>
</style> <div class="header">
</head> <h1>邮件服务器管理后台</h1>
<body> <div>欢迎, <?php echo htmlspecialchars($_SESSION['username']); ?>
<div class="header"> (<a href="logout.php" style="color: white;">退出</a>)
<h1>邮件服务器管理后台</h1> </div>
<div>欢迎, <?php echo htmlspecialchars($_SESSION['username']); ?> </div>
(<a href="logout.php" style="color: white;">退出</a>)
</div> <div class="menu">
</div> <a href="index.php">仪表盘</a>
<?php if ($_SESSION['is_admin'] ?? false): ?>
<div class="menu"> <a href="users.php">用户管理</a>
<a href="index.php">仪表盘</a> <?php endif; ?>
<a href="users.php">用户管理</a> <a href="emails.php">邮件管理</a>
<a href="broadcast.php">群发邮件</a> <a href="filters.php">过滤规则</a>
<a href="filters.php">过滤规则</a> <a href="logs.php">系统日志</a>
<a href="logs.php">系统日志</a> <?php if ($_SESSION['is_admin'] ?? false): ?>
<a href="services.php">服务管理</a> <a href="settings.php">系统设置</a>
<a href="settings.php">系统设置</a> <?php endif; ?>
<a href="help.php">帮助</a> </div>
</div>
<div class="container">
<div class="container"> <h2>系统日志管理</h2>
<h2>系统日志管理</h2>
<?php if ($message): ?>
<?php if ($message): ?> <div class="message"><?php echo $message; ?></div>
<div class="message"><?php echo $message; ?></div> <?php endif; ?>
<?php endif; ?>
<?php if ($error): ?>
<?php if ($error): ?> <div class="error"><?php echo $error; ?></div>
<div class="error"><?php echo $error; ?></div> <?php endif; ?>
<?php endif; ?>
<!-- 日志统计 -->
<!-- 日志统计 --> <div class="stats">
<div class="stats"> <div class="stat-box">
<div class="stat-box"> <h3><?php echo $totalLogs; ?></h3>
<h3><?php echo $totalLogs; ?></h3> <p>总日志数</p>
<p>总日志数</p> </div>
</div> <div class="stat-box">
<div class="stat-box"> <h3><?php echo $smtpLogs; ?></h3>
<h3><?php echo $smtpLogs; ?></h3> <p>SMTP日志</p>
<p>SMTP日志</p> </div>
</div> <div class="stat-box">
<div class="stat-box"> <h3><?php echo $pop3Logs; ?></h3>
<h3><?php echo $pop3Logs; ?></h3> <p>POP3日志</p>
<p>POP3日志</p> </div>
</div> </div>
</div>
<!-- 日志信息 -->
<!-- 日志信息 --> <div class="log-info">
<div class="log-info"> <strong>日志存储路径:</strong><?php echo htmlspecialchars($logPath); ?><br>
<strong>日志存储路径:</strong><?php echo htmlspecialchars($logPath); ?><br> <strong>日志文件最大大小:</strong><?php echo round($logMaxSize / 1048576, 2); ?> MB
<strong>日志文件最大大小:</strong><?php echo round($logMaxSize / 1048576, 2); ?> MB </div>
</div>
<!-- 过滤和操作 -->
<!-- 过滤和操作 --> <div class="filters">
<div class="filters"> <a href="?type=all" class="<?php echo $filterType === 'all' ? 'active' : ''; ?>">全部</a>
<a href="?type=all" class="<?php echo $filterType === 'all' ? 'active' : ''; ?>">全部</a> <a href="?type=smtp" class="<?php echo $filterType === 'smtp' ? 'active' : ''; ?>">SMTP日志</a>
<a href="?type=smtp" class="<?php echo $filterType === 'smtp' ? 'active' : ''; ?>">SMTP日志</a> <a href="?type=pop3" class="<?php echo $filterType === 'pop3' ? 'active' : ''; ?>">POP3日志</a>
<a href="?type=pop3" class="<?php echo $filterType === 'pop3' ? 'active' : ''; ?>">POP3日志</a> <?php if ($_SESSION['is_admin'] ?? false): ?>
<?php if ($_SESSION['is_admin'] ?? false): ?> <a href="?clear_logs=smtp" class="btn btn-danger" onclick="return confirm('确定要清除SMTP日志吗');">清除SMTP日志</a>
<a href="?clear_logs=smtp" class="btn btn-danger" onclick="return confirm('确定要清除SMTP日志吗');">清除SMTP日志</a> <a href="?clear_logs=pop3" class="btn btn-danger" onclick="return confirm('确定要清除POP3日志吗');">清除POP3日志</a>
<a href="?clear_logs=pop3" class="btn btn-danger" onclick="return confirm('确定要清除POP3日志吗');">清除POP3日志</a> <a href="?clear_logs=all" class="btn btn-danger" onclick="return confirm('确定要清除所有日志吗?');">清除所有日志</a>
<a href="?clear_logs=all" class="btn btn-danger" onclick="return confirm('确定要清除所有日志吗?');">清除所有日志</a> <?php endif; ?>
<?php endif; ?> </div>
</div>
<!-- 日志列表 -->
<!-- 日志列表 --> <table>
<table> <thead>
<thead> <tr>
<tr> <th>ID</th>
<th>ID</th> <th>类型</th>
<th>类型</th> <th>消息</th>
<th>消息</th> <th>用户</th>
<th>用户</th> <th>IP地址</th>
<th>IP地址</th> <th>时间</th>
<th>时间</th> </tr>
</tr> </thead>
</thead> <tbody>
<tbody> <?php if (empty($logs)): ?>
<?php if (empty($logs)): ?> <tr>
<tr> <td colspan="6" style="text-align: center; padding: 40px;">暂无日志</td>
<td colspan="6" style="text-align: center; padding: 40px;">暂无日志</td> </tr>
</tr> <?php else: ?>
<?php else: ?> <?php foreach ($logs as $log): ?>
<?php foreach ($logs as $log): ?> <tr>
<tr> <td><?php echo $log['id']; ?></td>
<td><?php echo $log['id']; ?></td> <td><?php echo htmlspecialchars($log['log_type'] ?? '-'); ?></td>
<td><?php echo htmlspecialchars($log['log_type'] ?? '-'); ?></td> <td><?php echo htmlspecialchars($log['message'] ?? '-'); ?></td>
<td><?php echo htmlspecialchars($log['message'] ?? '-'); ?></td> <td><?php echo htmlspecialchars($log['username'] ?? '-'); ?></td>
<td><?php echo htmlspecialchars($log['username'] ?? '-'); ?></td> <td><?php echo htmlspecialchars($log['client_ip'] ?? '-'); ?></td>
<td><?php echo htmlspecialchars($log['client_ip'] ?? '-'); ?></td> <td><?php echo $log['created_at']; ?></td>
<td><?php echo $log['created_at']; ?></td> </tr>
</tr> <?php endforeach; ?>
<?php endforeach; ?> <?php endif; ?>
<?php endif; ?> </tbody>
</tbody> </table>
</table>
<!-- 分页 -->
<!-- 分页 --> <?php if ($totalPages > 1): ?>
<?php if ($totalPages > 1): ?> <div class="pagination">
<div class="pagination"> <?php if ($page > 1): ?>
<?php if ($page > 1): ?> <a href="?page=<?php echo $page - 1; ?>&type=<?php echo $filterType; ?>">上一页</a>
<a href="?page=<?php echo $page - 1; ?>&type=<?php echo $filterType; ?>">上一页</a> <?php endif; ?>
<?php endif; ?>
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
<?php for ($i = 1; $i <= $totalPages; $i++): ?> <?php if ($i == $page): ?>
<?php if ($i == $page): ?> <span class="current"><?php echo $i; ?></span>
<span class="current"><?php echo $i; ?></span> <?php else: ?>
<?php else: ?> <a href="?page=<?php echo $i; ?>&type=<?php echo $filterType; ?>"><?php echo $i; ?></a>
<a href="?page=<?php echo $i; ?>&type=<?php echo $filterType; ?>"><?php echo $i; ?></a> <?php endif; ?>
<?php endif; ?> <?php endfor; ?>
<?php endfor; ?>
<?php if ($page < $totalPages): ?>
<?php if ($page < $totalPages): ?> <a href="?page=<?php echo $page + 1; ?>&type=<?php echo $filterType; ?>">下一页</a>
<a href="?page=<?php echo $page + 1; ?>&type=<?php echo $filterType; ?>">下一页</a> <?php endif; ?>
<?php endif; ?> </div>
</div> <?php endif; ?>
<?php endif; ?> </div>
</div> </body>
</body> </html>
</html>

@ -1,288 +1,288 @@
<?php <?php
require_once __DIR__ . '/../config/database.php'; require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../src/storage/Database.php'; require_once __DIR__ . '/../src/storage/Database.php';
require_once __DIR__ . '/../src/storage/UserRepository.php'; require_once __DIR__ . '/../src/storage/UserRepository.php';
require_once __DIR__ . '/../src/utils/Validator.php'; require_once __DIR__ . '/../src/utils/Validator.php';
require_once __DIR__ . '/../src/utils/Security.php'; require_once __DIR__ . '/../src/utils/Security.php';
session_start(); session_start();
$error = ''; $error = '';
$success = ''; $success = '';
// 处理注册请求 // 处理注册请求
if (isset($_POST['register'])) { if (isset($_POST['register'])) {
$username = trim($_POST['username'] ?? ''); $username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? ''; $password = $_POST['password'] ?? '';
$confirmPassword = $_POST['confirm_password'] ?? ''; $confirmPassword = $_POST['confirm_password'] ?? '';
// 验证CSRF令牌 // 验证CSRF令牌
if (!Security::verifyCSRFToken($_POST['csrf_token'] ?? '')) { if (!Security::verifyCSRFToken($_POST['csrf_token'] ?? '')) {
$error = "安全验证失败,请重试"; $error = "安全验证失败,请重试";
} else { } else {
// 验证输入 // 验证输入
$usernameValidation = Validator::validateUsername($username); $usernameValidation = Validator::validateUsername($username);
if (!$usernameValidation['valid']) { if (!$usernameValidation['valid']) {
$error = implode('<br>', $usernameValidation['errors']); $error = implode('<br>', $usernameValidation['errors']);
} else { } else {
// 验证邮箱域名默认test.com // 验证邮箱域名默认test.com
$domain = 'test.com'; $domain = 'test.com';
if (!Validator::validateEmailDomain($username, $domain)) { if (!Validator::validateEmailDomain($username, $domain)) {
$error = "邮箱域名必须是 @{$domain}"; $error = "邮箱域名必须是 @{$domain}";
} else { } else {
// 验证密码 // 验证密码
$passwordValidation = Validator::validatePassword($password, 6); $passwordValidation = Validator::validatePassword($password, 6);
if (!$passwordValidation['valid']) { if (!$passwordValidation['valid']) {
$error = implode('<br>', $passwordValidation['errors']); $error = implode('<br>', $passwordValidation['errors']);
} else { } else {
// 验证密码确认 // 验证密码确认
$matchValidation = Validator::validatePasswordMatch($password, $confirmPassword); $matchValidation = Validator::validatePasswordMatch($password, $confirmPassword);
if (!$matchValidation['valid']) { if (!$matchValidation['valid']) {
$error = implode('<br>', $matchValidation['errors']); $error = implode('<br>', $matchValidation['errors']);
} else { } else {
// 尝试创建用户 // 尝试创建用户
try { try {
$userRepo = new UserRepository(); $userRepo = new UserRepository();
// 检查用户名是否已存在 // 检查用户名是否已存在
if ($userRepo->usernameExists($username)) { if ($userRepo->usernameExists($username)) {
$error = "该邮箱已被注册"; $error = "该邮箱已被注册";
} else { } else {
// 创建新用户(默认非管理员,激活状态) // 创建新用户(默认非管理员,激活状态)
$user = $userRepo->create($username, $password, false, true); $user = $userRepo->create($username, $password, false, true);
if ($user) { if ($user) {
$success = "注册成功!请使用您的账号登录。"; $success = "注册成功!请使用您的账号登录。";
// 3秒后跳转到登录页面 // 3秒后跳转到登录页面
header("Refresh: 3; url=index.php"); header("Refresh: 3; url=index.php");
} else { } else {
$error = "注册失败,请稍后重试"; $error = "注册失败,请稍后重试";
} }
} }
} catch (Exception $e) { } catch (Exception $e) {
$error = "注册失败: " . $e->getMessage(); $error = "注册失败: " . $e->getMessage();
} }
} }
} }
} }
} }
} }
} }
// 生成CSRF令牌 // 生成CSRF令牌
$csrfToken = Security::generateCSRFToken(); $csrfToken = Security::generateCSRFToken();
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>用户注册 - 邮件服务器</title> <title>用户注册 - 邮件服务器</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<style> <style>
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
} }
body { body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 20px; padding: 20px;
} }
.register-container { .register-container {
background: white; background: white;
border-radius: 10px; border-radius: 10px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2); box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
padding: 40px; padding: 40px;
width: 100%; width: 100%;
max-width: 450px; max-width: 450px;
} }
h1 { h1 {
color: #333; color: #333;
margin-bottom: 10px; margin-bottom: 10px;
text-align: center; text-align: center;
} }
.subtitle { .subtitle {
color: #666; color: #666;
text-align: center; text-align: center;
margin-bottom: 30px; margin-bottom: 30px;
font-size: 14px; font-size: 14px;
} }
.form-group { .form-group {
margin-bottom: 20px; margin-bottom: 20px;
} }
label { label {
display: block; display: block;
margin-bottom: 8px; margin-bottom: 8px;
color: #333; color: #333;
font-weight: 500; font-weight: 500;
font-size: 14px; font-size: 14px;
} }
input[type="text"], input[type="text"],
input[type="email"], input[type="email"],
input[type="password"] { input[type="password"] {
width: 100%; width: 100%;
padding: 12px; padding: 12px;
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 5px; border-radius: 5px;
font-size: 14px; font-size: 14px;
transition: border-color 0.3s; transition: border-color 0.3s;
} }
input[type="text"]:focus, input[type="text"]:focus,
input[type="email"]:focus, input[type="email"]:focus,
input[type="password"]:focus { input[type="password"]:focus {
outline: none; outline: none;
border-color: #667eea; border-color: #667eea;
} }
.help-text { .help-text {
font-size: 12px; font-size: 12px;
color: #999; color: #999;
margin-top: 5px; margin-top: 5px;
} }
.error { .error {
background: #fee; background: #fee;
color: #c33; color: #c33;
padding: 12px; padding: 12px;
border-radius: 5px; border-radius: 5px;
margin-bottom: 20px; margin-bottom: 20px;
font-size: 14px; font-size: 14px;
border-left: 4px solid #c33; border-left: 4px solid #c33;
} }
.success { .success {
background: #efe; background: #efe;
color: #3c3; color: #3c3;
padding: 12px; padding: 12px;
border-radius: 5px; border-radius: 5px;
margin-bottom: 20px; margin-bottom: 20px;
font-size: 14px; font-size: 14px;
border-left: 4px solid #3c3; border-left: 4px solid #3c3;
} }
button[type="submit"] { button[type="submit"] {
width: 100%; width: 100%;
padding: 12px; padding: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; color: white;
border: none; border: none;
border-radius: 5px; border-radius: 5px;
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s; transition: transform 0.2s, box-shadow 0.2s;
} }
button[type="submit"]:hover { button[type="submit"]:hover {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
} }
button[type="submit"]:active { button[type="submit"]:active {
transform: translateY(0); transform: translateY(0);
} }
.login-link { .login-link {
text-align: center; text-align: center;
margin-top: 20px; margin-top: 20px;
font-size: 14px; font-size: 14px;
color: #666; color: #666;
} }
.login-link a { .login-link a {
color: #667eea; color: #667eea;
text-decoration: none; text-decoration: none;
font-weight: 500; font-weight: 500;
} }
.login-link a:hover { .login-link a:hover {
text-decoration: underline; text-decoration: underline;
} }
.domain-hint { .domain-hint {
display: inline-block; display: inline-block;
background: #f0f0f0; background: #f0f0f0;
padding: 2px 6px; padding: 2px 6px;
border-radius: 3px; border-radius: 3px;
font-family: monospace; font-family: monospace;
font-size: 12px; font-size: 12px;
} }
</style> </style>
</head> </head>
<body> <body>
<div class="register-container"> <div class="register-container">
<h1>用户注册</h1> <h1>用户注册</h1>
<p class="subtitle">创建您的邮件服务器账号</p> <p class="subtitle">创建您的邮件服务器账号</p>
<?php if ($error): ?> <?php if ($error): ?>
<div class="error"><?php echo $error; ?></div> <div class="error"><?php echo $error; ?></div>
<?php endif; ?> <?php endif; ?>
<?php if ($success): ?> <?php if ($success): ?>
<div class="success"><?php echo $success; ?></div> <div class="success"><?php echo $success; ?></div>
<?php else: ?> <?php else: ?>
<form method="POST" action=""> <form method="POST" action="">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>"> <input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
<div class="form-group"> <div class="form-group">
<label for="username">邮箱地址</label> <label for="username">邮箱地址</label>
<input <input
type="email" type="email"
id="username" id="username"
name="username" name="username"
value="<?php echo htmlspecialchars($_POST['username'] ?? ''); ?>" value="<?php echo htmlspecialchars($_POST['username'] ?? ''); ?>"
placeholder="example@test.com" placeholder="example@test.com"
required required
autofocus autofocus
> >
<div class="help-text">请输入您的邮箱地址(域名必须是 <span class="domain-hint">@test.com</span></div> <div class="help-text">请输入您的邮箱地址(域名必须是 <span class="domain-hint">@test.com</span></div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="password">密码</label> <label for="password">密码</label>
<input <input
type="password" type="password"
id="password" id="password"
name="password" name="password"
placeholder="至少6个字符" placeholder="至少6个字符"
required required
minlength="6" minlength="6"
> >
<div class="help-text">密码长度至少需要6个字符</div> <div class="help-text">密码长度至少需要6个字符</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="confirm_password">确认密码</label> <label for="confirm_password">确认密码</label>
<input <input
type="password" type="password"
id="confirm_password" id="confirm_password"
name="confirm_password" name="confirm_password"
placeholder="请再次输入密码" placeholder="请再次输入密码"
required required
minlength="6" minlength="6"
> >
</div> </div>
<button type="submit" name="register">注册</button> <button type="submit" name="register">注册</button>
</form> </form>
<?php endif; ?> <?php endif; ?>
<div class="login-link"> <div class="login-link">
已有账号?<a href="index.php">立即登录</a> 已有账号?<a href="index.php">立即登录</a>
</div> </div>
</div> </div>
</body> </body>
</html> </html>

@ -1,361 +1,166 @@
<?php <?php
require_once __DIR__ . '/../config/database.php'; require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../src/storage/Database.php'; require_once __DIR__ . '/../src/storage/Database.php';
require_once __DIR__ . '/../src/storage/ServiceRepository.php'; require_once __DIR__ . '/../src/storage/ServiceRepository.php';
require_once __DIR__ . '/../src/storage/SystemSettingsRepository.php'; require_once __DIR__ . '/../src/storage/SystemSettingsRepository.php';
//错误提示 session_start();
error_reporting(E_ALL);
ini_set('display_errors', 1); // 身份验证
if (!isset($_SESSION['user_id'])) {
session_start(); header('Location: index.php');
exit;
// 登录验证 }
if (!isset($_SESSION['user_id'])) {
header('Location: index.php'); // 检查管理员权限
exit; if (!$_SESSION['is_admin']) {
} die('权限不足:只有管理员可以访问此页面');
}
$serviceRepo = new ServiceRepository();
$settingsRepo = new SystemSettingsRepository(); $serviceRepo = new ServiceRepository();
$message = ''; $settingsRepo = new SystemSettingsRepository();
$error = ''; $message = '';
$error = '';
// 获取端口设置(放在函数定义之前)
$smtpPort = $settingsRepo->get('smtp_port', 25); // 处理服务起停
$pop3Port = $settingsRepo->get('pop3_port', 110); if (isset($_GET['action'])) {
$serviceName = $_GET['service'] ?? '';
function startService($serviceName, $port) { $action = $_GET['action'] ?? '';
$scriptPath = __DIR__ . "/../scripts/start_{$serviceName}.php";
$logFile = __DIR__ . "/../logs/{$serviceName}.log"; if ($serviceName === 'smtp' || $serviceName === 'pop3') {
if ($action === 'start') {
error_log("尝试启动服务: $serviceName , 端口: $port"); // 启动服务(实际应该通过系统服务管理,这里只是更新状态)
$pid = null; // 实际应该获取进程ID
if (!file_exists($scriptPath)) { $serviceRepo->updateStatus($serviceName, true, $pid);
return ['success' => false, 'message' => '启动脚本不存在']; $message = strtoupper($serviceName) . "服务已启动";
} } elseif ($action === 'stop') {
// 停止服务
$status = $serviceRepo->getStatus($serviceName);
if (isServiceRunning($serviceName, $port)) { if ($status && $status['pid']) {
return ['success' => false, 'message' => '服务已在运行']; // 尝试终止进程
} @exec("kill {$status['pid']} 2>/dev/null");
}
@file_put_contents($logFile, ''); $serviceRepo->updateStatus($serviceName, false, null);
//===== 修复:更稳健的命令执行 ===== $message = strtoupper($serviceName) . "服务已停止";
$raw = shell_exec(sprintf( }
'cd %s && (%s > %s 2>/dev/null </dev/null & echo $!)', }
escapeshellarg(dirname($scriptPath, 2)), }
'nohup php ' . escapeshellarg($scriptPath),
escapeshellarg($logFile) // 获取服务状态
)); $smtpStatus = $serviceRepo->getStatus('smtp');
/*$raw = shell_exec(sprintf( $pop3Status = $serviceRepo->getStatus('pop3');
'cd %s && (%s > /dev/null 2>&1 </dev/null & echo $!)', $smtpRunning = $serviceRepo->isRunning('smtp');
escapeshellarg(dirname($scriptPath, 2)), $pop3Running = $serviceRepo->isRunning('pop3');
'nohup php ' . escapeshellarg($scriptPath),
escapeshellarg($logFile) // 这一行现在仅用于占位,实际不再写 // 获取端口设置
));*/ $smtpPort = $settingsRepo->get('smtp_port', 25);
$pop3Port = $settingsRepo->get('pop3_port', 110);
// 修复trim(null)问题 ?>
$raw = (string)$raw; // 强制转换为字符串 <!DOCTYPE html>
$pid = trim($raw); // 现在可以安全trim <html>
<head>
error_log("Shell命令执行结果: " . var_export($raw, true)); <title>服务管理 - 邮件服务器</title>
error_log("获取的PID: " . $pid); // 修复:这里$pid已定义 <meta charset="UTF-8">
<style>
if (!is_numeric($pid) || $pid < 1) { body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
error_log("PID无效: $pid"); .header { background: #007bff; color: white; padding: 15px; margin: -20px -20px 20px -20px; }
return ['success' => false, 'message' => '未能获取有效进程号']; .menu { background: white; padding: 10px; margin-bottom: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
} .menu a { margin-right: 15px; text-decoration: none; color: #007bff; }
.container { background: white; padding: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
/* ===== 修复:添加进程状态检查 ===== */ .message { background: #d4edda; color: #155724; padding: 12px; border-radius: 5px; margin-bottom: 20px; }
sleep(1); // 先等待1秒 .error { background: #f8d7da; color: #721c24; padding: 12px; border-radius: 5px; margin-bottom: 20px; }
.service-box { border: 1px solid #ddd; padding: 20px; margin-bottom: 20px; border-radius: 5px; }
// 检查进程是否还在运行 .service-box h3 { margin-top: 0; }
$processExists = file_exists("/proc/$pid"); .status { display: inline-block; padding: 6px 12px; border-radius: 4px; font-weight: 500; margin-right: 10px; }
error_log("进程是否存在: " . ($processExists ? "是" : "否")); .status-running { background: #28a745; color: white; }
.status-stopped { background: #dc3545; color: white; }
if (!$processExists) { .btn { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; text-decoration: none; display: inline-block; }
// 查看脚本为什么退出了 .btn-success { background: #28a745; color: white; }
if (file_exists($logFile)) { .btn-danger { background: #dc3545; color: white; }
$logContent = file_get_contents($logFile); .info { color: #666; font-size: 14px; margin-top: 10px; }
error_log("脚本输出日志:\n" . $logContent); .note { background: #fff3cd; border: 1px solid #ffc107; padding: 15px; border-radius: 5px; margin-top: 20px; }
} </style>
return ['success' => false, 'message' => '进程已退出']; </head>
} <body>
<div class="header">
/* ===== 修复:更长的等待时间 ===== */ <h1>邮件服务器管理后台</h1>
$maxChecks = 6; <div>欢迎, <?php echo htmlspecialchars($_SESSION['username']); ?>
for ($i = 0; $i < $maxChecks; $i++) { (<a href="logout.php" style="color: white;">退出</a>)
sleep(1); // 每次检查等待1秒 </div>
</div>
if (isServiceRunning($serviceName, $port)) {
// 再次确认PID仍然有效 <div class="menu">
if (file_exists("/proc/$pid")) { <a href="index.php">仪表盘</a>
return ['success' => true, 'pid' => $pid]; <a href="users.php">用户管理</a>
} else { <a href="emails.php">邮件管理</a>
// 进程已退出 <a href="broadcast.php">群发邮件</a>
return ['success' => false, 'message' => '进程已退出']; <a href="filters.php">过滤规则</a>
} <a href="logs.php">系统日志</a>
} <a href="services.php">服务管理</a>
<a href="settings.php">系统设置</a>
error_log("第 " . ($i+1) . " 次检查:端口未监听"); </div>
}
<div class="container">
// 启动失败 → 清理孤儿 <h2>服务管理</h2>
if (file_exists("/proc/$pid")) {
@exec("kill -9 {$pid} 2>/dev/null"); <?php if ($message): ?>
} <div class="message"><?php echo $message; ?></div>
<?php endif; ?>
// 输出失败原因
if (file_exists($logFile)) { <?php if ($error): ?>
// 只读取最后10行避免读取大文件 <div class="error"><?php echo $error; ?></div>
$logContent = shell_exec("tail -10 " . escapeshellarg($logFile)); <?php endif; ?>
error_log("最终日志内容(最后100行):\n" . $logContent);
} <!-- SMTP服务 -->
<div class="service-box">
return ['success' => false, 'message' => '服务端口未监听,启动失败']; <h3>SMTP服务邮件发送</h3>
} <p>
<span class="status status-<?php echo $smtpRunning ? 'running' : 'stopped'; ?>">
<?php echo $smtpRunning ? '运行中' : '已停止'; ?>
function stopService($serviceName, $pid, $port) { </span>
// ===== 改:优先用 DB 里的 PID没有再现场嗅探 ===== <?php if ($smtpRunning): ?>
if (empty($pid) || !is_numeric($pid)) { <a href="?service=smtp&action=stop" class="btn btn-danger" onclick="return confirm('确定要停止SMTP服务吗');">停止服务</a>
$pid = getServicePid($serviceName, $port); <?php else: ?>
} <a href="?service=smtp&action=start" class="btn btn-success" onclick="return confirm('确定要启动SMTP服务吗');">启动服务</a>
if ($pid) { <?php endif; ?>
@exec("kill {$pid} 2>/dev/null"); </p>
sleep(1); <div class="info">
if (isServiceRunning($serviceName, $port)) { <strong>端口:</strong><?php echo $smtpPort; ?><br>
@exec("kill -9 {$pid} 2>/dev/null"); <strong>进程ID</strong><?php echo $smtpStatus['pid'] ?? '-'; ?><br>
sleep(1); <strong>最后启动:</strong><?php echo $smtpStatus['last_started_at'] ?? '-'; ?><br>
} <strong>最后停止:</strong><?php echo $smtpStatus['last_stopped_at'] ?? '-'; ?>
} </div>
return !isServiceRunning($serviceName, $port); </div>
}
<!-- POP3服务 -->
function isServiceRunning($serviceName, $port) { <div class="service-box">
// 方法1: 使用单引号确保变量正确展开 <h3>POP3服务邮件接收</h3>
$cmd1 = "netstat -tlnp 2>/dev/null | grep ':" . $port . "' | grep LISTEN"; <p>
$result1 = shell_exec($cmd1); <span class="status status-<?php echo $pop3Running ? 'running' : 'stopped'; ?>">
<?php echo $pop3Running ? '运行中' : '已停止'; ?>
// 方法2: 使用ss命令更可靠 </span>
$cmd2 = "ss -tlnp 2>/dev/null | grep ':" . $port . "'"; <?php if ($pop3Running): ?>
$result2 = shell_exec($cmd2); <a href="?service=pop3&action=stop" class="btn btn-danger" onclick="return confirm('确定要停止POP3服务吗');">停止服务</a>
<?php else: ?>
// 方法3: 使用lsof你已验证有效 <a href="?service=pop3&action=start" class="btn btn-success" onclick="return confirm('确定要启动POP3服务吗');">启动服务</a>
$cmd3 = "lsof -i :" . $port . " 2>/dev/null"; <?php endif; ?>
$result3 = shell_exec($cmd3); </p>
<div class="info">
// 方法4: 直接socket连接测试最可靠 <strong>端口:</strong><?php echo $pop3Port; ?><br>
$fp = @fsockopen('127.0.0.1', $port, $errno, $errstr, 1); <strong>进程ID</strong><?php echo $pop3Status['pid'] ?? '-'; ?><br>
if ($fp) { <strong>最后启动:</strong><?php echo $pop3Status['last_started_at'] ?? '-'; ?><br>
fclose($fp); <strong>最后停止:</strong><?php echo $pop3Status['last_stopped_at'] ?? '-'; ?>
return true; </div>
} </div>
// 任意一个方法有结果都算服务在运行 <div class="note">
return !empty(trim($result1 ?? '')) || <strong>注意:</strong>此页面仅用于管理服务状态。实际启动服务需要使用命令行:
!empty(trim($result2 ?? '')) || <ul>
!empty(trim($result3 ?? '')); <li>SMTP服务<code>sudo php scripts/start_smtp.php</code></li>
} <li>POP3服务<code>sudo php scripts/start_pop3.php</code></li>
</ul>
function getServicePid($serviceName, $port) { </div>
return trim(shell_exec("lsof -ti: {$port} 2>/dev/null | head -1")); </div>
} </body>
// ===================== 修改部分结束 ===================== </html>
// 处理服务起停
if (isset($_GET['action'])) {
$serviceName = $_GET['service'] ?? '';
$action = $_GET['action'] ?? '';
if ($serviceName === 'smtp' || $serviceName === 'pop3') {
$port = $serviceName === 'smtp' ? $smtpPort : $pop3Port;
if ($action === 'start') {
$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, $port);
if (stopService($serviceName, $pid, $port)) {
$serviceRepo->updateStatus($serviceName, false, null);
$message = strtoupper($serviceName) . "服务已停止";
} else {
$error = strtoupper($serviceName) . "服务停止失败";
}
// ===================== 修改部分结束 =====================
}
}
}
// 获取服务状态(修改判断逻辑)
// ===================== 修改部分开始 =====================
$smtpStatus = $serviceRepo->getStatus('smtp');
$pop3Status = $serviceRepo->getStatus('pop3');
// 实际检查服务是否运行(使用配置的端口)
$smtpRunning = isServiceRunning('smtp', $smtpPort);
$pop3Running = isServiceRunning('pop3', $pop3Port);
// 如果数据库状态与实际状态不一致,更新数据库
if ($smtpStatus['is_running'] != $smtpRunning) {
$pid = $smtpRunning ? getServicePid('smtp', $smtpPort) : null;
$serviceRepo->updateStatus('smtp', $smtpRunning, $pid);
}
if ($pop3Status['is_running'] != $pop3Running) {
$pid = $pop3Running ? getServicePid('pop3', $pop3Port) : null;
$serviceRepo->updateStatus('pop3', $pop3Running, $pid);
}
// 重新获取更新后的状态
$smtpStatus = $serviceRepo->getStatus('smtp');
$pop3Status = $serviceRepo->getStatus('pop3');
$smtpRunning = $smtpStatus['is_running'];
$pop3Running = $pop3Status['is_running'];
// ===================== 修改部分结束 =====================
?>
<!DOCTYPE html>
<html>
<head>
<title>服务管理 - 邮件服务器</title>
<meta charset="UTF-8">
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
.header { background: #007bff; color: white; padding: 15px; margin: -20px -20px 20px -20px; }
.menu { background: white; padding: 10px; margin-bottom: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.menu a { margin-right: 15px; text-decoration: none; color: #007bff; }
.container { background: white; padding: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.message { background: #d4edda; color: #155724; padding: 12px; border-radius: 5px; margin-bottom: 20px; }
.error { background: #f8d7da; color: #721c24; padding: 12px; border-radius: 5px; margin-bottom: 20px; }
.service-box { border: 1px solid #ddd; padding: 20px; margin-bottom: 20px; border-radius: 5px; }
.service-box h3 { margin-top: 0; }
.status { display: inline-block; padding: 6px 12px; border-radius: 4px; font-weight: 500; margin-right: 10px; }
.status-running { background: #28a745; color: white; }
.status-stopped { background: #dc3545; color: white; }
.btn { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; text-decoration: none; display: inline-block; }
.btn-success { background: #28a745; color: white; }
.btn-danger { background: #dc3545; color: white; }
.info { color: #666; font-size: 14px; margin-top: 10px; }
.note { background: #fff3cd; border: 1px solid #ffc107; padding: 15px; border-radius: 5px; margin-top: 20px; }
</style>
</head>
<body>
<div class="header">
<h1>邮件服务器管理后台</h1>
<div>欢迎, <?php echo htmlspecialchars($_SESSION['username']); ?>
(<a href="logout.php" style="color: white;">退出</a>)
</div>
</div>
<div class="menu">
<a href="index.php">仪表盘</a>
<a href="users.php">用户管理</a>
<a href="broadcast.php">群发邮件</a>
<a href="filters.php">过滤规则</a>
<a href="logs.php">系统日志</a>
<a href="services.php">服务管理</a>
<a href="settings.php">系统设置</a>
<a href="help.php">帮助</a>
</div>
<div class="container">
<h2>服务管理</h2>
<?php if ($message): ?>
<div class="message"><?php echo $message; ?></div>
<?php endif; ?>
<?php if ($error): ?>
<div class="error"><?php echo $error; ?></div>
<?php endif; ?>
<!-- SMTP服务 -->
<div class="service-box">
<h3>SMTP服务邮件发送</h3>
<p>
<span class="status status-<?php echo $smtpRunning ? 'running' : 'stopped'; ?>">
<?php echo $smtpRunning ? '运行中' : '已停止'; ?>
</span>
<?php if ($smtpRunning): ?>
<a href="?service=smtp&action=stop" class="btn btn-danger" onclick="return confirm('确定要停止SMTP服务吗');">停止服务</a>
<?php else: ?>
<a href="?service=smtp&action=start" class="btn btn-success" onclick="return confirm('确定要启动SMTP服务吗');">启动服务</a>
<?php endif; ?>
</p>
<div class="info">
<strong>端口:</strong><?php echo $smtpPort; ?><br>
<strong>进程ID</strong><?php echo $smtpStatus['pid'] ?? '-'; ?><br>
<strong>最后启动:</strong><?php echo $smtpStatus['last_started_at'] ?? '-'; ?><br>
<strong>最后停止:</strong><?php echo $smtpStatus['last_stopped_at'] ?? '-'; ?>
</div>
</div>
<!-- POP3服务 -->
<div class="service-box">
<h3>POP3服务邮件接收</h3>
<p>
<span class="status status-<?php echo $pop3Running ? 'running' : 'stopped'; ?>">
<?php echo $pop3Running ? '运行中' : '已停止'; ?>
</span>
<?php if ($pop3Running): ?>
<a href="?service=pop3&action=stop" class="btn btn-danger" onclick="return confirm('确定要停止POP3服务吗');">停止服务</a>
<?php else: ?>
<a href="?service=pop3&action=start" class="btn btn-success" onclick="return confirm('确定要启动POP3服务吗');">启动服务</a>
<?php endif; ?>
</p>
<div class="info">
<strong>端口:</strong><?php echo $pop3Port; ?><br>
<strong>进程ID</strong><?php echo $pop3Status['pid'] ?? '-'; ?><br>
<strong>最后启动:</strong><?php echo $pop3Status['last_started_at'] ?? '-'; ?><br>
<strong>最后停止:</strong><?php echo $pop3Status['last_stopped_at'] ?? '-'; ?>
</div>
</div>
<div class="note">
<strong>注意:</strong>
<ul>
<li>如果服务启动失败,请检查是否没有该端口访问权限或端口被占用</li>
<li>当前配置SMTP端口 <?php echo $smtpPort; ?>, POP3端口 <?php echo $pop3Port; ?></li>
</ul>
</div>
</div>
<!-- ===================== 新增部分:自动刷新状态 =====================
<script>
// 每5秒自动检查服务状态
setInterval(function() {
fetch(window.location.href)
.then(response => response.text())
.then(html => {
// 从返回的HTML中提取运行状态
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
// 检查SMTP状态是否变化
const smtpStatusText = tempDiv.querySelector('.service-box:nth-child(1) .status')?.textContent;
const currentSmtpStatus = document.querySelector('.service-box:nth-child(1) .status')?.textContent;
// 检查POP3状态是否变化
const pop3StatusText = tempDiv.querySelector('.service-box:nth-child(2) .status')?.textContent;
const currentPop3Status = document.querySelector('.service-box:nth-child(2) .status')?.textContent;
// 如果有变化,刷新页面
if (smtpStatusText !== currentSmtpStatus || pop3StatusText !== currentPop3Status) {
console.log('服务状态变化,刷新页面...');
location.reload();
}
})
.catch(error => console.error('状态检查失败:', error));
}, 30000);
</script>**-->
</body>
</html>

@ -1,341 +1,320 @@
<?php <?php
/** /**
* 系统设置页面 * 系统设置页面
* *
* @uses SystemSettingsRepository * @uses SystemSettingsRepository
* @uses UserRepository * @uses UserRepository
* @uses MailboxRepository * @uses MailboxRepository
*/ */
require_once __DIR__ . '/../config/database.php'; require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../src/storage/Database.php'; require_once __DIR__ . '/../src/storage/Database.php';
require_once __DIR__ . '/../src/storage/SystemSettingsRepository.php'; require_once __DIR__ . '/../src/storage/SystemSettingsRepository.php';
require_once __DIR__ . '/../src/storage/UserRepository.php'; require_once __DIR__ . '/../src/storage/UserRepository.php';
require_once __DIR__ . '/../src/storage/MailboxRepository.php'; require_once __DIR__ . '/../src/storage/MailboxRepository.php';
require_once __DIR__ . '/../src/utils/Validator.php'; require_once __DIR__ . '/../src/utils/Validator.php';
require_once __DIR__ . '/../src/utils/Security.php'; require_once __DIR__ . '/../src/utils/Security.php';
//开启报错提示 session_start();
ini_set('log_errors', 1);
ini_set('error_log', '/dev/stdout'); // 身份验证
if (!isset($_SESSION['user_id'])) {
session_start(); header('Location: index.php');
exit;
// 是否登录验证 }
if (!isset($_SESSION['user_id'])) {
header('Location: index.php'); // 检查管理员权限
exit; if (!$_SESSION['is_admin']) {
} die('权限不足:只有管理员可以访问此页面');
}
/** @var SystemSettingsRepository */ /** @var SystemSettingsRepository */
$settingsRepo = new SystemSettingsRepository(); $settingsRepo = new SystemSettingsRepository();
/** @var UserRepository */ /** @var UserRepository */
$userRepo = new UserRepository(); $userRepo = new UserRepository();
/** @var MailboxRepository */ /** @var MailboxRepository */
$mailboxRepo = new MailboxRepository(); $mailboxRepo = new MailboxRepository();
$message = ''; $message = '';
$error = ''; $error = '';
$domain = $settingsRepo->get('domain', 'test.com');
// 处理系统设置更新
// 处理系统设置更新 if (isset($_POST['update_settings'])) {
if (isset($_POST['update_settings'])) { // SMTP端口
// SMTP端口 if (isset($_POST['smtp_port'])) {
if (isset($_POST['smtp_port'])) { $port = (int)$_POST['smtp_port'];
$port = (int)$_POST['smtp_port']; if (Validator::validatePort($port)) {
if (Validator::validatePort($port)) { $settingsRepo->set('smtp_port', $port);
$settingsRepo->set('smtp_port', $port); } else {
} else { $error = "SMTP端口无效1-65535";
$error = "SMTP端口无效1-65535"; }
} }
}
// POP3端口
// POP3端口 if (isset($_POST['pop3_port'])) {
if (isset($_POST['pop3_port'])) { $port = (int)$_POST['pop3_port'];
$port = (int)$_POST['pop3_port']; if (Validator::validatePort($port)) {
if (Validator::validatePort($port)) { $settingsRepo->set('pop3_port', $port);
$settingsRepo->set('pop3_port', $port); } else {
} else { $error = "POP3端口无效1-65535";
$error = "POP3端口无效1-65535"; }
} }
}
// 域名
// 域名 if (isset($_POST['domain'])) {
if (isset($_POST['domain'])) { $domain = trim($_POST['domain']);
$newDomain = trim($_POST['domain']); if (!empty($domain)) {
if (!empty($newDomain)) { $settingsRepo->set('domain', $domain);
/* **** 调试开始 **** */ }
$oldDomain = $settingsRepo->get('domain', 'test.com'); // 实时旧值 }
error_log('【调试】实时旧域名:' . $oldDomain);
error_log('【调试】提交新域名:' . $newDomain); // 默认邮箱大小限制
/* **** 调试结束 **** */ if (isset($_POST['mailbox_size_limit'])) {
$size = (int)$_POST['mailbox_size_limit'];
if ($newDomain !== $oldDomain) { if ($size > 0) {
$settingsRepo->set('domain', $newDomain); $settingsRepo->set('mailbox_size_limit', $size);
}
require_once __DIR__ . '/../src/admin/SyncDomainService.php'; }
$sync = new SyncDomainService();
$count = $sync->run($oldDomain, $newDomain); // 日志路径
$message .= " 已同步 $count 个用户邮箱后缀。"; if (isset($_POST['log_path'])) {
$settingsRepo->set('log_path', trim($_POST['log_path']));
/* **** 调试开始 **** */ }
error_log('【调试】REPLACE 影响行数:' . $count);
/* **** 调试结束 **** */ // 日志最大大小
if (isset($_POST['log_max_size'])) {
} $size = (int)$_POST['log_max_size'];
} if ($size > 0) {
} $settingsRepo->set('log_max_size', $size);
}
// 默认邮箱大小限制 }
if (isset($_POST['mailbox_size_limit'])) {
$size = (int)$_POST['mailbox_size_limit']; if (empty($error)) {
if ($size > 0) { $message = "系统设置已更新";
$settingsRepo->set('mailbox_size_limit', $size); }
} }
}
// 处理管理员密码修改
// 日志路径 if (isset($_POST['change_admin_password'])) {
if (isset($_POST['log_path'])) { $oldPassword = $_POST['old_password'] ?? '';
$settingsRepo->set('log_path', trim($_POST['log_path'])); $newPassword = $_POST['new_password'] ?? '';
} $confirmPassword = $_POST['confirm_password'] ?? '';
// 日志最大大小 $user = $userRepo->findById($_SESSION['user_id']);
if (isset($_POST['log_max_size'])) {
$size = (int)$_POST['log_max_size']; if (!Security::verifyPassword($oldPassword, $user['password_hash'])) {
if ($size > 0) { $error = "原密码错误";
$settingsRepo->set('log_max_size', $size); } else {
} $passwordValidation = Validator::validatePassword($newPassword, 6);
} if (!$passwordValidation['valid']) {
$error = implode('<br>', $passwordValidation['errors']);
if (empty($error)) { } elseif ($newPassword !== $confirmPassword) {
$message = "系统设置已更新"; $error = "两次输入的密码不一致";
} } else {
} if ($userRepo->update($_SESSION['user_id'], ['password' => $newPassword])) {
$message = "管理员密码已更新";
// 处理管理员密码修改 } else {
if (isset($_POST['change_admin_password'])) { $error = "密码更新失败";
$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'])) { if (isset($_POST['set_mailbox_size'])) {
$error = "原密码错误"; $userId = (int)$_POST['user_id'];
} else { $sizeBytes = (int)$_POST['mailbox_size'];
$passwordValidation = Validator::validatePassword($newPassword, 6);
if (!$passwordValidation['valid']) { if ($sizeBytes > 0) {
$error = implode('<br>', $passwordValidation['errors']); if ($mailboxRepo->setSizeLimit($userId, $sizeBytes)) {
} elseif ($newPassword !== $confirmPassword) { $message = "邮箱大小限制已更新";
$error = "两次输入的密码不一致"; } else {
} else { $error = "更新失败";
if ($userRepo->update($_SESSION['user_id'], ['password' => $newPassword])) { }
$message = "管理员密码已更新"; } else {
} else { $error = "邮箱大小必须大于0";
$error = "密码更新失败"; }
} }
}
} // 获取当前设置
} $settings = $settingsRepo->getAll();
$users = $userRepo->getAll();
// 处理用户邮箱大小设置 ?>
if (isset($_POST['set_mailbox_size'])) { <!DOCTYPE html>
$userId = (int)$_POST['user_id']; <html>
$sizeBytes = (int)$_POST['mailbox_size']; <head>
<title>系统设置 - 邮件服务器</title>
if ($sizeBytes > 0) { <meta charset="UTF-8">
if ($mailboxRepo->setSizeLimit($userId, $sizeBytes)) { <style>
$message = "邮箱大小限制已更新"; body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
} else { .header { background: #007bff; color: white; padding: 15px; margin: -20px -20px 20px -20px; }
$error = "更新失败"; .menu { background: white; padding: 10px; margin-bottom: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
} .menu a { margin-right: 15px; text-decoration: none; color: #007bff; }
} else { .container { background: white; padding: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
$error = "邮箱大小必须大于0"; .section { margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid #ddd; }
} .section:last-child { border-bottom: none; }
} .section h3 { margin-top: 0; color: #333; }
.message { background: #d4edda; color: #155724; padding: 12px; border-radius: 5px; margin-bottom: 20px; }
// 获取当前设置 .error { background: #f8d7da; color: #721c24; padding: 12px; border-radius: 5px; margin-bottom: 20px; }
$settings = $settingsRepo->getAll(); .form-group { margin-bottom: 15px; }
$users = $userRepo->getAll(); .form-group label { display: block; margin-bottom: 5px; font-weight: 500; }
?> .form-group input, .form-group select { width: 100%; max-width: 400px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
<!DOCTYPE html> .form-group small { color: #666; font-size: 12px; }
<html> .btn { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; }
<head> .btn-primary { background: #007bff; color: white; }
<title>系统设置 - 邮件服务器</title> .btn-success { background: #28a745; color: white; }
<meta charset="UTF-8"> table { width: 100%; border-collapse: collapse; margin-top: 15px; }
<style> th, td { border: 1px solid #ddd; padding: 10px; text-align: left; }
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; } th { background: #f8f9fa; }
.header { background: #007bff; color: white; padding: 15px; margin: -20px -20px 20px -20px; } .size-input { width: 150px; }
.menu { background: white; padding: 10px; margin-bottom: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } </style>
.menu a { margin-right: 15px; text-decoration: none; color: #007bff; } </head>
.container { background: white; padding: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } <body>
.section { margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid #ddd; } <div class="header">
.section:last-child { border-bottom: none; } <h1>邮件服务器管理后台</h1>
.section h3 { margin-top: 0; color: #333; } <div>欢迎, <?php echo htmlspecialchars($_SESSION['username']); ?>
.message { background: #d4edda; color: #155724; padding: 12px; border-radius: 5px; margin-bottom: 20px; } (<a href="logout.php" style="color: white;">退出</a>)
.error { background: #f8d7da; color: #721c24; padding: 12px; border-radius: 5px; margin-bottom: 20px; } </div>
.form-group { margin-bottom: 15px; } </div>
.form-group label { display: block; margin-bottom: 5px; font-weight: 500; }
.form-group input, .form-group select { width: 100%; max-width: 400px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; } <div class="menu">
.form-group small { color: #666; font-size: 12px; } <a href="index.php">仪表盘</a>
.btn { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; } <a href="users.php">用户管理</a>
.btn-primary { background: #007bff; color: white; } <a href="emails.php">邮件管理</a>
.btn-success { background: #28a745; color: white; } <a href="broadcast.php">群发邮件</a>
table { width: 100%; border-collapse: collapse; margin-top: 15px; } <a href="filters.php">过滤规则</a>
th, td { border: 1px solid #ddd; padding: 10px; text-align: left; } <a href="logs.php">系统日志</a>
th { background: #f8f9fa; } <a href="settings.php">系统设置</a>
.size-input { width: 150px; } </div>
</style>
</head> <div class="container">
<body> <h2>系统设置</h2>
<div class="header">
<h1>邮件服务器管理后台</h1> <?php if ($message): ?>
<div>欢迎, <?php echo htmlspecialchars($_SESSION['username']); ?> <div class="message"><?php echo $message; ?></div>
(<a href="logout.php" style="color: white;">退出</a>) <?php endif; ?>
</div>
</div> <?php if ($error): ?>
<div class="error"><?php echo $error; ?></div>
<div class="menu"> <?php endif; ?>
<a href="index.php">仪表盘</a>
<a href="users.php">用户管理</a> <!-- 服务器端口设置 -->
<a href="broadcast.php">群发邮件</a> <div class="section">
<a href="filters.php">过滤规则</a> <h3>服务器端口设置</h3>
<a href="logs.php">系统日志</a> <form method="POST">
<a href="services.php">服务管理</a> <div class="form-group">
<a href="settings.php">系统设置</a> <label>SMTP端口默认25</label>
<a href="help.php">帮助</a> <input type="number" name="smtp_port" value="<?php echo htmlspecialchars($settings['smtp_port'] ?? '25'); ?>" min="1" max="65535" required>
</div> <small>SMTP服务器监听端口</small>
</div>
<div class="container"> <div class="form-group">
<h2>系统设置</h2> <label>POP3端口默认110</label>
<input type="number" name="pop3_port" value="<?php echo htmlspecialchars($settings['pop3_port'] ?? '110'); ?>" min="1" max="65535" required>
<?php if ($message): ?> <small>POP3服务器监听端口</small>
<div class="message"><?php echo $message; ?></div> </div>
<?php endif; ?> <button type="submit" name="update_settings" class="btn btn-primary">保存端口设置</button>
</form>
<?php if ($error): ?> </div>
<div class="error"><?php echo $error; ?></div>
<?php endif; ?> <!-- 域名设置 -->
<div class="section">
<!-- 服务器端口设置 --> <h3>域名设置</h3>
<div class="section"> <form method="POST">
<h3>服务器端口设置</h3> <div class="form-group">
<form method="POST"> <label>服务器域名默认test.com</label>
<div class="form-group"> <input type="text" name="domain" value="<?php echo htmlspecialchars($settings['domain'] ?? 'test.com'); ?>" required>
<label>SMTP端口默认25</label> <small>邮件服务器域名,用户邮箱必须使用此域名</small>
<input type="number" name="smtp_port" value="<?php echo htmlspecialchars($settings['smtp_port'] ?? '25'); ?>" min="1" max="65535" required> </div>
<small>SMTP服务器监听端口</small> <button type="submit" name="update_settings" class="btn btn-primary">保存域名设置</button>
</div> </form>
<div class="form-group"> </div>
<label>POP3端口默认110</label>
<input type="number" name="pop3_port" value="<?php echo htmlspecialchars($settings['pop3_port'] ?? '110'); ?>" min="1" max="65535" required> <!-- 邮箱管理 -->
<small>POP3服务器监听端口</small> <div class="section">
</div> <h3>邮箱大小管理</h3>
<button type="submit" name="update_settings" class="btn btn-primary">保存端口设置</button> <form method="POST">
</form> <div class="form-group">
</div> <label>默认邮箱大小限制(字节)</label>
<input type="number" name="mailbox_size_limit" value="<?php echo htmlspecialchars($settings['mailbox_size_limit'] ?? '104857600'); ?>" min="1" required>
<!-- 域名设置 --> <small>默认值104857600 (100MB)</small>
<div class="section"> </div>
<h3>域名设置</h3> <button type="submit" name="update_settings" class="btn btn-primary">保存默认大小</button>
<form method="POST"> </form>
<div class="form-group">
<label>服务器域名默认test.com</label> <h4>用户邮箱大小设置</h4>
<input type="text" name="domain" value="<?php echo htmlspecialchars($settings['domain'] ?? 'test.com'); ?>" > <table>
<small>邮件服务器域名,用户邮箱必须使用此域名</small> <thead>
</div> <tr>
<th>用户</th>
<button type="submit" name="update_settings" class="btn btn-primary">保存域名设置</button> <th>当前使用</th>
</form> <th>限制大小</th>
</div> <th>使用率</th>
<th>操作</th>
<!-- 邮箱管理 --> </tr>
<div class="section"> </thead>
<h3>邮箱大小管理</h3> <tbody>
<form method="POST"> <?php foreach ($users as $user): ?>
<div class="form-group"> <?php
<label>默认邮箱大小限制(字节)</label> $usage = $mailboxRepo->getUsage($user['id']);
<input type="number" name="mailbox_size_limit" value="<?php echo htmlspecialchars($settings['mailbox_size_limit'] ?? '104857600'); ?>" min="1" required> $usedMB = round($usage['used'] / 1048576, 2);
<small>默认值104857600 (100MB)</small> $limitMB = round($usage['limit'] / 1048576, 2);
</div> ?>
<button type="submit" name="update_settings" class="btn btn-primary">保存默认大小</button> <tr>
</form> <td><?php echo htmlspecialchars($user['username']); ?></td>
<td><?php echo $usedMB; ?> MB</td>
<h4>用户邮箱大小设置</h4> <td><?php echo $limitMB; ?> MB</td>
<table> <td><?php echo $usage['percentage']; ?>%</td>
<thead> <td>
<tr> <form method="POST" style="display: inline;">
<th>用户</th> <input type="hidden" name="user_id" value="<?php echo $user['id']; ?>">
<th>当前使用</th> <input type="number" name="mailbox_size" value="<?php echo $usage['limit']; ?>" class="size-input" min="1" required>
<th>限制大小</th> <button type="submit" name="set_mailbox_size" class="btn btn-success">设置</button>
<th>使用率</th> </form>
<th>操作</th> </td>
</tr> </tr>
</thead> <?php endforeach; ?>
<tbody> </tbody>
<?php foreach ($users as $user): ?> </table>
<?php </div>
$usage = $mailboxRepo->getUsage($user['id']);
$usedMB = round($usage['used'] / 1048576, 2); <!-- 日志设置 -->
$limitMB = round($usage['limit'] / 1048576, 2); <div class="section">
?> <h3>日志设置</h3>
<tr> <form method="POST">
<td><?php echo htmlspecialchars($user['username']); ?></td> <div class="form-group">
<td><?php echo $usedMB; ?> MB</td> <label>日志文件存储路径</label>
<td><?php echo $limitMB; ?> MB</td> <input type="text" name="log_path" value="<?php echo htmlspecialchars($settings['log_path'] ?? '/var/log/mailserver'); ?>" required>
<td><?php echo $usage['percentage']; ?>%</td> <small>日志文件存储的目录路径</small>
<td> </div>
<form method="POST" style="display: inline;"> <div class="form-group">
<input type="hidden" name="user_id" value="<?php echo $user['id']; ?>"> <label>日志文件最大大小(字节)</label>
<input type="number" name="mailbox_size" value="<?php echo $usage['limit']; ?>" class="size-input" min="1" required> <input type="number" name="log_max_size" value="<?php echo htmlspecialchars($settings['log_max_size'] ?? '10485760'); ?>" min="1" required>
<button type="submit" name="set_mailbox_size" class="btn btn-success">设置</button> <small>默认值10485760 (10MB)</small>
</form> </div>
</td> <button type="submit" name="update_settings" class="btn btn-primary">保存日志设置</button>
</tr> </form>
<?php endforeach; ?> </div>
</tbody>
</table> <!-- 管理员密码修改 -->
</div> <div class="section">
<h3>修改管理员密码</h3>
<!-- 日志设置 --> <form method="POST">
<div class="section"> <div class="form-group">
<h3>日志设置</h3> <label>原密码</label>
<form method="POST"> <input type="password" name="old_password" required>
<div class="form-group"> </div>
<label>日志文件存储路径</label> <div class="form-group">
<input type="text" name="log_path" value="<?php echo htmlspecialchars($settings['log_path'] ?? '/var/log/mailserver'); ?>" required> <label>新密码</label>
<small>日志文件存储的目录路径</small> <input type="password" name="new_password" required minlength="6">
</div> <small>密码长度至少6个字符</small>
<div class="form-group"> </div>
<label>日志文件最大大小(字节)</label> <div class="form-group">
<input type="number" name="log_max_size" value="<?php echo htmlspecialchars($settings['log_max_size'] ?? '10485760'); ?>" min="1" required> <label>确认新密码</label>
<small>默认值10485760 (10MB)</small> <input type="password" name="confirm_password" required minlength="6">
</div> </div>
<button type="submit" name="update_settings" class="btn btn-primary">保存日志设置</button> <button type="submit" name="change_admin_password" class="btn btn-primary">修改密码</button>
</form> </form>
</div> </div>
</div>
<!-- 管理员密码修改 --> </body>
<div class="section"> </html>
<h3>修改管理员密码</h3>
<form method="POST">
<div class="form-group">
<label>原密码</label>
<input type="password" name="old_password" required>
</div>
<div class="form-group">
<label>新密码</label>
<input type="password" name="new_password" required minlength="6">
<small>密码长度至少6个字符</small>
</div>
<div class="form-group">
<label>确认新密码</label>
<input type="password" name="confirm_password" required minlength="6">
</div>
<button type="submit" name="change_admin_password" class="btn btn-primary">修改密码</button>
</form>
</div>
</div>
</body>
</html>

@ -1,299 +1,292 @@
<?php <?php
require_once __DIR__ . '/../config/database.php'; require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../src/storage/Database.php'; require_once __DIR__ . '/../src/storage/Database.php';
require_once __DIR__ . '/../src/storage/UserRepository.php'; require_once __DIR__ . '/../src/storage/UserRepository.php';
require_once __DIR__ . '/../src/utils/Validator.php'; require_once __DIR__ . '/../src/utils/Validator.php';
require_once __DIR__ . '/../src/utils/Security.php'; require_once __DIR__ . '/../src/utils/Security.php';
require_once __DIR__ . '/../src/storage/SystemSettingsRepository.php';
session_start();
session_start();
// 身份验证
// 身份验证 if (!isset($_SESSION['user_id'])) {
if (!isset($_SESSION['user_id'])) { header('Location: index.php');
header('Location: index.php'); exit;
exit; }
}
// 检查管理员权限
$settingsRepo = new SystemSettingsRepository(); if (!$_SESSION['is_admin']) {
// 获取域名设置(放在函数定义之前) die('权限不足:只有管理员可以访问此页面');
$domain = $settingsRepo->get('domain', 'test.com'); }
$userRepo = new UserRepository(); $userRepo = new UserRepository();
$message = ''; $message = '';
$error = ''; $error = '';
// 处理创建用户 // 处理创建用户
if (isset($_POST['create_user'])) { if (isset($_POST['create_user'])) {
$username = trim($_POST['username'] ?? ''); $username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? ''; $password = $_POST['password'] ?? '';
$isAdmin = isset($_POST['is_admin']) ? 1 : 0; $isAdmin = isset($_POST['is_admin']) ? 1 : 0;
$isActive = isset($_POST['is_active']) ? 1 : 0; $isActive = isset($_POST['is_active']) ? 1 : 0;
$usernameValidation = Validator::validateUsername($username); $usernameValidation = Validator::validateUsername($username);
if (!$usernameValidation['valid']) { if (!$usernameValidation['valid']) {
$error = implode('<br>', $usernameValidation['errors']); $error = implode('<br>', $usernameValidation['errors']);
} else { } else {
if (!Validator::validateEmailDomain($username, $domain)) { if (!Validator::validateEmailDomain($username, 'test.com')) {
$error = "邮箱域名必须是 @".$domain; $error = "邮箱域名必须是 @test.com";
} else { } else {
$passwordValidation = Validator::validatePassword($password, 6); $passwordValidation = Validator::validatePassword($password, 6);
if (!$passwordValidation['valid']) { if (!$passwordValidation['valid']) {
$error = implode('<br>', $passwordValidation['errors']); $error = implode('<br>', $passwordValidation['errors']);
} else { } else {
try { try {
if ($userRepo->usernameExists($username)) { if ($userRepo->usernameExists($username)) {
$error = "用户名已存在"; $error = "用户名已存在";
} else { } else {
$userRepo->create($username, $password, $isAdmin, $isActive); $userRepo->create($username, $password, $isAdmin, $isActive);
$message = "用户创建成功"; $message = "用户创建成功";
} }
} catch (Exception $e) { } catch (Exception $e) {
$error = "创建失败: " . $e->getMessage(); $error = "创建失败: " . $e->getMessage();
} }
} }
} }
} }
} }
// 处理更新用户 // 处理更新用户
if (isset($_POST['update_user'])) { if (isset($_POST['update_user'])) {
$userId = (int)$_POST['user_id']; $userId = (int)$_POST['user_id'];
$data = []; $data = [];
if (!empty($_POST['new_password'])) { if (!empty($_POST['new_password'])) {
$passwordValidation = Validator::validatePassword($_POST['new_password'], 6); $passwordValidation = Validator::validatePassword($_POST['new_password'], 6);
if (!$passwordValidation['valid']) { if (!$passwordValidation['valid']) {
$error = implode('<br>', $passwordValidation['errors']); $error = implode('<br>', $passwordValidation['errors']);
} else { } else {
$data['password'] = $_POST['new_password']; $data['password'] = $_POST['new_password'];
} }
} }
/**if (isset($_POST['is_admin'])) { if (isset($_POST['is_admin'])) {
$data['is_admin'] = (int)$_POST['is_admin']; $data['is_admin'] = (int)$_POST['is_admin'];
} }
if (isset($_POST['is_active'])) { if (isset($_POST['is_active'])) {
$data['is_active'] = (int)$_POST['is_active']; $data['is_active'] = (int)$_POST['is_active'];
}**/ }
// 管理员权限总是更新
$data['is_admin'] = isset($_POST['is_admin']) ? 1 : 0; if (empty($error) && !empty($data)) {
if ($userRepo->update($userId, $data)) {
// 激活状态也是 $message = "用户更新成功";
$data['is_active'] = isset($_POST['is_active']) ? 1 : 0; } else {
$error = "更新失败";
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 = "用户删除成功";
if (isset($_GET['delete'])) { } else {
$userId = (int)$_GET['delete']; $error = "删除失败";
if ($userId != $_SESSION['user_id']) { // 不能删除自己 }
if ($userRepo->delete($userId)) { } else {
$message = "用户删除成功"; $error = "不能删除自己的账号";
} else { }
$error = "删除失败"; }
}
} else { // 获取所有用户
$error = "不能删除自己的账号"; $users = $userRepo->getAll();
} ?>
} <!DOCTYPE html>
<html>
// 获取所有用户 <head>
$users = $userRepo->getAll(); <title>用户管理 - 邮件服务器</title>
?> <meta charset="UTF-8">
<!DOCTYPE html> <style>
<html> body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
<head> .header { background: #007bff; color: white; padding: 15px; margin: -20px -20px 20px -20px; }
<title>用户管理 - 邮件服务器</title> .menu { background: white; padding: 10px; margin-bottom: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
<meta charset="UTF-8"> .menu a { margin-right: 15px; text-decoration: none; color: #007bff; }
<style> .container { background: white; padding: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; } .message { background: #d4edda; color: #155724; padding: 12px; border-radius: 5px; margin-bottom: 20px; }
.header { background: #007bff; color: white; padding: 15px; margin: -20px -20px 20px -20px; } .error { background: #f8d7da; color: #721c24; padding: 12px; border-radius: 5px; margin-bottom: 20px; }
.menu { background: white; padding: 10px; margin-bottom: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } table { width: 100%; border-collapse: collapse; margin-top: 20px; }
.menu a { margin-right: 15px; text-decoration: none; color: #007bff; } th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
.container { background: white; padding: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } th { background: #f8f9fa; font-weight: 600; }
.message { background: #d4edda; color: #155724; padding: 12px; border-radius: 5px; margin-bottom: 20px; } tr:hover { background: #f8f9fa; }
.error { background: #f8d7da; color: #721c24; padding: 12px; border-radius: 5px; margin-bottom: 20px; } .btn { padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; text-decoration: none; display: inline-block; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; } .btn-primary { background: #007bff; color: white; }
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; } .btn-danger { background: #dc3545; color: white; }
th { background: #f8f9fa; font-weight: 600; } .btn-success { background: #28a745; color: white; }
tr:hover { background: #f8f9fa; } .btn-small { padding: 4px 8px; font-size: 12px; }
.btn { padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; text-decoration: none; display: inline-block; } .form-group { margin-bottom: 15px; }
.btn-primary { background: #007bff; color: white; } .form-group label { display: block; margin-bottom: 5px; font-weight: 500; }
.btn-danger { background: #dc3545; color: white; } .form-group input, .form-group select { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
.btn-success { background: #28a745; color: white; } .form-inline { display: flex; gap: 10px; align-items: flex-end; }
.btn-small { padding: 4px 8px; font-size: 12px; } .form-inline .form-group { flex: 1; margin-bottom: 0; }
.form-group { margin-bottom: 15px; } .badge { padding: 4px 8px; border-radius: 3px; font-size: 12px; font-weight: 500; }
.form-group label { display: block; margin-bottom: 5px; font-weight: 500; } .badge-admin { background: #ffc107; color: #000; }
.form-group input, .form-group select { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; } .badge-active { background: #28a745; color: white; }
.form-inline { display: flex; gap: 10px; align-items: flex-end; } .badge-inactive { background: #6c757d; color: white; }
.form-inline .form-group { flex: 1; margin-bottom: 0; } .modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); }
.badge { padding: 4px 8px; border-radius: 3px; font-size: 12px; font-weight: 500; } .modal-content { background: white; margin: 50px auto; padding: 20px; width: 500px; border-radius: 5px; }
.badge-admin { background: #ffc107; color: #000; } .close { float: right; font-size: 28px; font-weight: bold; cursor: pointer; }
.badge-active { background: #28a745; color: white; } </style>
.badge-inactive { background: #6c757d; color: white; } </head>
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); } <body>
.modal-content { background: white; margin: 50px auto; padding: 20px; width: 500px; border-radius: 5px; } <div class="header">
.close { float: right; font-size: 28px; font-weight: bold; cursor: pointer; } <h1>邮件服务器管理后台</h1>
</style> <div>欢迎, <?php echo htmlspecialchars($_SESSION['username']); ?>
</head> (<a href="logout.php" style="color: white;">退出</a>)
<body> </div>
<div class="header"> </div>
<h1>邮件服务器管理后台</h1>
<div>欢迎, <?php echo htmlspecialchars($_SESSION['username']); ?> <div class="menu">
(<a href="logout.php" style="color: white;">退出</a>) <a href="index.php">仪表盘</a>
</div> <a href="users.php">用户管理</a>
</div> <a href="emails.php">邮件管理</a>
<a href="filters.php">过滤规则</a>
<div class="menu"> <a href="logs.php">系统日志</a>
<a href="index.php">仪表盘</a> <a href="settings.php">系统设置</a>
<a href="users.php">用户管理</a> </div>
<a href="broadcast.php">群发邮件</a>
<a href="filters.php">过滤规则</a> <div class="container">
<a href="logs.php">系统日志</a> <h2>用户管理</h2>
<a href="services.php">服务管理</a>
<a href="settings.php">系统设置</a> <?php if ($message): ?>
<a href="help.php">帮助</a> <div class="message"><?php echo $message; ?></div>
</div> <?php endif; ?>
<div class="container"> <?php if ($error): ?>
<h2>用户管理</h2> <div class="error"><?php echo $error; ?></div>
<?php endif; ?>
<?php if ($message): ?>
<div class="message"><?php echo $message; ?></div> <!-- 创建用户表单 -->
<?php endif; ?> <h3>创建新用户</h3>
<form method="POST" class="form-inline">
<?php if ($error): ?> <div class="form-group">
<div class="error"><?php echo $error; ?></div> <label>邮箱地址</label>
<?php endif; ?> <input type="email" name="username" placeholder="user@test.com" required>
</div>
<!-- 创建用户表单 --> <div class="form-group">
<h3>创建新用户</h3> <label>密码</label>
<form method="POST" class="form-inline"> <input type="password" name="password" placeholder="至少6个字符" required minlength="6">
<div class="form-group"> </div>
<label>邮箱地址</label> <div class="form-group">
<input type="email" name="username" placeholder="user@<?= htmlspecialchars($domain) ?>" required> <label>管理员</label>
</div> <input type="checkbox" name="is_admin" value="1">
<div class="form-group"> </div>
<label>密码</label> <div class="form-group">
<input type="password" name="password" placeholder="至少6个字符" required minlength="6"> <label>激活</label>
</div> <input type="checkbox" name="is_active" value="1" checked>
<div class="form-group"> </div>
<label>管理员</label> <div class="form-group">
<input type="checkbox" name="is_admin" value="1"> <button type="submit" name="create_user" class="btn btn-primary">创建用户</button>
</div> </div>
<div class="form-group"> </form>
<label>激活</label>
<input type="checkbox" name="is_active" value="1" checked> <!-- 用户列表 -->
</div> <h3>用户列表 (<?php echo count($users); ?>)</h3>
<div class="form-group"> <table>
<button type="submit" name="create_user" class="btn btn-primary">创建用户</button> <thead>
</div> <tr>
</form> <th>ID</th>
<th>用户名</th>
<!-- 用户列表 --> <th>角色</th>
<h3>用户列表 (<?php echo count($users); ?>)</h3> <th>状态</th>
<table> <th>创建时间</th>
<thead> <th>操作</th>
<tr> </tr>
<th>ID</th> </thead>
<th>用户名</th> <tbody>
<th>角色</th> <?php foreach ($users as $user): ?>
<th>状态</th> <tr>
<th>创建时间</th> <td><?php echo $user['id']; ?></td>
<th>操作</th> <td><?php echo htmlspecialchars($user['username']); ?></td>
</tr> <td>
</thead> <?php if ($user['is_admin']): ?>
<tbody> <span class="badge badge-admin">管理员</span>
<?php foreach ($users as $user): ?> <?php else: ?>
<tr> <span>普通用户</span>
<td><?php echo $user['id']; ?></td> <?php endif; ?>
<td><?php echo htmlspecialchars($user['username']); ?></td> </td>
<td> <td>
<?php if ($user['is_admin']): ?> <?php if ($user['is_active']): ?>
<span class="badge badge-admin">管理员</span> <span class="badge badge-active">激活</span>
<?php else: ?> <?php else: ?>
<span>普通用户</span> <span class="badge badge-inactive">禁用</span>
<?php endif; ?> <?php endif; ?>
</td> </td>
<td> <td><?php echo $user['created_at']; ?></td>
<?php if ($user['is_active']): ?> <td>
<span class="badge badge-active">激活</span> <a href="#" onclick="editUser(<?php echo htmlspecialchars(json_encode($user)); ?>); return false;" class="btn btn-primary btn-small">编辑</a>
<?php else: ?> <?php if ($user['id'] != $_SESSION['user_id']): ?>
<span class="badge badge-inactive">禁用</span> <a href="?delete=<?php echo $user['id']; ?>" class="btn btn-danger btn-small" onclick="return confirm('确定要删除此用户吗?');">删除</a>
<?php endif; ?> <?php endif; ?>
</td> </td>
<td><?php echo $user['created_at']; ?></td> </tr>
<td> <?php endforeach; ?>
<a href="#" onclick="editUser(<?php echo htmlspecialchars(json_encode($user)); ?>); return false;" class="btn btn-primary btn-small">编辑</a> </tbody>
<?php if ($user['id'] != $_SESSION['user_id']): ?> </table>
<a href="?delete=<?php echo $user['id']; ?>" class="btn btn-danger btn-small" onclick="return confirm('确定要删除此用户吗?');">删除</a> </div>
<?php endif; ?>
</td> <!-- 编辑用户模态框 -->
</tr> <div id="editModal" class="modal">
<?php endforeach; ?> <div class="modal-content">
</tbody> <span class="close" onclick="closeModal()">&times;</span>
</table> <h3>编辑用户</h3>
</div> <form method="POST">
<input type="hidden" name="user_id" id="edit_user_id">
<!-- 编辑用户模态框 --> <div class="form-group">
<div id="editModal" class="modal"> <label>用户名</label>
<div class="modal-content"> <input type="text" id="edit_username" readonly style="background: #f5f5f5;">
<span class="close" onclick="closeModal()">&times;</span> </div>
<h3>编辑用户</h3> <div class="form-group">
<form method="POST"> <label>新密码(留空则不修改)</label>
<input type="hidden" name="user_id" id="edit_user_id"> <input type="password" name="new_password" placeholder="留空则不修改">
<div class="form-group"> </div>
<label>用户名</label> <div class="form-group">
<input type="text" id="edit_username" readonly style="background: #f5f5f5;"> <label>
</div> <input type="checkbox" name="is_admin" id="edit_is_admin" value="1"> 管理员
<div class="form-group"> </label>
<label>新密码(留空则不修改)</label> </div>
<input type="password" name="new_password" placeholder="留空则不修改"> <div class="form-group">
</div> <label>
<div class="form-group"> <input type="checkbox" name="is_active" id="edit_is_active" value="1"> 激活
<label> </label>
<input type="checkbox" name="is_admin" id="edit_is_admin" value="1"> 管理员 </div>
</label> <button type="submit" name="update_user" class="btn btn-success">保存</button>
</div> <button type="button" onclick="closeModal()" class="btn">取消</button>
<div class="form-group"> </form>
<label> </div>
<input type="checkbox" name="is_active" id="edit_is_active" value="1"> 激活 </div>
</label>
</div> <script>
<button type="submit" name="update_user" class="btn btn-success">保存</button> function editUser(user) {
<button type="button" onclick="closeModal()" class="btn">取消</button> document.getElementById('edit_user_id').value = user.id;
</form> document.getElementById('edit_username').value = user.username;
</div> document.getElementById('edit_is_admin').checked = user.is_admin == 1;
</div> document.getElementById('edit_is_active').checked = user.is_active == 1;
document.getElementById('editModal').style.display = 'block';
<script> }
function editUser(user) {
document.getElementById('edit_user_id').value = user.id; function closeModal() {
document.getElementById('edit_username').value = user.username; document.getElementById('editModal').style.display = 'none';
document.getElementById('edit_is_admin').checked = user.is_admin == 1; }
document.getElementById('edit_is_active').checked = user.is_active == 1;
document.getElementById('editModal').style.display = 'block'; window.onclick = function(event) {
} var modal = document.getElementById('editModal');
if (event.target == modal) {
function closeModal() { closeModal();
document.getElementById('editModal').style.display = 'none'; }
} }
</script>
window.onclick = function(event) { </body>
var modal = document.getElementById('editModal'); </html>
if (event.target == modal) {
closeModal();
}
}
</script>
</body>
</html>

@ -1,11 +1,11 @@
#!/bin/bash #!/bin/bash
echo "重置数据库..." echo "重置数据库..."
docker-compose down -v docker-compose down -v
echo "启动数据库SQL会自动执行..." echo "启动数据库SQL会自动执行..."
docker-compose up -d docker-compose up -d
echo "等待数据库初始化10秒..." echo "等待数据库初始化10秒..."
sleep 10 sleep 10
echo "验证数据..." 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;" 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 "完成!" echo "完成!"

@ -1,135 +1,135 @@
-- ============================================ -- ============================================
-- 邮件服务器完整数据库初始化脚本 -- 邮件服务器完整数据库初始化脚本
-- ============================================ -- ============================================
-- 创建数据库(如果不存在) -- 创建数据库(如果不存在)
CREATE DATABASE IF NOT EXISTS mail_server CREATE DATABASE IF NOT EXISTS mail_server
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE mail_server; USE mail_server;
-- 创建用户并授权(关键步骤!) -- 创建用户并授权(关键步骤!)
CREATE USER IF NOT EXISTS 'mail_user'@'%' IDENTIFIED BY 'user123'; CREATE USER IF NOT EXISTS 'mail_user'@'%' IDENTIFIED BY 'user123';
GRANT ALL PRIVILEGES ON mail_server.* TO 'mail_user'@'%'; GRANT ALL PRIVILEGES ON mail_server.* TO 'mail_user'@'%';
FLUSH PRIVILEGES; FLUSH PRIVILEGES;
-- ============================================ -- ============================================
-- 核心功能表 -- 核心功能表
-- ============================================ -- ============================================
-- 用户表 -- 用户表
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(100) UNIQUE NOT NULL, username VARCHAR(100) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL, password_hash VARCHAR(255) NOT NULL,
is_admin TINYINT(1) DEFAULT 0, is_admin TINYINT(1) DEFAULT 0,
is_active TINYINT(1) DEFAULT 1, is_active TINYINT(1) DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 邮件表兼容两种存储方式SimpleSmtpServer用sender/recipientSmtpHandler用sender_id/recipient_id -- 邮件表兼容两种存储方式SimpleSmtpServer用sender/recipientSmtpHandler用sender_id/recipient_id
CREATE TABLE IF NOT EXISTS emails ( CREATE TABLE IF NOT EXISTS emails (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
sender VARCHAR(100), sender VARCHAR(100),
recipient VARCHAR(100), recipient VARCHAR(100),
sender_id INT, sender_id INT,
recipient_id INT, recipient_id INT,
subject VARCHAR(200), subject VARCHAR(200),
body TEXT, body TEXT,
headers TEXT, headers TEXT,
size_bytes INT DEFAULT 0, size_bytes INT DEFAULT 0,
is_deleted TINYINT(1) DEFAULT 0, is_deleted TINYINT(1) DEFAULT 0,
is_read TINYINT(1) DEFAULT 0, is_read TINYINT(1) DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (sender_id) REFERENCES users(id) ON DELETE SET NULL, FOREIGN KEY (sender_id) REFERENCES users(id) ON DELETE SET NULL,
FOREIGN KEY (recipient_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; ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE=InnoDB;
-- 系统日志表 -- 系统日志表
CREATE TABLE IF NOT EXISTS server_logs ( CREATE TABLE IF NOT EXISTS server_logs (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
log_type VARCHAR(50), log_type VARCHAR(50),
message TEXT, message TEXT,
client_ip VARCHAR(45), client_ip VARCHAR(45),
user_id INT, user_id INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE=InnoDB; ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE=InnoDB;
-- ============================================ -- ============================================
-- 管理功能表 -- 管理功能表
-- ============================================ -- ============================================
-- 系统设置表 -- 系统设置表
CREATE TABLE IF NOT EXISTS system_settings ( CREATE TABLE IF NOT EXISTS system_settings (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
setting_key VARCHAR(100) UNIQUE NOT NULL, setting_key VARCHAR(100) UNIQUE NOT NULL,
setting_value TEXT, setting_value TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE=InnoDB; ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE=InnoDB;
-- 过滤规则表 -- 过滤规则表
CREATE TABLE IF NOT EXISTS filter_rules ( CREATE TABLE IF NOT EXISTS filter_rules (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
rule_type ENUM('email', 'ip') NOT NULL, rule_type ENUM('email', 'ip') NOT NULL,
rule_value VARCHAR(255) NOT NULL, rule_value VARCHAR(255) NOT NULL,
action ENUM('block', 'allow') DEFAULT 'block', action ENUM('block', 'allow') DEFAULT 'block',
description TEXT, description TEXT,
is_active TINYINT(1) DEFAULT 1, is_active TINYINT(1) DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY unique_rule (rule_type, rule_value) UNIQUE KEY unique_rule (rule_type, rule_value)
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE=InnoDB; ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE=InnoDB;
-- 服务状态表 -- 服务状态表
CREATE TABLE IF NOT EXISTS service_status ( CREATE TABLE IF NOT EXISTS service_status (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
service_name VARCHAR(50) UNIQUE NOT NULL, service_name VARCHAR(50) UNIQUE NOT NULL,
is_running TINYINT(1) DEFAULT 0, is_running TINYINT(1) DEFAULT 0,
pid INT, pid INT,
last_started_at TIMESTAMP NULL, last_started_at TIMESTAMP NULL,
last_stopped_at TIMESTAMP NULL, last_stopped_at TIMESTAMP NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE=InnoDB; ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE=InnoDB;
-- 用户邮箱大小限制表 -- 用户邮箱大小限制表
CREATE TABLE IF NOT EXISTS user_mailbox_limits ( CREATE TABLE IF NOT EXISTS user_mailbox_limits (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL, user_id INT NOT NULL,
size_limit_bytes BIGINT DEFAULT 104857600, -- 100MB size_limit_bytes BIGINT DEFAULT 104857600, -- 100MB
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE KEY unique_user (user_id) UNIQUE KEY unique_user (user_id)
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE=InnoDB; ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE=InnoDB;
-- ============================================ -- ============================================
-- 插入默认数据 -- 插入默认数据
-- ============================================ -- ============================================
-- 插入测试用户密码都是123456 -- 插入测试用户密码都是123456
INSERT INTO users (username, password_hash, is_admin, is_active) VALUES INSERT INTO users (username, password_hash, is_admin, is_active) VALUES
('admin@test.com', '$2y$10$jB21V61k9aLAyp5.5qBpV.L70Aq6.XrtJrvlNI28bOXeJboLBJwoq', 1, 1), ('admin@test.com', '$2y$10$jB21V61k9aLAyp5.5qBpV.L70Aq6.XrtJrvlNI28bOXeJboLBJwoq', 1, 1),
('user1@test.com', '$2y$10$jB21V61k9aLAyp5.5qBpV.L70Aq6.XrtJrvlNI28bOXeJboLBJwoq', 0, 1) ('user1@test.com', '$2y$10$jB21V61k9aLAyp5.5qBpV.L70Aq6.XrtJrvlNI28bOXeJboLBJwoq', 0, 1)
ON DUPLICATE KEY UPDATE username = VALUES(username); ON DUPLICATE KEY UPDATE username = VALUES(username);
-- 插入测试邮件 -- 插入测试邮件
INSERT INTO emails (sender, recipient, subject, body) VALUES INSERT INTO emails (sender, recipient, subject, body) VALUES
('admin@test.com', 'user1@test.com', '欢迎使用邮件系统', '这是第一封测试邮件'), ('admin@test.com', 'user1@test.com', '欢迎使用邮件系统', '这是第一封测试邮件'),
('user1@test.com', 'admin@test.com', '回复测试', '我收到了,谢谢!') ('user1@test.com', 'admin@test.com', '回复测试', '我收到了,谢谢!')
ON DUPLICATE KEY UPDATE id = id; ON DUPLICATE KEY UPDATE id = id;
-- 插入系统默认设置 -- 插入系统默认设置
INSERT INTO system_settings (setting_key, setting_value) VALUES INSERT INTO system_settings (setting_key, setting_value) VALUES
('smtp_port', '25'), ('smtp_port', '25'),
('pop3_port', '110'), ('pop3_port', '110'),
('domain', 'test.com'), ('domain', 'test.com'),
('mailbox_size_limit', '104857600'), -- 100MB ('mailbox_size_limit', '104857600'), -- 100MB
('smtp_enabled', '1'), ('smtp_enabled', '1'),
('pop3_enabled', '1'), ('pop3_enabled', '1'),
('log_path', '/var/log/mailserver'), ('log_path', '/var/log/mailserver'),
('log_max_size', '10485760') -- 10MB ('log_max_size', '10485760') -- 10MB
ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value); ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value);
-- 插入服务状态初始记录 -- 插入服务状态初始记录
INSERT INTO service_status (service_name, is_running) VALUES INSERT INTO service_status (service_name, is_running) VALUES
('smtp', 0), ('smtp', 0),
('pop3', 0) ('pop3', 0)
ON DUPLICATE KEY UPDATE service_name = VALUES(service_name); ON DUPLICATE KEY UPDATE service_name = VALUES(service_name);

@ -1,19 +1,18 @@
#!/usr/bin/env php #!/usr/bin/env php
<?php <?php
// 打印当前目录 require_once __DIR__ . '/../src/protocol/Pop3Server.php';
error_log('start_smtp.php CWD: ' . getcwd());
error_log('start_smtp.php __DIR__: ' . __DIR__); echo "启动最简POP3邮件服务器\n";
echo "==============================\n\n";
require_once __DIR__.'/../src/storage/SystemSettingsRepository.php';
require_once __DIR__ . '/../src/protocol/Pop3Server.php'; // 检查是否有权限监听110端口需要sudo
if (posix_getuid() != 0) {
$settingsRepo = new SystemSettingsRepository(); echo "错误需要管理员权限监听110端口\n";
// 获取端口设置(放在函数定义之前) echo "请使用sudo php " . __FILE__ . "\n";
$pop3Port = $settingsRepo->get('pop3_port', 110); echo "或者使用其他端口(需要修改代码)\n";
exit(1);
echo "启动最简POP3邮件服务器\n"; }
echo "==============================\n\n";
$server = new SimplePop3Server();
$server = new SimplePop3Server('0.0.0.0',$pop3Port); $server->start();
$server->start();
?> ?>

@ -1,29 +1,18 @@
#!/usr/bin/env php #!/usr/bin/env php
<?php <?php
require_once __DIR__ . '/../src/protocol/SmtpServer.php';
// 打印当前目录
error_log('start_smtp.php CWD: ' . getcwd()); echo "启动最简SMTP邮件服务器\n";
error_log('start_smtp.php __DIR__: ' . __DIR__); echo "==============================\n\n";
// 检查端口权限
require_once __DIR__.'/../src/storage/SystemSettingsRepository.php'; if (posix_getuid() != 0) {
require_once __DIR__ . '/../src/protocol/SmtpServer.php'; echo "错误需要管理员权限监听25端口\n";
echo "请使用sudo php " . __FILE__ . "\n";
$settingsRepo = new SystemSettingsRepository(); echo "或者使用其他端口(需要修改代码)\n";
// 获取端口设置(放在函数定义之前) exit(1);
$smtpPort = $settingsRepo->get('smtp_port', 25); }
echo "启动最简SMTP邮件服务器\n"; $server = new SimpleSmtpServer();
echo "==============================\n\n"; $server->start();
// 检查端口权限
/**if (posix_getuid() != 0) {
echo "错误:需要管理员权限监听".$smtpPort."端口\n";
echo "请使用sudo php " . __FILE__ . "\n";
echo "或者使用其他端口\n";
exit(1);
}**/
$server = new SimpleSmtpServer('0.0.0.0',$smtpPort);
$server->start();
?> ?>

@ -1,106 +1,106 @@
<?php <?php
/** /**
* 测试用户注册功能 * 测试用户注册功能
* 用法: php scripts/test_register.php * 用法: php scripts/test_register.php
*/ */
require_once __DIR__ . '/../config/database.php'; require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../src/storage/Database.php'; require_once __DIR__ . '/../src/storage/Database.php';
require_once __DIR__ . '/../src/storage/UserRepository.php'; require_once __DIR__ . '/../src/storage/UserRepository.php';
require_once __DIR__ . '/../src/utils/Validator.php'; require_once __DIR__ . '/../src/utils/Validator.php';
require_once __DIR__ . '/../src/utils/Security.php'; require_once __DIR__ . '/../src/utils/Security.php';
echo "=== 用户注册功能测试 ===\n\n"; echo "=== 用户注册功能测试 ===\n\n";
try { try {
$userRepo = new UserRepository(); $userRepo = new UserRepository();
// 测试1: 验证邮箱格式 // 测试1: 验证邮箱格式
echo "测试1: 验证邮箱格式\n"; echo "测试1: 验证邮箱格式\n";
$testEmails = [ $testEmails = [
'valid@test.com' => true, 'valid@test.com' => true,
'invalid-email' => false, 'invalid-email' => false,
'test@test.com' => true, 'test@test.com' => true,
'user@wrong.com' => false, 'user@wrong.com' => false,
]; ];
foreach ($testEmails as $email => $expected) { foreach ($testEmails as $email => $expected) {
$isValid = Validator::validateEmail($email); $isValid = Validator::validateEmail($email);
$domainValid = Validator::validateEmailDomain($email, 'test.com'); $domainValid = Validator::validateEmailDomain($email, 'test.com');
$result = $isValid && ($expected ? $domainValid : !$domainValid); $result = $isValid && ($expected ? $domainValid : !$domainValid);
echo " {$email}: " . ($result ? "✓" : "✗") . "\n"; echo " {$email}: " . ($result ? "✓" : "✗") . "\n";
} }
// 测试2: 验证密码强度 // 测试2: 验证密码强度
echo "\n测试2: 验证密码强度\n"; echo "\n测试2: 验证密码强度\n";
$testPasswords = [ $testPasswords = [
'12345' => false, // 太短 '12345' => false, // 太短
'123456' => true, // 符合最小长度 '123456' => true, // 符合最小长度
'password123' => true, 'password123' => true,
]; ];
foreach ($testPasswords as $password => $expected) { foreach ($testPasswords as $password => $expected) {
$validation = Validator::validatePassword($password, 6); $validation = Validator::validatePassword($password, 6);
$result = $validation['valid'] === $expected; $result = $validation['valid'] === $expected;
echo " '{$password}': " . ($result ? "✓" : "✗") . "\n"; echo " '{$password}': " . ($result ? "✓" : "✗") . "\n";
if (!$validation['valid']) { if (!$validation['valid']) {
echo " 错误: " . implode(', ', $validation['errors']) . "\n"; echo " 错误: " . implode(', ', $validation['errors']) . "\n";
} }
} }
// 测试3: 检查用户名是否存在 // 测试3: 检查用户名是否存在
echo "\n测试3: 检查用户名是否存在\n"; echo "\n测试3: 检查用户名是否存在\n";
$existingUser = $userRepo->findByUsername('admin@test.com'); $existingUser = $userRepo->findByUsername('admin@test.com');
if ($existingUser) { if ($existingUser) {
echo " admin@test.com 存在: ✓\n"; echo " admin@test.com 存在: ✓\n";
} else { } else {
echo " admin@test.com 不存在: ✗\n"; echo " admin@test.com 不存在: ✗\n";
} }
// 测试4: 创建测试用户(如果不存在) // 测试4: 创建测试用户(如果不存在)
echo "\n测试4: 创建测试用户\n"; echo "\n测试4: 创建测试用户\n";
$testUsername = 'testuser@test.com'; $testUsername = 'testuser@test.com';
if ($userRepo->usernameExists($testUsername)) { if ($userRepo->usernameExists($testUsername)) {
echo " 测试用户已存在,跳过创建\n"; echo " 测试用户已存在,跳过创建\n";
} else { } else {
try { try {
$newUser = $userRepo->create($testUsername, 'test123456', false, true); $newUser = $userRepo->create($testUsername, 'test123456', false, true);
echo " 创建用户成功: ✓\n"; echo " 创建用户成功: ✓\n";
echo " 用户ID: {$newUser['id']}\n"; echo " 用户ID: {$newUser['id']}\n";
echo " 用户名: {$newUser['username']}\n"; echo " 用户名: {$newUser['username']}\n";
echo " 是否管理员: " . ($newUser['is_admin'] ? '是' : '否') . "\n"; echo " 是否管理员: " . ($newUser['is_admin'] ? '是' : '否') . "\n";
} catch (Exception $e) { } catch (Exception $e) {
echo " 创建用户失败: ✗ - " . $e->getMessage() . "\n"; echo " 创建用户失败: ✗ - " . $e->getMessage() . "\n";
} }
} }
// 测试5: 验证密码 // 测试5: 验证密码
echo "\n测试5: 验证密码\n"; echo "\n测试5: 验证密码\n";
$testUser = $userRepo->findByUsername($testUsername); $testUser = $userRepo->findByUsername($testUsername);
if ($testUser) { if ($testUser) {
$verified = $userRepo->verifyPassword($testUsername, 'test123456'); $verified = $userRepo->verifyPassword($testUsername, 'test123456');
if ($verified) { if ($verified) {
echo " 密码验证成功: ✓\n"; echo " 密码验证成功: ✓\n";
} else { } else {
echo " 密码验证失败: ✗\n"; echo " 密码验证失败: ✗\n";
} }
} }
// 测试6: 获取所有用户 // 测试6: 获取所有用户
echo "\n测试6: 获取用户列表\n"; echo "\n测试6: 获取用户列表\n";
$users = $userRepo->getAll(10); $users = $userRepo->getAll(10);
echo " 用户总数: " . count($users) . "\n"; echo " 用户总数: " . count($users) . "\n";
foreach ($users as $user) { foreach ($users as $user) {
echo " - {$user['username']} (ID: {$user['id']}, " . echo " - {$user['username']} (ID: {$user['id']}, " .
($user['is_admin'] ? '管理员' : '普通用户') . ", " . ($user['is_admin'] ? '管理员' : '普通用户') . ", " .
($user['is_active'] ? '激活' : '禁用') . ")\n"; ($user['is_active'] ? '激活' : '禁用') . ")\n";
} }
echo "\n=== 测试完成 ===\n"; echo "\n=== 测试完成 ===\n";
} catch (Exception $e) { } catch (Exception $e) {
echo "错误: " . $e->getMessage() . "\n"; echo "错误: " . $e->getMessage() . "\n";
echo "堆栈跟踪:\n" . $e->getTraceAsString() . "\n"; echo "堆栈跟踪:\n" . $e->getTraceAsString() . "\n";
} }

@ -0,0 +1 @@
//处理管理请求

@ -1,150 +1,150 @@
<?php <?php
require_once __DIR__ . '/../storage/Database.php'; require_once __DIR__ . '/../storage/Database.php';
require_once __DIR__ . '/../storage/UserRepository.php'; require_once __DIR__ . '/../storage/UserRepository.php';
require_once __DIR__ . '/../storage/EmailRepository.php'; require_once __DIR__ . '/../storage/EmailRepository.php';
/** /**
* 群发邮件服务 * 群发邮件服务
*/ */
class BroadcastService { class BroadcastService {
private $db; private $db;
private $userRepo; private $userRepo;
private $emailRepo; private $emailRepo;
public function __construct() { public function __construct() {
$this->db = Database::getInstance(); $this->db = Database::getInstance();
$this->userRepo = new UserRepository(); $this->userRepo = new UserRepository();
$this->emailRepo = new EmailRepository(); $this->emailRepo = new EmailRepository();
} }
/** /**
* 群发邮件给所有用户 * 群发邮件给所有用户
* @param string $senderEmail 发件人邮箱 * @param string $senderEmail 发件人邮箱
* @param string $subject 主题 * @param string $subject 主题
* @param string $body 内容 * @param string $body 内容
* @return array ['success' => int, 'failed' => int, 'errors' => array] * @return array ['success' => int, 'failed' => int, 'errors' => array]
*/ */
public function broadcastToAll($senderEmail, $subject, $body) { public function broadcastToAll($senderEmail, $subject, $body) {
$users = $this->userRepo->getAll(); $users = $this->userRepo->getAll();
$sender = $this->userRepo->findByUsername($senderEmail); $sender = $this->userRepo->findByUsername($senderEmail);
if (!$sender) { if (!$sender) {
return ['success' => 0, 'failed' => 0, 'errors' => ['发件人不存在']]; return ['success' => 0, 'failed' => 0, 'errors' => ['发件人不存在']];
} }
$success = 0; $success = 0;
$failed = 0; $failed = 0;
$errors = []; $errors = [];
foreach ($users as $user) { foreach ($users as $user) {
// 跳过发件人自己 // 跳过发件人自己
if ($user['username'] === $senderEmail) { if ($user['username'] === $senderEmail) {
continue; continue;
} }
// 跳过禁用的用户 // 跳过禁用的用户
if (!$user['is_active']) { if (!$user['is_active']) {
continue; continue;
} }
try { try {
$this->sendEmail($sender['id'], $user['id'], $subject, $body); $this->sendEmail($sender['id'], $user['id'], $subject, $body);
$success++; $success++;
} catch (Exception $e) { } catch (Exception $e) {
$failed++; $failed++;
$errors[] = "发送给 {$user['username']} 失败: " . $e->getMessage(); $errors[] = "发送给 {$user['username']} 失败: " . $e->getMessage();
} }
} }
return [ return [
'success' => $success, 'success' => $success,
'failed' => $failed, 'failed' => $failed,
'errors' => $errors 'errors' => $errors
]; ];
} }
/** /**
* 群发邮件给指定用户列表 * 群发邮件给指定用户列表
* @param string $senderEmail 发件人邮箱 * @param string $senderEmail 发件人邮箱
* @param array $recipientEmails 收件人邮箱列表 * @param array $recipientEmails 收件人邮箱列表
* @param string $subject 主题 * @param string $subject 主题
* @param string $body 内容 * @param string $body 内容
* @return array ['success' => int, 'failed' => int, 'errors' => array] * @return array ['success' => int, 'failed' => int, 'errors' => array]
*/ */
public function broadcastToUsers($senderEmail, $recipientEmails, $subject, $body) { public function broadcastToUsers($senderEmail, $recipientEmails, $subject, $body) {
$sender = $this->userRepo->findByUsername($senderEmail); $sender = $this->userRepo->findByUsername($senderEmail);
if (!$sender) { if (!$sender) {
return ['success' => 0, 'failed' => 0, 'errors' => ['发件人不存在']]; return ['success' => 0, 'failed' => 0, 'errors' => ['发件人不存在']];
} }
$success = 0; $success = 0;
$failed = 0; $failed = 0;
$errors = []; $errors = [];
foreach ($recipientEmails as $email) { foreach ($recipientEmails as $email) {
$recipient = $this->userRepo->findByUsername(trim($email)); $recipient = $this->userRepo->findByUsername(trim($email));
if (!$recipient) { if (!$recipient) {
$failed++; $failed++;
$errors[] = "用户 {$email} 不存在"; $errors[] = "用户 {$email} 不存在";
continue; continue;
} }
if (!$recipient['is_active']) { if (!$recipient['is_active']) {
$failed++; $failed++;
$errors[] = "用户 {$email} 已被禁用"; $errors[] = "用户 {$email} 已被禁用";
continue; continue;
} }
try { try {
$this->sendEmail($sender['id'], $recipient['id'], $subject, $body); $this->sendEmail($sender['id'], $recipient['id'], $subject, $body);
$success++; $success++;
} catch (Exception $e) { } catch (Exception $e) {
$failed++; $failed++;
$errors[] = "发送给 {$email} 失败: " . $e->getMessage(); $errors[] = "发送给 {$email} 失败: " . $e->getMessage();
} }
} }
return [ return [
'success' => $success, 'success' => $success,
'failed' => $failed, 'failed' => $failed,
'errors' => $errors 'errors' => $errors
]; ];
} }
/** /**
* 发送邮件 * 发送邮件
* @param int $senderId 发件人ID * @param int $senderId 发件人ID
* @param int $recipientId 收件人ID * @param int $recipientId 收件人ID
* @param string $subject 主题 * @param string $subject 主题
* @param string $body 内容 * @param string $body 内容
* @return bool 是否成功 * @return bool 是否成功
*/ */
private function sendEmail($senderId, $recipientId, $subject, $body) { private function sendEmail($senderId, $recipientId, $subject, $body) {
$sender = $this->userRepo->findById($senderId); $sender = $this->userRepo->findById($senderId);
$recipient = $this->userRepo->findById($recipientId); $recipient = $this->userRepo->findById($recipientId);
if (!$sender || !$recipient) { if (!$sender || !$recipient) {
throw new Exception("发件人或收件人不存在"); throw new Exception("发件人或收件人不存在");
} }
$sizeBytes = strlen($subject) + strlen($body); $sizeBytes = strlen($subject) + strlen($body);
$stmt = $this->db->prepare(" $stmt = $this->db->prepare("
INSERT INTO emails (sender_id, recipient_id, sender, recipient, subject, body, size_bytes, created_at) INSERT INTO emails (sender_id, recipient_id, sender, recipient, subject, body, size_bytes, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, NOW()) VALUES (?, ?, ?, ?, ?, ?, ?, NOW())
"); ");
return $stmt->execute([ return $stmt->execute([
$senderId, $senderId,
$recipientId, $recipientId,
$sender['username'], $sender['username'],
$recipient['username'], $recipient['username'],
$subject, $subject,
$body, $body,
$sizeBytes $sizeBytes
]); ]);
} }
} }

@ -1,32 +0,0 @@
<?php
require_once __DIR__ . '/../storage/Database.php';
/**
* 一次性把 users.username 中 @oldDomain 批量替换成 @newDomain
*/
class SyncDomainService
{
private $db;
public function __construct()
{
$this->db = Database::getInstance();
}
/**
* @return int 实际被更新的用户数
*/
public function run(string $oldDomain, string $newDomain): int
{
// 防止注入,直接用 REPLACE 函数最省事
$sql = "UPDATE users
SET username = REPLACE(username, :old, :new)
WHERE username LIKE :like";
$stmt = $this->db->prepare($sql);
$stmt->execute([
':old' => '@' . $oldDomain,
':new' => '@' . $newDomain,
':like' => '%@' . $oldDomain
]);
return $stmt->rowCount();
}
}

@ -0,0 +1 @@
// 管理后台网页" - 网页界面

@ -1,326 +1,309 @@
<?php <?php
/** /**
* 最简POP3服务器 - 负责取信 * 最简POP3服务器 - 负责取信
* 运行php src/Pop3Server.php * 运行php src/Pop3Server.php
*/ */
require_once __DIR__ . '/../storage/MailboxRepository.php'; class SimplePop3Server
require_once __DIR__ . '/../storage/UserRepository.php'; {
require_once __DIR__ . '/../storage/FilterRepository.php'; private $socket;
require_once __DIR__ . '/../storage/SystemSettingsRepository.php'; private $isRunning = false;
private $db;
$settingsRepo = new SystemSettingsRepository(); private $currentUser = null;
// 获取端口设置(放在函数定义之前) private $currentUserId = null;
$smtpPort = $settingsRepo->get('smtp_port', 25); private $userEmails = [];
$pop3Port = $settingsRepo->get('pop3_port', 110); private $deletedEmails = []; // 存储待删除的邮件ID
class SimplePop3Server public function __construct($host = '0.0.0.0', $port = 110)
{ {
private $socket; echo "POP3服务器启动在 {$host}:{$port}\n";
private $isRunning = false; echo "按 Ctrl+C 停止\n\n";
private $db;
private $currentUser = null; $this->connectDB();
private $currentUserId = null; }
private $userEmails = [];
private $deletedEmails = []; // 存储待删除的邮件ID private function connectDB()
{
// 新增:存储主机和端口 try {
private $host; $this->db = new PDO(
private $port; 'mysql:host=127.0.0.1;port=3308;dbname=mail_server',
'mail_user',
public function __construct($host = '0.0.0.0', $port = 110) 'user123'
{ );
echo "POP3服务器启动在 {$host}:{$port}\n"; $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
echo "按 Ctrl+C 停止\n\n"; echo "数据库连接成功\n";
} catch (PDOException $e) {
$this->host = $host; echo "数据库连接失败: " . $e->getMessage() . "\n";
$this->port = $port; exit(1);
}
$this->connectDB(); }
}
public function start()
private function connectDB() {
{ $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
try { socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, 1);
$this->db = new PDO( socket_bind($this->socket, '0.0.0.0', 110);
'mysql:host=127.0.0.1;port=3308;dbname=mail_server', socket_listen($this->socket, 5);
'mail_user',
'user123' $this->isRunning = true;
);
$this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); while ($this->isRunning) {
echo "数据库连接成功\n"; $client = socket_accept($this->socket);
} catch (PDOException $e) { if ($client !== false) {
echo "数据库连接失败: " . $e->getMessage() . "\n"; $this->handleClient($client);
exit(1); socket_close($client);
} $this->currentUser = null; // 重置用户状态
} }
}
public function start() }
{
$this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); private function handleClient($client)
socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, 1); {
socket_bind($this->socket,$this->host, $this->port); // 获取客户端IP地址
socket_listen($this->socket, 5); socket_getpeername($client, $clientIp);
$clientIp = $clientIp ?: 'unknown';
$this->isRunning = true;
// 记录连接日志
while ($this->isRunning) { $this->log("客户端连接", $clientIp);
$client = socket_accept($this->socket);
if ($client !== false) { $this->send($client, "+OK POP3 Simple Server Ready");
$this->handleClient($client);
socket_close($client); $state = 'AUTH'; // 状态AUTH -> TRANSACTION -> UPDATE
$this->currentUser = null; // 重置用户状态 $this->deletedEmails = []; // 重置删除列表
}
} try {
} while (true) {
$input = socket_read($client, 1024);
private function handleClient($client) if ($input === false || trim($input) === '') {
{ break;
// 获取客户端IP地址 }
socket_getpeername($client, $clientIp);
$clientIp = $clientIp ?: 'unknown'; $input_trimmed = trim($input);
$command = strtoupper($input_trimmed);
// 记录连接日志 echo "客户端: {$input_trimmed}\n";
$this->log("客户端连接", $clientIp);
if ($state === 'AUTH') {
$this->send($client, "+OK POP3 Simple Server Ready"); // 认证阶段
if (strpos($command, 'USER ') === 0) {
$state = 'AUTH'; // 状态AUTH -> TRANSACTION -> UPDATE $username = trim(substr($input_trimmed, 5)); // 使用原始输入,保持大小写
$this->deletedEmails = []; // 重置删除列表 if ($this->userExists($username)) {
$this->currentUser = $username;
try { $this->send($client, "+OK User found");
while (true) { $this->log("用户认证: USER {$username}", $clientIp);
$input = socket_read($client, 1024); } else {
if ($input === false || trim($input) === '') { $this->send($client, "-ERR User not found");
break; $this->log("用户不存在: {$username}", $clientIp);
} }
} elseif (strpos($command, 'PASS ') === 0) {
$input_trimmed = trim($input); if ($this->currentUser) {
$command = strtoupper($input_trimmed); // 提取密码
//echo "客户端: {$input_trimmed}\n"; $password = substr($input, 5); // 保留原始大小写
$password = trim($password);
if ($state === 'AUTH') {
// 认证阶段 // 验证密码
if (strpos($command, 'USER ') === 0) { if ($this->verifyPassword($this->currentUser, $password)) {
$username = trim(substr($input_trimmed, 5)); // 使用原始输入,保持大小写 $this->loadUserEmails();
if ($this->userExists($username)) { $this->send($client, "+OK Logged in, {$this->userEmails['count']} messages");
$this->currentUser = $username; $state = 'TRANSACTION';
$this->send($client, "+OK User found"); $this->log("登录成功: {$this->currentUser}", $clientIp, $this->currentUserId);
$this->log("用户认证: USER {$username}", $clientIp); } else {
} else { $this->send($client, "-ERR Invalid password");
$this->send($client, "-ERR User not found"); $this->currentUser = null; // 重置用户状态
$this->log("用户不存在: {$username}", $clientIp); $this->log("密码错误: {$this->currentUser}", $clientIp);
} }
} elseif (strpos($command, 'PASS ') === 0) { } else {
if ($this->currentUser) { $this->send($client, "-ERR USER first");
// 提取密码 }
$password = substr($input, 5); // 保留原始大小写 } elseif ($command === 'QUIT') {
$password = trim($password); $this->send($client, "+OK Bye");
$this->log("客户端断开连接", $clientIp);
// 验证密码 break;
if ($this->verifyPassword($this->currentUser, $password)) { }
$this->loadUserEmails(); } elseif ($state === 'TRANSACTION') {
$this->send($client, "+OK Logged in, {$this->userEmails['count']} messages"); // 事务阶段
$state = 'TRANSACTION'; if ($command === 'STAT') {
$this->log("登录成功: {$this->currentUser}", $clientIp, $this->currentUserId); // 统计时排除已标记删除的邮件
} else { $activeCount = $this->userEmails['count'] - count($this->deletedEmails);
$this->send($client, "-ERR Invalid password"); $activeSize = $this->userEmails['total_size'];
$this->currentUser = null; // 重置用户状态 foreach ($this->deletedEmails as $deletedId) {
$this->log("密码错误: {$this->currentUser}", $clientIp); if (isset($this->userEmails['emails'][$deletedId])) {
} $activeSize -= $this->userEmails['emails'][$deletedId]['size'];
} else { }
$this->send($client, "-ERR USER first"); }
} $this->send($client, "+OK {$activeCount} {$activeSize}");
} elseif ($command === 'QUIT') { } elseif ($command === 'LIST') {
$this->send($client, "+OK Bye"); $activeCount = $this->userEmails['count'] - count($this->deletedEmails);
$this->log("客户端断开连接", $clientIp); $response = "+OK {$activeCount} messages\n";
break; foreach ($this->userEmails['emails'] as $id => $email) {
} // 跳过已标记删除的邮件
} elseif ($state === 'TRANSACTION') { if (!in_array($id, $this->deletedEmails)) {
// 事务阶段 $response .= "{$id} {$email['size']}\n";
if ($command === 'STAT') { }
// 统计时排除已标记删除的邮件 }
$activeCount = $this->userEmails['count'] - count($this->deletedEmails); $response .= ".";
$activeSize = $this->userEmails['total_size']; $this->send($client, $response);
foreach ($this->deletedEmails as $deletedId) { } elseif (strpos($command, 'RETR ') === 0) {
if (isset($this->userEmails['emails'][$deletedId])) { $msgId = intval(substr($command, 5));
$activeSize -= $this->userEmails['emails'][$deletedId]['size']; if (isset($this->userEmails['emails'][$msgId]) && !in_array($msgId, $this->deletedEmails)) {
} $email = $this->userEmails['emails'][$msgId];
} $response = "+OK {$email['size']} octets\n";
$this->send($client, "+OK {$activeCount} {$activeSize}"); $response .= "From: {$email['sender']}\n";
} elseif ($command === 'LIST') { $response .= "To: {$email['recipient']}\n";
$activeCount = $this->userEmails['count'] - count($this->deletedEmails); $response .= "Subject: {$email['subject']}\n";
$response = "+OK {$activeCount} messages\n"; $response .= "Date: {$email['date']}\n\n";
foreach ($this->userEmails['emails'] as $id => $email) { $response .= $email['body'] . "\n.";
// 跳过已标记删除的邮件 $this->send($client, $response);
if (!in_array($id, $this->deletedEmails)) { $this->log("读取邮件: ID {$msgId}", $clientIp, $this->currentUserId);
$response .= "{$id} {$email['size']}\n"; } else {
} $this->send($client, "-ERR No such message");
} }
$response .= "."; } elseif (strpos($command, 'DELE ') === 0) {
$this->send($client, $response); // 删除邮件命令
} elseif (strpos($command, 'RETR ') === 0) { $msgId = intval(substr($command, 5));
$msgId = intval(substr($command, 5)); if (isset($this->userEmails['emails'][$msgId]) && !in_array($msgId, $this->deletedEmails)) {
if (isset($this->userEmails['emails'][$msgId]) && !in_array($msgId, $this->deletedEmails)) { $this->deletedEmails[] = $msgId;
$email = $this->userEmails['emails'][$msgId]; $this->send($client, "+OK Message {$msgId} deleted");
$response = "+OK {$email['size']} octets\n"; $this->log("标记删除邮件: ID {$msgId}", $clientIp, $this->currentUserId);
$response .= "From: {$email['sender']}\n"; } else {
$response .= "To: {$email['recipient']}\n"; $this->send($client, "-ERR No such message");
$response .= "Subject: {$email['subject']}\n"; }
$response .= "Date: {$email['date']}\n\n"; } elseif ($command === 'QUIT') {
$response .= $email['body'] . "\n."; // 在QUIT时执行实际删除
$this->send($client, $response); $this->processDeletions();
$this->log("读取邮件: ID {$msgId}", $clientIp, $this->currentUserId); $this->send($client, "+OK Bye");
} else { $this->log("客户端断开连接", $clientIp, $this->currentUserId);
$this->send($client, "-ERR No such message"); $state = 'UPDATE';
} break;
} elseif (strpos($command, 'DELE ') === 0) { } else {
// 删除邮件命令 $this->send($client, "-ERR Unknown command");
$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"); } catch (Exception $e) {
$this->log("标记删除邮件: ID {$msgId}", $clientIp, $this->currentUserId); $this->log("处理客户端错误: " . $e->getMessage(), $clientIp);
} else { } finally {
$this->send($client, "-ERR No such message"); // 重置状态
} $this->currentUser = null;
} elseif ($command === 'QUIT') { $this->currentUserId = null;
// 在QUIT时执行实际删除 $this->deletedEmails = [];
$this->processDeletions(); }
$this->send($client, "+OK Bye"); }
$this->log("客户端断开连接", $clientIp, $this->currentUserId);
$state = 'UPDATE'; private function send($client, $message)
break; {
} else { socket_write($client, $message . "\r\n");
$this->send($client, "-ERR Unknown command"); echo "服务器: {$message}\n";
} }
}
} private function userExists($username)
} catch (Exception $e) { {
$this->log("处理客户端错误: " . $e->getMessage(), $clientIp); $stmt = $this->db->prepare("SELECT id FROM users WHERE username = ? AND is_active = 1");
} finally { $stmt->execute([$username]);
// 重置状态 return $stmt->rowCount() > 0;
$this->currentUser = null; }
$this->currentUserId = null;
$this->deletedEmails = []; private function verifyPassword($username, $password)
} {
} try {
$stmt = $this->db->prepare("SELECT password_hash FROM users WHERE username = ? AND is_active = 1");
private function send($client, $message) $stmt->execute([$username]);
{ $user = $stmt->fetch(PDO::FETCH_ASSOC);
socket_write($client, $message . "\r\n");
echo "服务器: {$message}\n"; if ($user && isset($user['password_hash'])) {
} return password_verify($password, $user['password_hash']);
}
private function userExists($username)
{ return false;
$stmt = $this->db->prepare("SELECT id FROM users WHERE username = ? AND is_active = 1"); } catch (Exception $e) {
$stmt->execute([$username]); echo "密码验证错误: " . $e->getMessage() . "\n";
return $stmt->rowCount() > 0; return false;
} }
}
private function verifyPassword($username, $password)
{ private function loadUserEmails()
try { {
$stmt = $this->db->prepare("SELECT password_hash FROM users WHERE username = ? AND is_active = 1"); $stmt = $this->db->prepare(
$stmt->execute([$username]); "SELECT id, sender, recipient, subject, body, created_at, size_bytes
$user = $stmt->fetch(PDO::FETCH_ASSOC); FROM emails
WHERE recipient = ? AND is_deleted = 0
if ($user && isset($user['password_hash'])) { ORDER BY created_at"
return password_verify($password, $user['password_hash']); );
} $stmt->execute([$this->currentUser]);
return false; $this->userEmails = ['count' => 0, 'total_size' => 0, 'emails' => []];
} catch (Exception $e) {
echo "密码验证错误: " . $e->getMessage() . "\n"; while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
return false; $id = $row['id'];
} // 使用数据库中的size_bytes如果没有则估算
} $size = $row['size_bytes'] > 0 ? $row['size_bytes'] : (strlen($row['body']) + 100);
private function loadUserEmails() $this->userEmails['emails'][$id] = [
{ 'sender' => $row['sender'],
$stmt = $this->db->prepare( 'recipient' => $row['recipient'],
"SELECT id, sender, recipient, subject, body, created_at, size_bytes 'subject' => $row['subject'],
FROM emails 'body' => $row['body'],
WHERE recipient = ? AND is_deleted = 0 'date' => $row['created_at'],
ORDER BY created_at" 'size' => $size
); ];
$stmt->execute([$this->currentUser]);
$this->userEmails['count']++;
$this->userEmails = ['count' => 0, 'total_size' => 0, 'emails' => []]; $this->userEmails['total_size'] += $size;
}
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$id = $row['id']; // 获取用户ID
// 使用数据库中的size_bytes如果没有则估算 $userStmt = $this->db->prepare("SELECT id FROM users WHERE username = ?");
$size = $row['size_bytes'] > 0 ? $row['size_bytes'] : (strlen($row['body']) + 100); $userStmt->execute([$this->currentUser]);
$user = $userStmt->fetch();
$this->userEmails['emails'][$id] = [ $this->currentUserId = $user ? $user['id'] : null;
'sender' => $row['sender'],
'recipient' => $row['recipient'], echo "为用户 {$this->currentUser} 加载了 {$this->userEmails['count']} 封邮件\n";
'subject' => $row['subject'], }
'body' => $row['body'],
'date' => $row['created_at'], /**
'size' => $size * 处理删除操作在QUIT时执行
]; */
private function processDeletions()
$this->userEmails['count']++; {
$this->userEmails['total_size'] += $size; if (empty($this->deletedEmails)) {
} return;
}
// 获取用户ID
$userStmt = $this->db->prepare("SELECT id FROM users WHERE username = ?"); try {
$userStmt->execute([$this->currentUser]); $placeholders = implode(',', array_fill(0, count($this->deletedEmails), '?'));
$user = $userStmt->fetch(); $stmt = $this->db->prepare(
$this->currentUserId = $user ? $user['id'] : null; "UPDATE emails SET is_deleted = 1 WHERE id IN ({$placeholders}) AND recipient = ?"
);
echo "为用户 {$this->currentUser} 加载了 {$this->userEmails['count']} 封邮件\n"; $params = array_merge($this->deletedEmails, [$this->currentUser]);
} $stmt->execute($params);
/** $deletedCount = $stmt->rowCount();
* 处理删除操作在QUIT时执行 echo "删除了 {$deletedCount} 封邮件\n";
*/ } catch (Exception $e) {
private function processDeletions() echo "删除邮件失败: " . $e->getMessage() . "\n";
{ error_log("删除邮件错误: " . $e->getMessage());
if (empty($this->deletedEmails)) { }
return; }
}
/**
try { * 记录日志到数据库
$placeholders = implode(',', array_fill(0, count($this->deletedEmails), '?')); */
$stmt = $this->db->prepare( private function log($message, $clientIp = 'unknown', $userId = null)
"UPDATE emails SET is_deleted = 1 WHERE id IN ({$placeholders}) AND recipient = ?" {
); try {
$params = array_merge($this->deletedEmails, [$this->currentUser]); $stmt = $this->db->prepare(
$stmt->execute($params); "INSERT INTO server_logs (log_type, message, client_ip, user_id) VALUES (?, ?, ?, ?)"
);
$deletedCount = $stmt->rowCount(); $stmt->execute(['POP3', $message, $clientIp, $userId]);
echo "删除了 {$deletedCount} 封邮件\n"; } catch (Exception $e) {
} catch (Exception $e) { // 日志记录失败不影响主流程
echo "删除邮件失败: " . $e->getMessage() . "\n"; error_log("日志记录失败: " . $e->getMessage());
error_log("删除邮件错误: " . $e->getMessage()); }
} }
} }
/** // 如果直接运行这个文件
* 记录日志到数据库 if (basename(__FILE__) == basename($_SERVER['PHP_SELF'])) {
*/ $server = new SimplePop3Server();
private function log($message, $clientIp = 'unknown', $userId = null) $server->start();
{ }
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('0.0.0.0',$pop3Port);
$server->start();
}
?> ?>

@ -0,0 +1,220 @@
<?php
require_once __DIR__ . '/../../config/database.php';
require_once __DIR__ . '/../storage/Database.php';
class SmtpHandler {
private $state = 'WAIT_HELO'; // 状态: WAIT_HELO, WAIT_MAIL, WAIT_RCPT, WAIT_DATA, IN_DATA
private $from = '';
private $to = [];
private $data = '';
private $currentUser = null;
public function processCommand($command) {
$command = strtoupper(trim($command));
switch ($this->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 <CRLF>.<CRLF>";
} 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:<user@domain.com>" 中提取邮箱
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;
}
}

@ -1,462 +1,445 @@
<?php <?php
/** // 设置PHP内部编码为UTF-8解决乱码的关键
* 一体化的SMTP服务器 mb_internal_encoding('UTF-8');
* mb_http_output('UTF-8');
* 注意:本类同时包含: if (function_exists('iconv_set_encoding')) {
* 1. 网络层Server功能- 负责socket通信 iconv_set_encoding("internal_encoding", "UTF-8");
* 2. 协议层Handler功能- 负责SMTP协议处理 iconv_set_encoding("output_encoding", "UTF-8");
* }
* 这种设计对于教学项目是合适的,保持了简单性。 // 设置默认时区
* 生产环境可以考虑拆分为 SmtpServer + SmtpHandler。 date_default_timezone_set('Asia/Shanghai');
*/
/**
ini_set('default_charset', 'UTF-8'); * 最简SMTP服务器 - 负责收信
mb_internal_encoding('UTF-8'); * 运行php src/SmtpServer.php
*/
/**
* 最简SMTP服务器 - 负责收信 require_once __DIR__ . '/../storage/FilterRepository.php';
* 运行php src/SmtpServer.php require_once __DIR__ . '/../storage/MailboxRepository.php';
*/ require_once __DIR__ . '/../storage/UserRepository.php';
require_once __DIR__ . '/../storage/MailboxRepository.php'; class SimpleSmtpServer
require_once __DIR__ . '/../storage/UserRepository.php'; {
require_once __DIR__ . '/../storage/FilterRepository.php'; private $socket;
require_once __DIR__ . '/../storage/SystemSettingsRepository.php'; private $isRunning = false;
private $db; // 数据库连接
$settingsRepo = new SystemSettingsRepository(); private $filterRepo;
// 获取端口设置(放在函数定义之前) private $mailboxRepo;
$smtpPort = $settingsRepo->get('smtp_port', 25); private $userRepo;
$pop3Port = $settingsRepo->get('pop3_port', 110);
public function __construct($host = '0.0.0.0', $port = 25)
class SimpleSmtpServer {
{ echo "SMTP服务器启动在 {$host}:{$port}\n";
private $socket; echo "按 Ctrl+C 停止\n\n";
private $isRunning = false;
private $db; // 数据库连接 // 连接数据库
private $filterRepo; $this->connectDB();
private $mailboxRepo;
private $userRepo; // 初始化Repository
$this->filterRepo = new FilterRepository();
// 新增:存储主机和端口 $this->mailboxRepo = new MailboxRepository();
private $host; $this->userRepo = new UserRepository();
private $port; }
private $clientIp;
private function connectDB()
public function __construct($host = '0.0.0.0', $port = 25) {
{ try {
echo "SMTP服务器启动在 {$host}:{$port}\n"; $this->db = new PDO(
echo "按 Ctrl+C 停止\n\n"; 'mysql:host=127.0.0.1;port=3308;dbname=mail_server',
'mail_user',
// 存储配置,而不是立即使用 'user123',
$this->host = $host; [
$this->port = $port; PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci",
// 连接数据库 PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
$this->connectDB(); PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]
// 初始化Repository );
$this->filterRepo = new FilterRepository(); // $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->mailboxRepo = new MailboxRepository(); echo "数据库连接成功\n";
$this->userRepo = new UserRepository(); } catch (PDOException $e) {
} echo "数据库连接失败: " . $e->getMessage() . "\n";
exit(1);
private function connectDB() }
{ }
try {
$this->db = new PDO( public function start()
'mysql:host=127.0.0.1;port=3308;dbname=mail_server', {
'mail_user', // 创建socket邮局开门
'user123', $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
[ socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, 1);
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci", socket_bind($this->socket, '0.0.0.0', 25);
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, socket_listen($this->socket, 5);
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
] $this->isRunning = true;
);
// $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); while ($this->isRunning) {
echo "数据库连接成功\n"; // 等待客户连接(有人来寄信)
} catch (PDOException $e) { $client = socket_accept($this->socket);
echo "数据库连接失败: " . $e->getMessage() . "\n"; if ($client !== false) {
exit(1); $this->handleClient($client);
} socket_close($client);
} }
}
public function start()
{ socket_close($this->socket);
// 创建socket邮局开门 }
$this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, 1); private function handleClient($client)
socket_bind($this->socket, '0.0.0.0',$this->port); {
socket_listen($this->socket, 5); // 获取客户端IP地址
socket_getpeername($client, $clientIp);
$this->isRunning = true; $clientIp = $clientIp ?: 'unknown';
while ($this->isRunning) { // 记录连接日志
// 等待客户连接(有人来寄信) $this->log("客户端连接", $clientIp);
$client = socket_accept($this->socket);
if ($client !== false) { try {
$this->handleClient($client); // 1. 说欢迎语
socket_close($client); $this->send($client, "220 mail.simple.com SMTP Ready");
}
} // 2. 等待客户说 HELO
$this->waitForCommand($client, 'HELO', 'HELO或EHLO');
socket_close($this->socket);
} // 3. 检查IP过滤规则
if ($this->filterRepo->isIPBlocked($clientIp)) {
private function handleClient($client) $this->send($client, "550 IP address blocked");
{ $this->log("IP被阻止: {$clientIp}", $clientIp);
// 获取客户端IP地址 return;
socket_getpeername($client, $clientIp); }
$clientIp = $clientIp ?: 'unknown';
$this->clientIp = $clientIp; $this->send($client, "250 OK - Hello");
// 记录连接日志
$this->log("客户端连接",$this->clientIp); // 4. 问:谁寄的?
$from = $this->waitForCommand($client, 'MAIL FROM:', '发件人邮箱');
try {
// 1. 说欢迎语 // 检查发件人邮箱过滤规则
$this->send($client, "220 mail.simple.com SMTP Ready"); if ($this->filterRepo->isEmailBlocked($from)) {
$this->send($client, "550 Sender email blocked");
// 2. 等待客户说 HELO $this->log("发件人邮箱被阻止: {$from}", $clientIp);
$this->waitForCommand($client, 'HELO', 'HELO或EHLO'); return;
}
// 3. 检查IP过滤规则
if ($this->filterRepo->isIPBlocked($clientIp)) { $this->send($client, "250 Sender OK");
$this->send($client, "550 IP address blocked");
$this->log("IP被阻止: {$clientIp}", $clientIp); // 5. 支持多收件人
return; $recipients = [];
} $this->send($client, "250 Recipient OK");
$this->send($client, "250 OK - Hello"); // 循环接收多个RCPT TO命令
while (true) {
// 4. 问:谁寄的? $input = socket_read($client, 1024);
$from = $this->waitForCommand($client, 'MAIL FROM:', '发件人邮箱'); if ($input === false) break;
// 检查发件人邮箱过滤规则 $input = trim($input);
if ($this->filterRepo->isEmailBlocked($from)) { echo "客户端: {$input}\n";
$this->send($client, "550 Sender email blocked");
$this->log("发件人邮箱被阻止: {$from}", $clientIp); if (stripos($input, 'RCPT TO:') === 0) {
return; // 提取收件人邮箱
} if (preg_match('/<(.+?)>/', $input, $matches)) {
$to = $matches[1];
$this->send($client, "250 Sender OK");
// 检查收件人邮箱过滤规则
// 5. 支持多收件人 if ($this->filterRepo->isEmailBlocked($to)) {
$recipients = []; $this->send($client, "550 Recipient email blocked");
$this->send($client, "250 Recipient OK"); $this->log("收件人邮箱被阻止: {$to}", $clientIp);
continue;
// 循环接收多个RCPT TO命令 }
while (true) {
$input = socket_read($client, 1024); // 初步检查收件人邮箱大小限制(使用估算值,实际检查在接收邮件内容后)
if ($input === false) break; $user = $this->userRepo->findByUsername($to);
if ($user) {
$input = trim($input); $usage = $this->mailboxRepo->getUsage($user['id']);
//echo "客户端: {$input}\n"; $estimatedSize = 50000; // 估算邮件大小50KB
if (stripos($input, 'RCPT TO:') === 0) { if ($usage['used'] + $estimatedSize > $usage['limit']) {
// 提取收件人邮箱 $this->send($client, "552 Mailbox full");
if (preg_match('/<(.+?)>/', $input, $matches)) { $this->log("收件人邮箱已满(初步检查): {$to}", $clientIp);
$to = $matches[1]; continue;
// 新增:收件人过滤 }
if ($this->filterRepo->isEmailBlocked($to)) { }
$this->send($client, "550 Recipient email blocked");
$this->log("收件人邮箱被过滤: {$to}", $this->clientIp); $recipients[] = $to;
continue; // 直接拒绝,不加入 $recipients $this->send($client, "250 Recipient OK");
} }
} elseif (stripos($input, 'DATA') === 0) {
// 初步检查收件人邮箱大小限制(使用估算值,实际检查在接收邮件内容后) // 收到DATA命令跳出循环
$user = $this->userRepo->findByUsername($to); break;
if ($user) { } elseif (strtoupper($input) === 'QUIT') {
$usage = $this->mailboxRepo->getUsage($user['id']); $this->send($client, "221 Bye");
$estimatedSize = 50000; // 估算邮件大小50KB return;
} else {
if ($usage['used'] + $estimatedSize > $usage['limit']) { $this->send($client, "500 Error: Expected RCPT TO or DATA");
$this->send($client, "552 Mailbox full"); }
$this->log("收件人邮箱已满(初步检查): {$to}", $clientIp); }
continue;
} if (empty($recipients)) {
} $this->send($client, "503 No valid recipients");
$this->log("没有有效收件人", $clientIp);
$recipients[] = $to; return;
$this->send($client, "250 Recipient OK"); }
}
} elseif (stripos($input, 'DATA') === 0) { // 6. 说:开始写内容吧
// 收到DATA命令跳出循环 $this->send($client, "354 Start mail input, end with <CRLF>.<CRLF>");
break;
} elseif (strtoupper($input) === 'QUIT') { // 7. 接收邮件内容
$this->send($client, "221 Bye"); $emailContent = $this->receiveEmailContent($client);
return; $emailSize = strlen($emailContent);
} else {
$this->send($client, "500 Error: Expected RCPT TO or DATA"); // 8. 再次检查邮箱大小限制(使用实际邮件大小)
} $validRecipients = [];
} foreach ($recipients as $to) {
$user = $this->userRepo->findByUsername($to);
if (empty($recipients)) { if ($user) {
$this->send($client, "503 No valid recipients"); $usage = $this->mailboxRepo->getUsage($user['id']);
$this->log("没有有效收件人", $clientIp); if ($usage['used'] + $emailSize > $usage['limit']) {
return; $this->log("收件人邮箱已满(实际检查): {$to}", $clientIp);
} continue;
}
// 6. 说:开始写内容吧 }
$this->send($client, "354 Start mail input, end with <CRLF>.<CRLF>"); $validRecipients[] = $to;
}
// 7. 接收邮件内容
$emailContent = $this->receiveEmailContent($client); if (empty($validRecipients)) {
$emailSize = strlen($emailContent); $this->send($client, "552 All recipients' mailboxes are full");
$this->log("所有收件人邮箱已满", $clientIp);
// 8. 再次检查邮箱大小限制(使用实际邮件大小) return;
$validRecipients = []; }
foreach ($recipients as $to) {
$user = $this->userRepo->findByUsername($to); // 9. 保存到数据库(支持多收件人)
if ($user) { $successCount = 0;
$usage = $this->mailboxRepo->getUsage($user['id']); foreach ($validRecipients as $to) {
if ($usage['used'] + $emailSize > $usage['limit']) { if ($this->saveEmail($from, $to, $emailContent, $clientIp, $emailSize)) {
$this->log("收件人邮箱已满(实际检查): {$to}", $clientIp); $successCount++;
continue; }
} }
}
$validRecipients[] = $to; // 10. 告诉客户:收到了
} if ($successCount > 0) {
$this->send($client, "250 Mail accepted");
if (empty($validRecipients)) { $this->log("邮件发送成功: {$from} -> " . implode(', ', $validRecipients) . " ({$successCount}个收件人)", $clientIp);
$this->send($client, "552 All recipients' mailboxes are full"); } else {
$this->log("所有收件人邮箱已满", $clientIp); $this->send($client, "550 Mail delivery failed");
return; $this->log("邮件发送失败: {$from} -> " . implode(', ', $validRecipients), $clientIp);
} }
// 9. 保存到数据库(支持多收件人) // 11. 等客户说再见
$successCount = 0; $this->waitForCommand($client, 'QUIT', 'QUIT');
foreach ($validRecipients as $to) { $this->send($client, "221 Bye");
if ($this->saveEmail($from, $to, $emailContent, $this->clientIp, $emailSize)) {
$successCount++; echo "收到一封邮件:{$from} -> " . implode(', ', $validRecipients) . "\n";
}
} } catch (Exception $e) {
$this->log("处理客户端错误: " . $e->getMessage(), $clientIp);
// 10. 告诉客户:收到了 $this->send($client, "500 Internal server error");
if ($successCount > 0) { }
$this->send($client, "250 Mail accepted"); }
$this->log("邮件发送成功: {$from} -> " . implode(', ', $validRecipients) . " ({$successCount}个收件人)", $clientIp);
} else { private function send($client, $message)
$this->send($client, "550 Mail delivery failed"); {
$this->log("邮件发送失败: {$from} -> " . implode(', ', $validRecipients), $clientIp); socket_write($client, $message . "\r\n");
} }
// 11. 等客户说再见 private function waitForCommand($client, $expected, $description)
$this->waitForCommand($client, 'QUIT', 'QUIT'); {
$this->send($client, "221 Bye"); while (true) {
$input = socket_read($client, 1024);
echo "收到一封邮件:{$from} -> " . implode(', ', $validRecipients) . "\n"; if ($input === false) break;
} catch (Exception $e) { $input = trim($input);
$this->log("处理客户端错误: " . $e->getMessage(), $clientIp); echo "客户端: {$input}\n";
$this->send($client, "500 Internal server error");
} if (stripos($input, $expected) === 0) {
} // 提取邮箱地址
if (preg_match('/<(.+?)>/', $input, $matches)) {
private function send($client, $message) return $matches[1];
{ }
socket_write($client, $message . "\r\n"); return $input;
} }
private function waitForCommand($client, $expected, $description) // 如果收到QUIT直接退出
{ if (strtoupper($input) === 'QUIT') {
while (true) { $this->send($client, "221 Bye");
$input = socket_read($client, 1024); exit(0);
if ($input === false) break; }
$input = trim($input); $this->send($client, "500 Error: Expected {$description}");
//echo "客户端: {$input}\n"; }
}
if (stripos($input, $expected) === 0) {
// 提取邮箱地址 private function receiveEmailContent($client)
if (preg_match('/<(.+?)>/', $input, $matches)) { {
return $matches[1]; $content = "";
}
return $input; $buffer = "";
}
while (true) {
// 如果收到QUIT直接退出 $data = socket_read($client, 1024, PHP_BINARY_READ);
if (strtoupper($input) === 'QUIT') { if ($data === false || $data === '') {
$this->send($client, "221 Bye"); break;
exit(0); }
}
$buffer .= $data;
$this->send($client, "500 Error: Expected {$description}");
} // 按行处理
} while (($pos = strpos($buffer, "\r\n")) !== false) {
$line = substr($buffer, 0, $pos);
private function receiveEmailContent($client) $buffer = substr($buffer, $pos + 2);
{
$content = ""; // 如果遇到单独一行的 '.' 就结束
if (trim($line) === '.') {
$buffer = ""; return $content;
}
while (true) {
$data = socket_read($client, 1024, PHP_BINARY_READ); $content .= $line . "\r\n";
if ($data === false || $data === '') { }
break; }
}
return $content;
$buffer .= $data; }
// 按行处理 private function saveEmail($from, $to, $content, $clientIp = 'unknown', $emailSize = null)
while (($pos = strpos($buffer, "\r\n")) !== false) { {
$line = substr($buffer, 0, $pos); try {
$buffer = substr($buffer, $pos + 2); // 1. 解析邮件内容(先不转换编码)
$lines = explode("\r\n", $content);
// 如果遇到单独一行的 '.' 就结束 $subject = "无主题";
if (trim($line) === '.') { $body = "";
return $content; $inBody = false;
}
foreach ($lines as $line) {
$content .= $line . "\r\n"; if (!$inBody && stripos($line, 'Subject:') === 0) {
} $subject = trim(substr($line, 8));
} // 解码MIME编码的主题
$subject = $this->decodeMimeHeader($subject);
return $content; }
}
if (!$inBody && trim($line) === '') {
private function saveEmail($from, $to, $content, $clientIp = 'unknown', $emailSize = null) $inBody = true;
{ continue;
try { }
// 1. 解析邮件内容(先不转换编码)
$lines = explode("\r\n", $content); if ($inBody) {
$subject = "无主题"; $body .= $line . "\n";
$body = ""; }
$inBody = false; }
foreach ($lines as $line) { // 2. 清理正文
if (!$inBody && stripos($line, 'Subject:') === 0) { $body = trim($body);
$subject = trim(substr($line, 8));
// 解码MIME编码的主题 // 3. 检测当前编码 - 直接假设为UTF-8
$subject = $this->decodeMimeHeader($subject); $detectedEncoding = 'UTF-8'; // 直接指定,不检测了
} echo "使用编码: {$detectedEncoding}\n";
if (!$inBody && trim($line) === '') { // 4. 验证确实是UTF-8如果不是就转换
$inBody = true; if (!mb_check_encoding($subject, 'UTF-8')) {
continue; $subject = mb_convert_encoding($subject, 'UTF-8', 'auto');
} }
if (!mb_check_encoding($body, 'UTF-8')) {
if ($inBody) { $body = mb_convert_encoding($body, 'UTF-8', 'auto');
$body .= $line . "\n"; }
}
} // 5. 计算邮件大小(如果未提供则计算)
if ($emailSize === null) {
// 2. 清理正文 $emailSize = strlen($content);
$body = trim($body); }
// 3. 检测当前编码 - 直接假设为UTF-8 // 6. 获取收件人用户ID
$detectedEncoding = 'UTF-8'; // 直接指定,不检测了 $user = $this->userRepo->findByUsername($to);
echo "使用编码: {$detectedEncoding}\n"; $recipientId = $user ? $user['id'] : null;
// 4. 验证确实是UTF-8如果不是就转换 // 7. 使用参数绑定确保UTF-8传输
if (!mb_check_encoding($subject, 'UTF-8')) { $stmt = $this->db->prepare(
$subject = mb_convert_encoding($subject, 'UTF-8', 'auto'); "INSERT INTO emails (sender, recipient, recipient_id, subject, body, size_bytes) VALUES (?, ?, ?, ?, ?, ?)"
} );
if (!mb_check_encoding($body, 'UTF-8')) {
$body = mb_convert_encoding($body, 'UTF-8', 'auto'); // 绑定参数时指定字符集
} $stmt->bindValue(1, $from, PDO::PARAM_STR);
$stmt->bindValue(2, $to, PDO::PARAM_STR);
// 5. 计算邮件大小(如果未提供则计算) $stmt->bindValue(3, $recipientId, PDO::PARAM_INT);
if ($emailSize === null) { $stmt->bindValue(4, $subject, PDO::PARAM_STR);
$emailSize = strlen($content); $stmt->bindValue(5, $body, PDO::PARAM_STR);
} $stmt->bindValue(6, $emailSize, PDO::PARAM_INT);
// 6. 获取收件人用户ID $stmt->execute();
$user = $this->userRepo->findByUsername($to);
$recipientId = $user ? $user['id'] : null; echo "邮件保存成功:{$from} -> {$to}\n";
echo " 主题: {$subject}\n";
// 7. 使用参数绑定确保UTF-8传输 echo " 长度: {$emailSize} 字节\n";
$stmt = $this->db->prepare(
"INSERT INTO emails (sender, recipient, recipient_id, subject, body, size_bytes) VALUES (?, ?, ?, ?, ?, ?)" return true;
);
} catch (Exception $e) {
// 绑定参数时指定字符集 echo "保存邮件失败: " . $e->getMessage() . "\n";
$stmt->bindValue(1, $from, PDO::PARAM_STR); error_log("邮件保存错误: " . $e->getMessage() . "\n" . $e->getTraceAsString());
$stmt->bindValue(2, $to, PDO::PARAM_STR); $this->log("保存邮件失败: " . $e->getMessage(), $clientIp);
$stmt->bindValue(3, $recipientId, PDO::PARAM_INT); return false;
$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"; private function log($message, $clientIp = 'unknown', $userId = null)
echo " 主题: {$subject}\n"; {
echo " 长度: {$emailSize} 字节\n"; try {
$stmt = $this->db->prepare(
return true; "INSERT INTO server_logs (log_type, message, client_ip, user_id) VALUES (?, ?, ?, ?)"
);
} catch (Exception $e) { $stmt->execute(['SMTP', $message, $clientIp, $userId]);
echo "保存邮件失败: " . $e->getMessage() . "\n"; } catch (Exception $e) {
error_log("邮件保存错误: " . $e->getMessage() . "\n" . $e->getTraceAsString()); // 日志记录失败不影响主流程
$this->log("保存邮件失败: " . $e->getMessage(), $clientIp); error_log("日志记录失败: " . $e->getMessage());
return false; }
} }
}
// 添加MIME头解码方法
/** private function decodeMimeHeader($header)
* 记录日志到数据库 {
*/ // 处理 =?UTF-8?B?5L2g5aW9?= 这样的MIME编码
private function log($message, $clientIp = 'unknown', $userId = null) $decoded = '';
{ $parts = preg_split('/(=\?[^?]+\?[BQ]\?[^?]+\?=)/i', $header, -1, PREG_SPLIT_DELIM_CAPTURE);
try {
$stmt = $this->db->prepare( foreach ($parts as $part) {
"INSERT INTO server_logs (log_type, message, client_ip, user_id) VALUES (?, ?, ?, ?)" if (preg_match('/=\?([^\?]+)\?([BQ])\?([^\?]+)\?=/i', $part, $matches)) {
); $charset = $matches[1];
$stmt->execute(['SMTP', $message, $clientIp, $userId]); $encoding = strtoupper($matches[2]);
} catch (Exception $e) { $text = $matches[3];
// 日志记录失败不影响主流程
error_log("日志记录失败: " . $e->getMessage()); if ($encoding === 'B') {
} // Base64解码
} $decodedText = base64_decode($text);
} elseif ($encoding === 'Q') {
// 添加MIME头解码方法 // Quoted-Printable解码
private function decodeMimeHeader($header) $decodedText = quoted_printable_decode(str_replace('_', ' ', $text));
{ }
// 处理 =?UTF-8?B?5L2g5aW9?= 这样的MIME编码
$decoded = ''; if (isset($decodedText)) {
$parts = preg_split('/(=\?[^?]+\?[BQ]\?[^?]+\?=)/i', $header, -1, PREG_SPLIT_DELIM_CAPTURE); $decoded .= mb_convert_encoding($decodedText, 'UTF-8', $charset);
}
foreach ($parts as $part) { } else {
if (preg_match('/=\?([^\?]+)\?([BQ])\?([^\?]+)\?=/i', $part, $matches)) { $decoded .= $part;
$charset = $matches[1]; }
$encoding = strtoupper($matches[2]); }
$text = $matches[3];
return $decoded ?: $header;
if ($encoding === 'B') { }
// Base64解码
$decodedText = base64_decode($text); public function stop()
} elseif ($encoding === 'Q') { {
// Quoted-Printable解码 $this->isRunning = false;
$decodedText = quoted_printable_decode(str_replace('_', ' ', $text)); }
} }
if (isset($decodedText)) { // 如果直接运行这个文件
$decoded .= mb_convert_encoding($decodedText, 'UTF-8', $charset); if (basename(__FILE__) == basename($_SERVER['PHP_SELF'])) {
} // 检查是否有权限监听25端口需要sudo
} else { if (posix_getuid() != 0) {
$decoded .= $part; echo "注意需要sudo权限监听25端口\n";
} echo "请运行sudo php " . __FILE__ . "\n";
} exit(1);
}
return $decoded ?: $header;
} $server = new SimpleSmtpServer();
$server->start();
public function stop() }
{
$this->isRunning = false;
}
}
// 如果直接运行这个文件
if (basename(__FILE__) == basename($_SERVER['PHP_SELF'])) {
// 检查是否有权限监听端口需要sudo
if (posix_getuid() != 0) {
echo "注意需要sudo权限监听该端口\n";
echo "请运行sudo php " . __FILE__ . "\n";
exit(1);
}
$server = new SimpleSmtpServer('0.0.0.0',$smtpPort);
$server->start();
}
?> ?>

@ -1,8 +1,4 @@
<?php <?php
/**
* 数据库配置文件
* 使用环境变量或默认配置
*/
require_once __DIR__ . '/../../config/database.php'; require_once __DIR__ . '/../../config/database.php';
class Database { class Database {
@ -10,9 +6,8 @@ class Database {
private $connection; private $connection;
private function __construct() { private function __construct() {
$config = require __DIR__ . '/../../config/database.php'; $config = require __DIR__ . '/../../config/database.php';
//自动每一条SQL提交一次
// $config['options'][PDO::ATTR_AUTOCOMMIT] = true;
$dsn = "mysql:host={$config['host']};port={$config['port']};dbname={$config['database']};charset={$config['charset']}"; $dsn = "mysql:host={$config['host']};port={$config['port']};dbname={$config['database']};charset={$config['charset']}";
try { try {

@ -1,176 +1,176 @@
<?php <?php
require_once __DIR__ . '/Database.php'; require_once __DIR__ . '/Database.php';
/** /**
* 邮件数据访问层 * 邮件数据访问层
* 信件管理员 - 存信、取信 * 信件管理员 - 存信、取信
*/ */
class EmailRepository { class EmailRepository {
private $db; private $db;
public function __construct() { public function __construct() {
$this->db = Database::getInstance(); $this->db = Database::getInstance();
} }
/** /**
* 根据ID查找邮件 * 根据ID查找邮件
* @param int $id 邮件ID * @param int $id 邮件ID
* @return array|null 邮件信息或null * @return array|null 邮件信息或null
*/ */
public function findById($id) { public function findById($id) {
$stmt = $this->db->prepare(" $stmt = $this->db->prepare("
SELECT e.*, SELECT e.*,
u1.username as sender_username, u1.username as sender_username,
u2.username as recipient_username u2.username as recipient_username
FROM emails e FROM emails e
LEFT JOIN users u1 ON e.sender_id = u1.id LEFT JOIN users u1 ON e.sender_id = u1.id
LEFT JOIN users u2 ON e.recipient_id = u2.id LEFT JOIN users u2 ON e.recipient_id = u2.id
WHERE e.id = ? AND e.is_deleted = 0 WHERE e.id = ? AND e.is_deleted = 0
"); ");
$stmt->execute([$id]); $stmt->execute([$id]);
return $stmt->fetch(); return $stmt->fetch();
} }
/** /**
* 获取用户的收件箱邮件 * 获取用户的收件箱邮件
* @param int $userId 用户ID * @param int $userId 用户ID
* @param int $limit 限制数量 * @param int $limit 限制数量
* @param int $offset 偏移量 * @param int $offset 偏移量
* @return array 邮件列表 * @return array 邮件列表
*/ */
public function getInbox($userId, $limit = null, $offset = 0) { public function getInbox($userId, $limit = null, $offset = 0) {
$sql = " $sql = "
SELECT e.*, SELECT e.*,
COALESCE(u1.username, e.sender) as sender_name, COALESCE(u1.username, e.sender) as sender_name,
COALESCE(u2.username, e.recipient) as recipient_name COALESCE(u2.username, e.recipient) as recipient_name
FROM emails e FROM emails e
LEFT JOIN users u1 ON e.sender_id = u1.id LEFT JOIN users u1 ON e.sender_id = u1.id
LEFT JOIN users u2 ON e.recipient_id = u2.id LEFT JOIN users u2 ON e.recipient_id = u2.id
WHERE (e.recipient_id = ? OR e.recipient = (SELECT username FROM users WHERE id = ?)) WHERE (e.recipient_id = ? OR e.recipient = (SELECT username FROM users WHERE id = ?))
AND e.is_deleted = 0 AND e.is_deleted = 0
ORDER BY e.created_at DESC ORDER BY e.created_at DESC
"; ";
if ($limit !== null) { if ($limit !== null) {
$sql .= " LIMIT ? OFFSET ?"; $sql .= " LIMIT ? OFFSET ?";
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
$stmt->execute([$userId, $userId, $limit, $offset]); $stmt->execute([$userId, $userId, $limit, $offset]);
} else { } else {
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
$stmt->execute([$userId, $userId]); $stmt->execute([$userId, $userId]);
} }
return $stmt->fetchAll(); return $stmt->fetchAll();
} }
/** /**
* 获取用户的发件箱邮件 * 获取用户的发件箱邮件
* @param int $userId 用户ID * @param int $userId 用户ID
* @param int $limit 限制数量 * @param int $limit 限制数量
* @param int $offset 偏移量 * @param int $offset 偏移量
* @return array 邮件列表 * @return array 邮件列表
*/ */
public function getSent($userId, $limit = null, $offset = 0) { public function getSent($userId, $limit = null, $offset = 0) {
$sql = " $sql = "
SELECT e.*, SELECT e.*,
COALESCE(u1.username, e.sender) as sender_name, COALESCE(u1.username, e.sender) as sender_name,
COALESCE(u2.username, e.recipient) as recipient_name COALESCE(u2.username, e.recipient) as recipient_name
FROM emails e FROM emails e
LEFT JOIN users u1 ON e.sender_id = u1.id LEFT JOIN users u1 ON e.sender_id = u1.id
LEFT JOIN users u2 ON e.recipient_id = u2.id LEFT JOIN users u2 ON e.recipient_id = u2.id
WHERE (e.sender_id = ? OR e.sender = (SELECT username FROM users WHERE id = ?)) WHERE (e.sender_id = ? OR e.sender = (SELECT username FROM users WHERE id = ?))
AND e.is_deleted = 0 AND e.is_deleted = 0
ORDER BY e.created_at DESC ORDER BY e.created_at DESC
"; ";
if ($limit !== null) { if ($limit !== null) {
$sql .= " LIMIT ? OFFSET ?"; $sql .= " LIMIT ? OFFSET ?";
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
$stmt->execute([$userId, $userId, $limit, $offset]); $stmt->execute([$userId, $userId, $limit, $offset]);
} else { } else {
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
$stmt->execute([$userId, $userId]); $stmt->execute([$userId, $userId]);
} }
return $stmt->fetchAll(); return $stmt->fetchAll();
} }
/** /**
* 获取所有邮件(管理员功能) * 获取所有邮件(管理员功能)
* @param int $limit 限制数量 * @param int $limit 限制数量
* @param int $offset 偏移量 * @param int $offset 偏移量
* @return array 邮件列表 * @return array 邮件列表
*/ */
public function getAll($limit = null, $offset = 0) { public function getAll($limit = null, $offset = 0) {
$sql = " $sql = "
SELECT e.*, SELECT e.*,
COALESCE(u1.username, e.sender) as sender_name, COALESCE(u1.username, e.sender) as sender_name,
COALESCE(u2.username, e.recipient) as recipient_name COALESCE(u2.username, e.recipient) as recipient_name
FROM emails e FROM emails e
LEFT JOIN users u1 ON e.sender_id = u1.id LEFT JOIN users u1 ON e.sender_id = u1.id
LEFT JOIN users u2 ON e.recipient_id = u2.id LEFT JOIN users u2 ON e.recipient_id = u2.id
WHERE e.is_deleted = 0 WHERE e.is_deleted = 0
ORDER BY e.created_at DESC ORDER BY e.created_at DESC
"; ";
if ($limit !== null) { if ($limit !== null) {
$sql .= " LIMIT ? OFFSET ?"; $sql .= " LIMIT ? OFFSET ?";
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
$stmt->execute([$limit, $offset]); $stmt->execute([$limit, $offset]);
} else { } else {
$stmt = $this->db->query($sql); $stmt = $this->db->query($sql);
} }
return $stmt->fetchAll(); return $stmt->fetchAll();
} }
/** /**
* 获取邮件总数 * 获取邮件总数
* @param int|null $userId 用户ID如果提供只统计该用户的邮件 * @param int|null $userId 用户ID如果提供只统计该用户的邮件
* @return int 邮件总数 * @return int 邮件总数
*/ */
public function getCount($userId = null) { public function getCount($userId = null) {
if ($userId !== null) { if ($userId !== null) {
$stmt = $this->db->prepare(" $stmt = $this->db->prepare("
SELECT COUNT(*) as count FROM emails SELECT COUNT(*) as count FROM emails
WHERE (recipient_id = ? OR sender_id = ?) AND is_deleted = 0 WHERE (recipient_id = ? OR sender_id = ?) AND is_deleted = 0
"); ");
$stmt->execute([$userId, $userId]); $stmt->execute([$userId, $userId]);
} else { } else {
$stmt = $this->db->query("SELECT COUNT(*) as count FROM emails WHERE is_deleted = 0"); $stmt = $this->db->query("SELECT COUNT(*) as count FROM emails WHERE is_deleted = 0");
} }
$result = $stmt->fetch(); $result = $stmt->fetch();
return (int)$result['count']; return (int)$result['count'];
} }
/** /**
* 标记邮件为已读 * 标记邮件为已读
* @param int $id 邮件ID * @param int $id 邮件ID
* @return bool 是否成功 * @return bool 是否成功
*/ */
public function markAsRead($id) { public function markAsRead($id) {
$stmt = $this->db->prepare("UPDATE emails SET is_read = 1 WHERE id = ?"); $stmt = $this->db->prepare("UPDATE emails SET is_read = 1 WHERE id = ?");
return $stmt->execute([$id]); return $stmt->execute([$id]);
} }
/** /**
* 删除邮件(软删除) * 删除邮件(软删除)
* @param int $id 邮件ID * @param int $id 邮件ID
* @return bool 是否成功 * @return bool 是否成功
*/ */
public function delete($id) { public function delete($id) {
$stmt = $this->db->prepare("UPDATE emails SET is_deleted = 1 WHERE id = ?"); $stmt = $this->db->prepare("UPDATE emails SET is_deleted = 1 WHERE id = ?");
return $stmt->execute([$id]); return $stmt->execute([$id]);
} }
/** /**
* 永久删除邮件 * 永久删除邮件
* @param int $id 邮件ID * @param int $id 邮件ID
* @return bool 是否成功 * @return bool 是否成功
*/ */
public function permanentDelete($id) { public function permanentDelete($id) {
$stmt = $this->db->prepare("DELETE FROM emails WHERE id = ?"); $stmt = $this->db->prepare("DELETE FROM emails WHERE id = ?");
return $stmt->execute([$id]); return $stmt->execute([$id]);
} }
} }

@ -1,121 +1,114 @@
<?php <?php
require_once __DIR__ . '/Database.php'; require_once __DIR__ . '/Database.php';
/** /**
* 过滤规则数据访问层 * 过滤规则数据访问层
*/ */
class FilterRepository { class FilterRepository {
private $db; private $db;
public function __construct() { public function __construct() {
$this->db = Database::getInstance(); $this->db = Database::getInstance();
} }
/** /**
* 创建过滤规则 * 创建过滤规则
* @param string $type 规则类型 ('email' 或 'ip') * @param string $type 规则类型 ('email' 或 'ip')
* @param string $value 规则值 * @param string $value 规则值
* @param string $action 动作 ('block' 或 'allow') * @param string $action 动作 ('block' 或 'allow')
* @param string $description 描述 * @param string $description 描述
* @return bool 是否成功 * @return bool 是否成功
*/ */
public function create($type, $value, $action = 'block', $description = '') { public function create($type, $value, $action = 'block', $description = '') {
$stmt = $this->db->prepare(" $stmt = $this->db->prepare("
INSERT INTO filter_rules (rule_type, rule_value, action, description, is_active) INSERT INTO filter_rules (rule_type, rule_value, action, description, is_active)
VALUES (?, ?, ?, ?, 1) VALUES (?, ?, ?, ?, 1)
"); ");
return $stmt->execute([$type, $value, $action, $description]); return $stmt->execute([$type, $value, $action, $description]);
} }
/** /**
* 获取所有过滤规则 * 获取所有过滤规则
* @return array 规则列表 * @return array 规则列表
*/ */
public function getAll() { public function getAll() {
$stmt = $this->db->query(" $stmt = $this->db->query("
SELECT * FROM filter_rules SELECT * FROM filter_rules
ORDER BY rule_type, created_at DESC ORDER BY rule_type, created_at DESC
"); ");
return $stmt->fetchAll(); return $stmt->fetchAll();
} }
/** /**
* 获取激活的过滤规则 * 获取激活的过滤规则
* @return array 规则列表 * @return array 规则列表
*/ */
public function getActive() { public function getActive() {
$stmt = $this->db->query(" $stmt = $this->db->query("
SELECT * FROM filter_rules SELECT * FROM filter_rules
WHERE is_active = 1 WHERE is_active = 1
ORDER BY rule_type, created_at DESC ORDER BY rule_type, created_at DESC
"); ");
return $stmt->fetchAll(); return $stmt->fetchAll();
} }
/** /**
* 检查邮箱是否被过滤 * 检查邮箱是否被过滤
* @param string $email 邮箱地址 * @param string $email 邮箱地址
* @return bool 是否被阻止 * @return bool 是否被阻止
*/ */
public function isEmailBlocked($email) { public function isEmailBlocked($email) {
$stmt = $this->db->prepare(" $stmt = $this->db->prepare("
SELECT action FROM filter_rules SELECT action FROM filter_rules
WHERE rule_type = 'email' WHERE rule_type = 'email'
AND is_active = 1 AND is_active = 1
AND (rule_value = ? OR rule_value LIKE ?) AND (rule_value = ? OR rule_value LIKE ?)
ORDER BY action DESC ORDER BY action DESC
LIMIT 1 LIMIT 1
"); ");
$stmt->execute([$email, $email]); $stmt->execute([$email, $email]);
$result = $stmt->fetch(); $result = $stmt->fetch();
return $result && $result['action'] === 'block'; return $result && $result['action'] === 'block';
} }
/** /**
* 检查IP是否被过滤 * 检查IP是否被过滤
* @param string $ip IP地址 * @param string $ip IP地址
* @return bool 是否被阻止 * @return bool 是否被阻止
*/ */
public function isIPBlocked($ip) { public function isIPBlocked($ip) {
$stmt = $this->db->prepare(" $stmt = $this->db->prepare("
SELECT action FROM filter_rules SELECT action FROM filter_rules
WHERE rule_type = 'ip' WHERE rule_type = 'ip'
AND is_active = 1 AND is_active = 1
AND rule_value = ? AND rule_value = ?
ORDER BY action DESC ORDER BY action DESC
LIMIT 1 LIMIT 1
"); ");
$stmt->execute([$ip]); $stmt->execute([$ip]);
$result = $stmt->fetch(); $result = $stmt->fetch();
return $result && $result['action'] === 'block'; return $result && $result['action'] === 'block';
} }
/** /**
* 删除规则 * 删除规则
* @param int $id 规则ID * @param int $id 规则ID
* @return bool 是否成功 * @return bool 是否成功
*/ */
public function delete($id) { public function delete($id) {
$stmt = $this->db->prepare("DELETE FROM filter_rules WHERE id = ?"); $stmt = $this->db->prepare("DELETE FROM filter_rules WHERE id = ?");
return $stmt->execute([$id]); return $stmt->execute([$id]);
} }
/** /**
* 更新规则状态 * 更新规则状态
* @param int $id 规则ID * @param int $id 规则ID
* @param bool $isActive 是否激活 * @param bool $isActive 是否激活
* @return bool 是否成功 * @return bool 是否成功
*/ */
public function updateStatus($id, $isActive) { public function updateStatus($id, $isActive) {
$stmt = $this->db->prepare("UPDATE filter_rules SET is_active = ? WHERE id = ?"); $stmt = $this->db->prepare("UPDATE filter_rules SET is_active = ? WHERE id = ?");
return $stmt->execute([$isActive ? 1 : 0, $id]); return $stmt->execute([$isActive ? 1 : 0, $id]);
$this->db->commit(); }
} }
public function getById(int $id): ?array
{
$stmt = $this->db->prepare("SELECT * FROM filter_rules WHERE id = ? LIMIT 1");
$stmt->execute([$id]);
return $stmt->fetch() ?: null;
}
}

@ -1,84 +1,84 @@
<?php <?php
require_once __DIR__ . '/Database.php'; require_once __DIR__ . '/Database.php';
/** /**
* 邮箱管理数据访问层 * 邮箱管理数据访问层
* *
* @package storage * @package storage
*/ */
class MailboxRepository { class MailboxRepository {
private $db; private $db;
public function __construct() { public function __construct() {
$this->db = Database::getInstance(); $this->db = Database::getInstance();
} }
/** /**
* 获取用户邮箱大小限制 * 获取用户邮箱大小限制
* @param int $userId 用户ID * @param int $userId 用户ID
* @return int 大小限制(字节) * @return int 大小限制(字节)
*/ */
public function getSizeLimit($userId) { public function getSizeLimit($userId) {
$stmt = $this->db->prepare(" $stmt = $this->db->prepare("
SELECT size_limit_bytes FROM user_mailbox_limits WHERE user_id = ? SELECT size_limit_bytes FROM user_mailbox_limits WHERE user_id = ?
"); ");
$stmt->execute([$userId]); $stmt->execute([$userId]);
$result = $stmt->fetch(); $result = $stmt->fetch();
if ($result) { if ($result) {
return (int)$result['size_limit_bytes']; return (int)$result['size_limit_bytes'];
} }
// 返回默认值 // 返回默认值
return 104857600; // 100MB return 104857600; // 100MB
} }
/** /**
* 设置用户邮箱大小限制 * 设置用户邮箱大小限制
* @param int $userId 用户ID * @param int $userId 用户ID
* @param int $sizeBytes 大小限制(字节) * @param int $sizeBytes 大小限制(字节)
* @return bool 是否成功 * @return bool 是否成功
*/ */
public function setSizeLimit($userId, $sizeBytes) { public function setSizeLimit($userId, $sizeBytes) {
$stmt = $this->db->prepare(" $stmt = $this->db->prepare("
INSERT INTO user_mailbox_limits (user_id, size_limit_bytes) INSERT INTO user_mailbox_limits (user_id, size_limit_bytes)
VALUES (?, ?) VALUES (?, ?)
ON DUPLICATE KEY UPDATE size_limit_bytes = VALUES(size_limit_bytes) ON DUPLICATE KEY UPDATE size_limit_bytes = VALUES(size_limit_bytes)
"); ");
return $stmt->execute([$userId, $sizeBytes]); return $stmt->execute([$userId, $sizeBytes]);
} }
/** /**
* 获取用户当前邮箱使用大小 * 获取用户当前邮箱使用大小
* @param int $userId 用户ID * @param int $userId 用户ID
* @return int 已使用大小(字节) * @return int 已使用大小(字节)
*/ */
public function getUsedSize($userId) { public function getUsedSize($userId) {
$stmt = $this->db->prepare(" $stmt = $this->db->prepare("
SELECT COALESCE(SUM(size_bytes), 0) as total_size SELECT COALESCE(SUM(size_bytes), 0) as total_size
FROM emails FROM emails
WHERE recipient_id = ? AND is_deleted = 0 WHERE recipient_id = ? AND is_deleted = 0
"); ");
$stmt->execute([$userId]); $stmt->execute([$userId]);
$result = $stmt->fetch(); $result = $stmt->fetch();
return (int)($result['total_size'] ?? 0); return (int)($result['total_size'] ?? 0);
} }
/** /**
* 获取用户邮箱使用情况 * 获取用户邮箱使用情况
* @param int $userId 用户ID * @param int $userId 用户ID
* @return array ['limit' => int, 'used' => int, 'percentage' => float] * @return array ['limit' => int, 'used' => int, 'percentage' => float]
*/ */
public function getUsage($userId) { public function getUsage($userId) {
$limit = $this->getSizeLimit($userId); $limit = $this->getSizeLimit($userId);
$used = $this->getUsedSize($userId); $used = $this->getUsedSize($userId);
$percentage = $limit > 0 ? ($used / $limit) * 100 : 0; $percentage = $limit > 0 ? ($used / $limit) * 100 : 0;
return [ return [
'limit' => $limit, 'limit' => $limit,
'used' => $used, 'used' => $used,
'percentage' => round($percentage, 2) 'percentage' => round($percentage, 2)
]; ];
} }
} }

@ -1,79 +1,79 @@
<?php <?php
require_once __DIR__ . '/Database.php'; require_once __DIR__ . '/Database.php';
/** /**
* 服务状态数据访问层 * 服务状态数据访问层
*/ */
class ServiceRepository { class ServiceRepository {
private $db; private $db;
public function __construct() { public function __construct() {
$this->db = Database::getInstance(); $this->db = Database::getInstance();
} }
/** /**
* 获取服务状态 * 获取服务状态
* @param string $serviceName 服务名称 ('smtp' 或 'pop3') * @param string $serviceName 服务名称 ('smtp' 或 'pop3')
* @return array|null 服务状态或null * @return array|null 服务状态或null
*/ */
public function getStatus($serviceName) { public function getStatus($serviceName) {
$stmt = $this->db->prepare("SELECT * FROM service_status WHERE service_name = ?"); $stmt = $this->db->prepare("SELECT * FROM service_status WHERE service_name = ?");
$stmt->execute([$serviceName]); $stmt->execute([$serviceName]);
return $stmt->fetch(); return $stmt->fetch();
} }
/** /**
* 更新服务状态 * 更新服务状态
* @param string $serviceName 服务名称 * @param string $serviceName 服务名称
* @param bool $isRunning 是否运行 * @param bool $isRunning 是否运行
* @param int|null $pid 进程ID * @param int|null $pid 进程ID
* @return bool 是否成功 * @return bool 是否成功
*/ */
public function updateStatus($serviceName, $isRunning, $pid = null) { public function updateStatus($serviceName, $isRunning, $pid = null) {
$stmt = $this->db->prepare(" $stmt = $this->db->prepare("
INSERT INTO service_status (service_name, is_running, pid, last_started_at, last_stopped_at) INSERT INTO service_status (service_name, is_running, pid, last_started_at, last_stopped_at)
VALUES (?, ?, ?, NOW(), NULL) VALUES (?, ?, ?, NOW(), NULL)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
is_running = VALUES(is_running), is_running = VALUES(is_running),
pid = VALUES(pid), pid = VALUES(pid),
last_started_at = IF(VALUES(is_running) = 1, NOW(), last_started_at), last_started_at = IF(VALUES(is_running) = 1, NOW(), last_started_at),
last_stopped_at = IF(VALUES(is_running) = 0, NOW(), last_stopped_at) last_stopped_at = IF(VALUES(is_running) = 0, NOW(), last_stopped_at)
"); ");
return $stmt->execute([$serviceName, $isRunning ? 1 : 0, $pid]); return $stmt->execute([$serviceName, $isRunning ? 1 : 0, $pid]);
} }
/** /**
* 获取所有服务状态 * 获取所有服务状态
* @return array 服务状态列表 * @return array 服务状态列表
*/ */
public function getAllStatus() { public function getAllStatus() {
$stmt = $this->db->query("SELECT * FROM service_status ORDER BY service_name"); $stmt = $this->db->query("SELECT * FROM service_status ORDER BY service_name");
return $stmt->fetchAll(); return $stmt->fetchAll();
} }
/** /**
* 检查服务是否运行 * 检查服务是否运行
* @param string $serviceName 服务名称 * @param string $serviceName 服务名称
* @return bool 是否运行 * @return bool 是否运行
*/ */
public function isRunning($serviceName) { public function isRunning($serviceName) {
$status = $this->getStatus($serviceName); $status = $this->getStatus($serviceName);
if (!$status) { if (!$status) {
return false; return false;
} }
// 检查进程是否真的在运行 // 检查进程是否真的在运行
if ($status['pid'] && $status['is_running']) { if ($status['pid'] && $status['is_running']) {
// 检查进程是否存在 // 检查进程是否存在
$result = shell_exec("ps -p {$status['pid']} -o pid= 2>/dev/null"); $result = shell_exec("ps -p {$status['pid']} -o pid= 2>/dev/null");
if (empty(trim($result))) { if (empty(trim($result))) {
// 进程不存在,更新状态 // 进程不存在,更新状态
$this->updateStatus($serviceName, false, null); $this->updateStatus($serviceName, false, null);
return false; return false;
} }
} }
return (bool)$status['is_running']; return (bool)$status['is_running'];
} }
} }

@ -1,56 +1,56 @@
<?php <?php
require_once __DIR__ . '/Database.php'; require_once __DIR__ . '/Database.php';
/** /**
* 系统设置数据访问层 * 系统设置数据访问层
*/ */
class SystemSettingsRepository { class SystemSettingsRepository {
private $db; private $db;
public function __construct() { public function __construct() {
$this->db = Database::getInstance(); $this->db = Database::getInstance();
} }
/** /**
* 获取设置值 * 获取设置值
* @param string $key 设置键 * @param string $key 设置键
* @param mixed $default 默认值 * @param mixed $default 默认值
* @return mixed 设置值 * @return mixed 设置值
*/ */
public function get($key, $default = null) { public function get($key, $default = null) {
$stmt = $this->db->prepare("SELECT setting_value FROM system_settings WHERE setting_key = ?"); $stmt = $this->db->prepare("SELECT setting_value FROM system_settings WHERE setting_key = ?");
$stmt->execute([$key]); $stmt->execute([$key]);
$result = $stmt->fetch(); $result = $stmt->fetch();
return $result ? $result['setting_value'] : $default; return $result ? $result['setting_value'] : $default;
} }
/** /**
* 设置值 * 设置值
* @param string $key 设置键 * @param string $key 设置键
* @param mixed $value 设置值 * @param mixed $value 设置值
* @return bool 是否成功 * @return bool 是否成功
*/ */
public function set($key, $value) { public function set($key, $value) {
$stmt = $this->db->prepare(" $stmt = $this->db->prepare("
INSERT INTO system_settings (setting_key, setting_value) INSERT INTO system_settings (setting_key, setting_value)
VALUES (?, ?) VALUES (?, ?)
ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)
"); ");
return $stmt->execute([$key, $value]); return $stmt->execute([$key, $value]);
} }
/** /**
* 获取所有设置 * 获取所有设置
* @return array 所有设置 * @return array 所有设置
*/ */
public function getAll() { public function getAll() {
$stmt = $this->db->query("SELECT setting_key, setting_value FROM system_settings"); $stmt = $this->db->query("SELECT setting_key, setting_value FROM system_settings");
$results = $stmt->fetchAll(); $results = $stmt->fetchAll();
$settings = []; $settings = [];
foreach ($results as $row) { foreach ($results as $row) {
$settings[$row['setting_key']] = $row['setting_value']; $settings[$row['setting_key']] = $row['setting_value'];
} }
return $settings; return $settings;
} }
} }

@ -1,173 +1,173 @@
<?php <?php
require_once __DIR__ . '/Database.php'; require_once __DIR__ . '/Database.php';
require_once __DIR__ . '/../utils/Security.php'; require_once __DIR__ . '/../utils/Security.php';
/**
* 用户数据访问层 /**
* 查客户信息、创建用户等 * 用户数据访问层
*/ * 查客户信息、创建用户等
*/
class UserRepository { class UserRepository {
private $db; private $db;
public function __construct() { public function __construct() {
$this->db = Database::getInstance(); $this->db = Database::getInstance();
} }
/** /**
* 根据用户名查找用户 * 根据用户名查找用户
* @param string $username 用户名(邮箱) * @param string $username 用户名(邮箱)
* @return array|null 用户信息或null * @return array|null 用户信息或null
*/ */
public function findByUsername($username) { public function findByUsername($username) {
$stmt = $this->db->prepare("SELECT * FROM users WHERE username = ?"); $stmt = $this->db->prepare("SELECT * FROM users WHERE username = ?");
$stmt->execute([$username]); $stmt->execute([$username]);
return $stmt->fetch(); return $stmt->fetch();
} }
/** /**
* 根据ID查找用户 * 根据ID查找用户
* @param int $id 用户ID * @param int $id 用户ID
* @return array|null 用户信息或null * @return array|null 用户信息或null
*/ */
public function findById($id) { public function findById($id) {
$stmt = $this->db->prepare("SELECT * FROM users WHERE id = ?"); $stmt = $this->db->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$id]); $stmt->execute([$id]);
return $stmt->fetch(); return $stmt->fetch();
} }
/** /**
* 检查用户名是否已存在 * 检查用户名是否已存在
* @param string $username 用户名 * @param string $username 用户名
* @return bool 是否存在 * @return bool 是否存在
*/ */
public function usernameExists($username) { public function usernameExists($username) {
$user = $this->findByUsername($username); $user = $this->findByUsername($username);
return $user !== false; return $user !== false;
} }
/** /**
* 创建新用户 * 创建新用户
* @param string $username 用户名(邮箱) * @param string $username 用户名(邮箱)
* @param string $password 明文密码 * @param string $password 明文密码
* @param bool $isAdmin 是否为管理员 * @param bool $isAdmin 是否为管理员
* @param bool $isActive 是否激活 * @param bool $isActive 是否激活
* @return array 创建的用户信息 * @return array 创建的用户信息
* @throws Exception 如果创建失败 * @throws Exception 如果创建失败
*/ */
public function create($username, $password, $isAdmin = false, $isActive = true) { public function create($username, $password, $isAdmin = false, $isActive = true) {
// 检查用户名是否已存在 // 检查用户名是否已存在
if ($this->usernameExists($username)) { if ($this->usernameExists($username)) {
throw new Exception("用户名已存在"); throw new Exception("用户名已存在");
} }
// 加密密码 // 加密密码
$passwordHash = Security::hashPassword($password); $passwordHash = Security::hashPassword($password);
// 插入数据库 // 插入数据库
$stmt = $this->db->prepare(" $stmt = $this->db->prepare("
INSERT INTO users (username, password_hash, is_admin, is_active, created_at) INSERT INTO users (username, password_hash, is_admin, is_active, created_at)
VALUES (?, ?, ?, ?, NOW()) VALUES (?, ?, ?, ?, NOW())
"); ");
$stmt->execute([$username, $passwordHash, $isAdmin ? 1 : 0, $isActive ? 1 : 0]); $stmt->execute([$username, $passwordHash, $isAdmin ? 1 : 0, $isActive ? 1 : 0]);
// 返回创建的用户信息 // 返回创建的用户信息
$userId = $this->db->lastInsertId(); $userId = $this->db->lastInsertId();
return $this->findById($userId); return $this->findById($userId);
} }
/** /**
* 更新用户信息 * 更新用户信息
* @param int $id 用户ID * @param int $id 用户ID
* @param array $data 要更新的数据 ['password' => string, 'is_admin' => bool, 'is_active' => bool] * @param array $data 要更新的数据 ['password' => string, 'is_admin' => bool, 'is_active' => bool]
* @return bool 是否成功 * @return bool 是否成功
*/ */
public function update($id, $data) { public function update($id, $data) {
$updates = []; $updates = [];
$params = []; $params = [];
if (isset($data['password'])) { if (isset($data['password'])) {
$updates[] = "password_hash = ?"; $updates[] = "password_hash = ?";
$params[] = Security::hashPassword($data['password']); $params[] = Security::hashPassword($data['password']);
} }
if (isset($data['is_admin'])) { if (isset($data['is_admin'])) {
$updates[] = "is_admin = ?"; $updates[] = "is_admin = ?";
$params[] = $data['is_admin'] ? 1 : 0; $params[] = $data['is_admin'] ? 1 : 0;
} }
if (isset($data['is_active'])) { if (isset($data['is_active'])) {
$updates[] = "is_active = ?"; $updates[] = "is_active = ?";
$params[] = $data['is_active'] ? 1 : 0; $params[] = $data['is_active'] ? 1 : 0;
} }
if (empty($updates)) { if (empty($updates)) {
return false; return false;
} }
$params[] = $id; $params[] = $id;
$sql = "UPDATE users SET " . implode(", ", $updates) . " WHERE id = ?"; $sql = "UPDATE users SET " . implode(", ", $updates) . " WHERE id = ?";
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
return $stmt->execute($params); return $stmt->execute($params);
} }
/** /**
* 删除用户 * 删除用户
* @param int $id 用户ID * @param int $id 用户ID
* @return bool 是否成功 * @return bool 是否成功
*/ */
public function delete($id) { public function delete($id) {
$stmt = $this->db->prepare("DELETE FROM users WHERE id = ?"); $stmt = $this->db->prepare("DELETE FROM users WHERE id = ?");
return $stmt->execute([$id]); return $stmt->execute([$id]);
} }
/** /**
* 获取所有用户列表 * 获取所有用户列表
* @param int $limit 限制数量 * @param int $limit 限制数量
* @param int $offset 偏移量 * @param int $offset 偏移量
* @return array 用户列表 * @return array 用户列表
*/ */
public function getAll($limit = null, $offset = 0) { public function getAll($limit = null, $offset = 0) {
$sql = "SELECT id, username, is_admin, is_active, created_at FROM users ORDER BY created_at DESC"; $sql = "SELECT id, username, is_admin, is_active, created_at FROM users ORDER BY created_at DESC";
if ($limit !== null) { if ($limit !== null) {
$sql .= " LIMIT ? OFFSET ?"; $sql .= " LIMIT ? OFFSET ?";
$stmt = $this->db->prepare($sql); $stmt = $this->db->prepare($sql);
$stmt->execute([$limit, $offset]); $stmt->execute([$limit, $offset]);
} else { } else {
$stmt = $this->db->query($sql); $stmt = $this->db->query($sql);
} }
return $stmt->fetchAll(); return $stmt->fetchAll();
} }
/** /**
* 获取用户总数 * 获取用户总数
* @return int 用户总数 * @return int 用户总数
*/ */
public function getCount() { public function getCount() {
$stmt = $this->db->query("SELECT COUNT(*) as count FROM users"); $stmt = $this->db->query("SELECT COUNT(*) as count FROM users");
$result = $stmt->fetch(); $result = $stmt->fetch();
return (int)$result['count']; return (int)$result['count'];
} }
/** /**
* 验证用户密码 * 验证用户密码
* @param string $username 用户名 * @param string $username 用户名
* @param string $password 明文密码 * @param string $password 明文密码
* @return array|null 用户信息或null如果验证失败 * @return array|null 用户信息或null如果验证失败
*/ */
public function verifyPassword($username, $password) { public function verifyPassword($username, $password) {
$user = $this->findByUsername($username); $user = $this->findByUsername($username);
if (!$user) { if (!$user) {
return null; return null;
} }
if (!Security::verifyPassword($password, $user['password_hash'])) { if (!Security::verifyPassword($password, $user['password_hash'])) {
return null; return null;
} }
return $user; return $user;
} }
} }

@ -1,129 +1,129 @@
<?php <?php
/** /**
* 安全工具类 * 安全工具类
* 提供密码加密、防攻击等功能 * 提供密码加密、防攻击等功能
*/ */
class Security { class Security {
/** /**
* 加密密码 * 加密密码
* @param string $password 明文密码 * @param string $password 明文密码
* @return string 加密后的密码哈希 * @return string 加密后的密码哈希
*/ */
public static function hashPassword($password) { public static function hashPassword($password) {
return password_hash($password, PASSWORD_BCRYPT, ['cost' => 10]); return password_hash($password, PASSWORD_BCRYPT, ['cost' => 10]);
} }
/** /**
* 验证密码 * 验证密码
* @param string $password 明文密码 * @param string $password 明文密码
* @param string $hash 密码哈希 * @param string $hash 密码哈希
* @return bool 是否匹配 * @return bool 是否匹配
*/ */
public static function verifyPassword($password, $hash) { public static function verifyPassword($password, $hash) {
return password_verify($password, $hash); return password_verify($password, $hash);
} }
/** /**
* 清理输入防止XSS攻击 * 清理输入防止XSS攻击
* @param string $input 用户输入 * @param string $input 用户输入
* @return string 清理后的字符串 * @return string 清理后的字符串
*/ */
public static function sanitizeInput($input) { public static function sanitizeInput($input) {
return htmlspecialchars(strip_tags(trim($input)), ENT_QUOTES, 'UTF-8'); return htmlspecialchars(strip_tags(trim($input)), ENT_QUOTES, 'UTF-8');
} }
/** /**
* 生成CSRF令牌 * 生成CSRF令牌
* @return string CSRF令牌 * @return string CSRF令牌
*/ */
public static function generateCSRFToken() { public static function generateCSRFToken() {
if (!isset($_SESSION['csrf_token'])) { if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32)); $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
} }
return $_SESSION['csrf_token']; return $_SESSION['csrf_token'];
} }
/** /**
* 验证CSRF令牌 * 验证CSRF令牌
* @param string $token 待验证的令牌 * @param string $token 待验证的令牌
* @return bool 是否有效 * @return bool 是否有效
*/ */
public static function verifyCSRFToken($token) { public static function verifyCSRFToken($token) {
return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token); return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
} }
/** /**
* 获取客户端IP地址 * 获取客户端IP地址
* @return string IP地址 * @return string IP地址
*/ */
public static function getClientIP() { public static function getClientIP() {
$ipKeys = ['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR']; $ipKeys = ['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR'];
foreach ($ipKeys as $key) { foreach ($ipKeys as $key) {
if (array_key_exists($key, $_SERVER) === true) { if (array_key_exists($key, $_SERVER) === true) {
foreach (explode(',', $_SERVER[$key]) as $ip) { foreach (explode(',', $_SERVER[$key]) as $ip) {
$ip = trim($ip); $ip = trim($ip);
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) { if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) {
return $ip; return $ip;
} }
} }
} }
} }
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
} }
/** /**
* 防止暴力破解:检查登录尝试次数 * 防止暴力破解:检查登录尝试次数
* @param string $username 用户名 * @param string $username 用户名
* @param int $maxAttempts 最大尝试次数 * @param int $maxAttempts 最大尝试次数
* @param int $lockoutTime 锁定时间(秒) * @param int $lockoutTime 锁定时间(秒)
* @return bool 是否允许登录 * @return bool 是否允许登录
*/ */
public static function checkLoginAttempts($username, $maxAttempts = 5, $lockoutTime = 300) { public static function checkLoginAttempts($username, $maxAttempts = 5, $lockoutTime = 300) {
$key = 'login_attempts_' . md5($username); $key = 'login_attempts_' . md5($username);
if (!isset($_SESSION[$key])) { if (!isset($_SESSION[$key])) {
$_SESSION[$key] = ['count' => 0, 'time' => time()]; $_SESSION[$key] = ['count' => 0, 'time' => time()];
return true; return true;
} }
$attempts = $_SESSION[$key]; $attempts = $_SESSION[$key];
// 如果超过锁定时间,重置计数 // 如果超过锁定时间,重置计数
if (time() - $attempts['time'] > $lockoutTime) { if (time() - $attempts['time'] > $lockoutTime) {
$_SESSION[$key] = ['count' => 0, 'time' => time()]; $_SESSION[$key] = ['count' => 0, 'time' => time()];
return true; return true;
} }
// 检查是否超过最大尝试次数 // 检查是否超过最大尝试次数
if ($attempts['count'] >= $maxAttempts) { if ($attempts['count'] >= $maxAttempts) {
return false; return false;
} }
return true; return true;
} }
/** /**
* 记录登录失败尝试 * 记录登录失败尝试
* @param string $username 用户名 * @param string $username 用户名
*/ */
public static function recordLoginAttempt($username) { public static function recordLoginAttempt($username) {
$key = 'login_attempts_' . md5($username); $key = 'login_attempts_' . md5($username);
if (!isset($_SESSION[$key])) { if (!isset($_SESSION[$key])) {
$_SESSION[$key] = ['count' => 1, 'time' => time()]; $_SESSION[$key] = ['count' => 1, 'time' => time()];
} else { } else {
$_SESSION[$key]['count']++; $_SESSION[$key]['count']++;
$_SESSION[$key]['time'] = time(); $_SESSION[$key]['time'] = time();
} }
} }
/** /**
* 清除登录尝试记录 * 清除登录尝试记录
* @param string $username 用户名 * @param string $username 用户名
*/ */
public static function clearLoginAttempts($username) { public static function clearLoginAttempts($username) {
$key = 'login_attempts_' . md5($username); $key = 'login_attempts_' . md5($username);
unset($_SESSION[$key]); unset($_SESSION[$key]);
} }
} }

@ -1,157 +1,157 @@
<?php <?php
/** /**
* 输入验证工具类 * 输入验证工具类
* 检查输入是否合法 * 检查输入是否合法
*/ */
class Validator { class Validator {
/** /**
* 验证邮箱格式 * 验证邮箱格式
* @param string $email 邮箱地址 * @param string $email 邮箱地址
* @return bool 是否有效 * @return bool 是否有效
*/ */
public static function validateEmail($email) { public static function validateEmail($email) {
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false; return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
} }
/** /**
* 验证邮箱域名(检查是否属于指定域名) * 验证邮箱域名(检查是否属于指定域名)
* @param string $email 邮箱地址 * @param string $email 邮箱地址
* @param string $domain 允许的域名test.com * @param string $domain 允许的域名test.com
* @return bool 是否属于指定域名 * @return bool 是否属于指定域名
*/ */
public static function validateEmailDomain($email, $domain = 'test.com') { public static function validateEmailDomain($email, $domain = 'test.com') {
if (!self::validateEmail($email)) { if (!self::validateEmail($email)) {
return false; return false;
} }
$emailDomain = substr(strrchr($email, "@"), 1); $emailDomain = substr(strrchr($email, "@"), 1);
return strtolower($emailDomain) === strtolower($domain); return strtolower($emailDomain) === strtolower($domain);
} }
/** /**
* 验证密码强度 * 验证密码强度
* @param string $password 密码 * @param string $password 密码
* @param int $minLength 最小长度 * @param int $minLength 最小长度
* @return array ['valid' => bool, 'errors' => array] 验证结果和错误信息 * @return array ['valid' => bool, 'errors' => array] 验证结果和错误信息
*/ */
public static function validatePassword($password, $minLength = 6) { public static function validatePassword($password, $minLength = 6) {
$errors = []; $errors = [];
if (strlen($password) < $minLength) { if (strlen($password) < $minLength) {
$errors[] = "密码长度至少需要 {$minLength} 个字符"; $errors[] = "密码长度至少需要 {$minLength} 个字符";
} }
if (preg_match('/^[a-zA-Z0-9]+$/', $password) && strlen($password) < 8) { if (preg_match('/^[a-zA-Z0-9]+$/', $password) && strlen($password) < 8) {
// 如果密码只包含字母和数字且长度小于8建议使用更复杂的密码 // 如果密码只包含字母和数字且长度小于8建议使用更复杂的密码
// 但不强制要求 // 但不强制要求
} }
return [ return [
'valid' => empty($errors), 'valid' => empty($errors),
'errors' => $errors 'errors' => $errors
]; ];
} }
/** /**
* 验证用户名格式 * 验证用户名格式
* @param string $username 用户名(邮箱格式) * @param string $username 用户名(邮箱格式)
* @return array ['valid' => bool, 'errors' => array] 验证结果和错误信息 * @return array ['valid' => bool, 'errors' => array] 验证结果和错误信息
*/ */
public static function validateUsername($username) { public static function validateUsername($username) {
$errors = []; $errors = [];
if (empty($username)) { if (empty($username)) {
$errors[] = "用户名不能为空"; $errors[] = "用户名不能为空";
} elseif (!self::validateEmail($username)) { } elseif (!self::validateEmail($username)) {
$errors[] = "用户名必须是有效的邮箱格式"; $errors[] = "用户名必须是有效的邮箱格式";
} }
return [ return [
'valid' => empty($errors), 'valid' => empty($errors),
'errors' => $errors 'errors' => $errors
]; ];
} }
/** /**
* 验证IP地址格式 * 验证IP地址格式
* @param string $ip IP地址 * @param string $ip IP地址
* @return bool 是否有效 * @return bool 是否有效
*/ */
public static function validateIP($ip) { public static function validateIP($ip) {
return filter_var($ip, FILTER_VALIDATE_IP) !== false; return filter_var($ip, FILTER_VALIDATE_IP) !== false;
} }
/** /**
* 验证端口号 * 验证端口号
* @param int $port 端口号 * @param int $port 端口号
* @return bool 是否有效1-65535 * @return bool 是否有效1-65535
*/ */
public static function validatePort($port) { public static function validatePort($port) {
return is_numeric($port) && $port >= 1 && $port <= 65535; return is_numeric($port) && $port >= 1 && $port <= 65535;
} }
/** /**
* 验证非空字符串 * 验证非空字符串
* @param string $value 待验证的值 * @param string $value 待验证的值
* @param string $fieldName 字段名称(用于错误提示) * @param string $fieldName 字段名称(用于错误提示)
* @return array ['valid' => bool, 'errors' => array] 验证结果和错误信息 * @return array ['valid' => bool, 'errors' => array] 验证结果和错误信息
*/ */
public static function validateRequired($value, $fieldName = '字段') { public static function validateRequired($value, $fieldName = '字段') {
$errors = []; $errors = [];
if (empty(trim($value))) { if (empty(trim($value))) {
$errors[] = "{$fieldName}不能为空"; $errors[] = "{$fieldName}不能为空";
} }
return [ return [
'valid' => empty($errors), 'valid' => empty($errors),
'errors' => $errors 'errors' => $errors
]; ];
} }
/** /**
* 验证字符串长度 * 验证字符串长度
* @param string $value 待验证的值 * @param string $value 待验证的值
* @param int $min 最小长度 * @param int $min 最小长度
* @param int $max 最大长度 * @param int $max 最大长度
* @param string $fieldName 字段名称 * @param string $fieldName 字段名称
* @return array ['valid' => bool, 'errors' => array] 验证结果和错误信息 * @return array ['valid' => bool, 'errors' => array] 验证结果和错误信息
*/ */
public static function validateLength($value, $min, $max, $fieldName = '字段') { public static function validateLength($value, $min, $max, $fieldName = '字段') {
$errors = []; $errors = [];
$length = mb_strlen($value, 'UTF-8'); $length = mb_strlen($value, 'UTF-8');
if ($length < $min) { if ($length < $min) {
$errors[] = "{$fieldName}长度不能少于 {$min} 个字符"; $errors[] = "{$fieldName}长度不能少于 {$min} 个字符";
} }
if ($length > $max) { if ($length > $max) {
$errors[] = "{$fieldName}长度不能超过 {$max} 个字符"; $errors[] = "{$fieldName}长度不能超过 {$max} 个字符";
} }
return [ return [
'valid' => empty($errors), 'valid' => empty($errors),
'errors' => $errors 'errors' => $errors
]; ];
} }
/** /**
* 验证两个密码是否匹配 * 验证两个密码是否匹配
* @param string $password 密码 * @param string $password 密码
* @param string $confirmPassword 确认密码 * @param string $confirmPassword 确认密码
* @return array ['valid' => bool, 'errors' => array] 验证结果和错误信息 * @return array ['valid' => bool, 'errors' => array] 验证结果和错误信息
*/ */
public static function validatePasswordMatch($password, $confirmPassword) { public static function validatePasswordMatch($password, $confirmPassword) {
$errors = []; $errors = [];
if ($password !== $confirmPassword) { if ($password !== $confirmPassword) {
$errors[] = "两次输入的密码不一致"; $errors[] = "两次输入的密码不一致";
} }
return [ return [
'valid' => empty($errors), 'valid' => empty($errors),
'errors' => $errors 'errors' => $errors
]; ];
} }
} }

@ -1,30 +1,30 @@
#!/bin/bash #!/bin/bash
echo "测试SMTP服务器..." echo "测试SMTP服务器..."
# 启动服务器(后台运行) # 启动服务器(后台运行)
sudo php scripts/start_smtp.php & sudo php scripts/start_smtp.php &
SERVER_PID=$! SERVER_PID=$!
sleep 3 # 等待更长时间确保服务器启动 sleep 3 # 等待更长时间确保服务器启动
echo "发送测试邮件..." echo "发送测试邮件..."
# 使用timeout防止telnet无限等待 # 使用timeout防止telnet无限等待
timeout 5 telnet localhost 25 << 'EOF' timeout 5 telnet localhost 25 << 'EOF'
HELO test HELO test
MAIL FROM: <user1@test.com> MAIL FROM: <user1@test.com>
RCPT TO: <admin@test.com> RCPT TO: <admin@test.com>
DATA DATA
Subject: 自动测试邮件 Subject: 自动测试邮件
From: user1@test.com From: user1@test.com
To: admin@test.com To: admin@test.com
这是自动发送的测试邮件 这是自动发送的测试邮件
. .
QUIT QUIT
EOF EOF
# 等待服务器处理完成 # 等待服务器处理完成
sleep 2 sleep 2
kill $SERVER_PID 2>/dev/null kill $SERVER_PID 2>/dev/null
echo "测试完成" echo "测试完成"
Loading…
Cancel
Save