Compare commits

..

9 Commits

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

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

@ -0,0 +1,20 @@
启动最简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

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

@ -1,193 +1,194 @@
<?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="emails.php">邮件管理</a> <a href="broadcast.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> <a href="services.php">服务管理</a>
<a href="settings.php">系统设置</a> <a href="settings.php">系统设置</a>
</div> <a href="help.php">帮助</a>
</div>
<div class="container">
<h2>群发邮件</h2> <div class="container">
<h2>群发邮件</h2>
<?php if ($message): ?>
<div class="message"><?php echo $message; ?></div> <?php if ($message): ?>
<?php endif; ?> <div class="message"><?php echo $message; ?></div>
<?php endif; ?>
<?php if ($error): ?>
<div class="error"><?php echo $error; ?></div> <?php if ($error): ?>
<?php endif; ?> <div class="error"><?php echo $error; ?></div>
<?php endif; ?>
<form method="POST">
<div class="form-group"> <form method="POST">
<label>发送方式</label> <div class="form-group">
<div class="radio-group"> <label>发送方式</label>
<label> <div class="radio-group">
<input type="radio" name="broadcast_type" value="all" checked onchange="toggleRecipients()"> <label>
发送给所有用户 <input type="radio" name="broadcast_type" value="all" checked onchange="toggleRecipients()">
</label> 发送给所有用户
<label> </label>
<input type="radio" name="broadcast_type" value="selected" onchange="toggleRecipients()"> <label>
发送给指定用户 <input type="radio" name="broadcast_type" value="selected" onchange="toggleRecipients()">
</label> 发送给指定用户
</div> </label>
</div> </div>
</div>
<div class="form-group" id="recipients-group" style="display: none;">
<label>收件人(多个邮箱用逗号分隔)</label> <div class="form-group" id="recipients-group" style="display: none;">
<input type="text" name="recipients" placeholder="user1@test.com, user2@test.com"> <label>收件人(多个邮箱用逗号分隔)</label>
<small>请输入邮箱地址,多个邮箱用逗号分隔</small> <input type="text" name="recipients" placeholder="user1@test.com, user2@test.com">
<div class="user-list"> <small>请输入邮箱地址,多个邮箱用逗号分隔</small>
<strong>可用用户列表:</strong> <div class="user-list">
<?php foreach ($allUsers as $user): ?> <strong>可用用户列表:</strong>
<?php if ($user['is_active'] && $user['username'] !== $_SESSION['username']): ?> <?php foreach ($allUsers as $user): ?>
<div class="user-list-item"><?php echo htmlspecialchars($user['username']); ?></div> <?php if ($user['is_active'] && $user['username'] !== $_SESSION['username']): ?>
<?php endif; ?> <div class="user-list-item"><?php echo htmlspecialchars($user['username']); ?></div>
<?php endforeach; ?> <?php endif; ?>
</div> <?php endforeach; ?>
</div> </div>
</div>
<div class="form-group">
<label>邮件主题 *</label> <div class="form-group">
<input type="text" name="subject" required placeholder="请输入邮件主题" value="<?php echo htmlspecialchars($_POST['subject'] ?? ''); ?>"> <label>邮件主题 *</label>
</div> <input type="text" name="subject" required placeholder="请输入邮件主题" value="<?php echo htmlspecialchars($_POST['subject'] ?? ''); ?>">
</div>
<div class="form-group">
<label>邮件内容 *</label> <div class="form-group">
<textarea name="body" required placeholder="请输入邮件内容"><?php echo htmlspecialchars($_POST['body'] ?? ''); ?></textarea> <label>邮件内容 *</label>
</div> <textarea name="body" required placeholder="请输入邮件内容"><?php echo htmlspecialchars($_POST['body'] ?? ''); ?></textarea>
</div>
<button type="submit" name="send_broadcast" class="btn btn-primary">发送群发邮件</button>
</form> <button type="submit" name="send_broadcast" class="btn btn-primary">发送群发邮件</button>
</div> </form>
</div>
<script>
function toggleRecipients() { <script>
const broadcastType = document.querySelector('input[name="broadcast_type"]:checked').value; function toggleRecipients() {
const recipientsGroup = document.getElementById('recipients-group'); const broadcastType = document.querySelector('input[name="broadcast_type"]:checked').value;
if (broadcastType === 'selected') { const recipientsGroup = document.getElementById('recipients-group');
recipientsGroup.style.display = 'block'; if (broadcastType === 'selected') {
} else { recipientsGroup.style.display = 'block';
recipientsGroup.style.display = 'none'; } else {
} recipientsGroup.style.display = 'none';
} }
</script> }
</body> </script>
</html> </body>
</html>

@ -1,240 +1,238 @@
<?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>
<?php if ($isAdmin): ?> <a href="users.php">用户管理</a>
<a href="users.php">用户管理</a> <a href="broadcast.php">群发邮件</a>
<?php endif; ?> <a href="filters.php">过滤规则</a>
<a href="emails.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>
<?php if ($isAdmin): ?> <a href="help.php">帮助</a>
<a href="settings.php">系统设置</a> </div>
<?php endif; ?>
</div> <div class="container">
<h2>邮箱管理 <?php if ($isAdmin): ?>(全部邮件)<?php else: ?>(我的收件箱)<?php endif; ?></h2>
<div class="container">
<h2>邮件管理 <?php if ($isAdmin): ?>(全部邮件)<?php else: ?>(我的收件箱)<?php endif; ?></h2> <?php if ($message): ?>
<div class="message"><?php echo $message; ?></div>
<?php if ($message): ?> <?php endif; ?>
<div class="message"><?php echo $message; ?></div>
<?php endif; ?> <?php if ($error): ?>
<div class="error"><?php echo $error; ?></div>
<?php if ($error): ?> <?php endif; ?>
<div class="error"><?php echo $error; ?></div>
<?php endif; ?> <p><?php echo $totalEmails; ?> 封邮件</p>
<p><?php echo $totalEmails; ?> 封邮件</p> <table>
<thead>
<table> <tr>
<thead> <th>ID</th>
<tr> <th>发件人</th>
<th>ID</th> <th>收件人</th>
<th>发件人</th> <th>主题</th>
<th>收件人</th> <th>状态</th>
<th>主题</th> <th>时间</th>
<th>状态</th> <th>操作</th>
<th>时间</th> </tr>
<th>操作</th> </thead>
</tr> <tbody>
</thead> <?php if (empty($emails)): ?>
<tbody> <tr>
<?php if (empty($emails)): ?> <td colspan="7" style="text-align: center; padding: 40px;">
<tr> 暂无邮件
<td colspan="7" style="text-align: center; padding: 40px;"> </td>
暂无邮件 </tr>
</td> <?php else: ?>
</tr> <?php foreach ($emails as $email): ?>
<?php else: ?> <tr class="<?php echo $email['is_read'] ? '' : 'email-unread'; ?>">
<?php foreach ($emails as $email): ?> <td><?php echo $email['id']; ?></td>
<tr class="<?php echo $email['is_read'] ? '' : 'email-unread'; ?>"> <td><?php echo htmlspecialchars($email['sender_name'] ?? $email['sender'] ?? '未知'); ?></td>
<td><?php echo $email['id']; ?></td> <td><?php echo htmlspecialchars($email['recipient_name'] ?? $email['recipient'] ?? '未知'); ?></td>
<td><?php echo htmlspecialchars($email['sender_name'] ?? $email['sender'] ?? '未知'); ?></td> <td class="email-preview">
<td><?php echo htmlspecialchars($email['recipient_name'] ?? $email['recipient'] ?? '未知'); ?></td> <a href="#" onclick="viewEmail(<?php echo htmlspecialchars(json_encode($email)); ?>); return false;">
<td class="email-preview"> <?php echo htmlspecialchars($email['subject'] ?? '(无主题)'); ?>
<a href="#" onclick="viewEmail(<?php echo htmlspecialchars(json_encode($email)); ?>); return false;"> </a>
<?php echo htmlspecialchars($email['subject'] ?? '(无主题)'); ?> </td>
</a> <td>
</td> <?php if ($email['is_read']): ?>
<td> <span class="badge badge-read">已读</span>
<?php if ($email['is_read']): ?> <?php else: ?>
<span class="badge badge-read">已读</span> <span class="badge badge-unread">未读</span>
<?php else: ?> <?php endif; ?>
<span class="badge badge-unread">未读</span> </td>
<?php endif; ?> <td><?php echo $email['created_at']; ?></td>
</td> <td>
<td><?php echo $email['created_at']; ?></td> <a href="#" onclick="viewEmail(<?php echo htmlspecialchars(json_encode($email)); ?>); return false;" class="btn btn-primary btn-small">查看</a>
<td> <?php if (!$email['is_read']): ?>
<a href="#" onclick="viewEmail(<?php echo htmlspecialchars(json_encode($email)); ?>); return false;" class="btn btn-primary btn-small">查看</a> <a href="?mark_read=<?php echo $email['id']; ?>" class="btn btn-success btn-small">标记已读</a>
<?php if (!$email['is_read']): ?> <?php endif; ?>
<a href="?mark_read=<?php echo $email['id']; ?>" class="btn btn-success btn-small">标记已读</a> <a href="?delete=<?php echo $email['id']; ?>" class="btn btn-danger btn-small" onclick="return confirm('确定要删除此邮件吗?');">删除</a>
<?php endif; ?> </td>
<a href="?delete=<?php echo $email['id']; ?>" class="btn btn-danger btn-small" onclick="return confirm('确定要删除此邮件吗?');">删除</a> </tr>
</td> <?php endforeach; ?>
</tr> <?php endif; ?>
<?php endforeach; ?> </tbody>
<?php endif; ?> </table>
</tbody>
</table> <!-- 分页 -->
<?php if ($totalPages > 1): ?>
<!-- 分页 --> <div class="pagination">
<?php if ($totalPages > 1): ?> <?php if ($page > 1): ?>
<div class="pagination"> <a href="?page=<?php echo $page - 1; ?>">上一页</a>
<?php if ($page > 1): ?> <?php endif; ?>
<a href="?page=<?php echo $page - 1; ?>">上一页</a>
<?php endif; ?> <?php for ($i = 1; $i <= $totalPages; $i++): ?>
<?php if ($i == $page): ?>
<?php for ($i = 1; $i <= $totalPages; $i++): ?> <span class="current"><?php echo $i; ?></span>
<?php if ($i == $page): ?> <?php else: ?>
<span class="current"><?php echo $i; ?></span> <a href="?page=<?php echo $i; ?>"><?php echo $i; ?></a>
<?php else: ?> <?php endif; ?>
<a href="?page=<?php echo $i; ?>"><?php echo $i; ?></a> <?php endfor; ?>
<?php endif; ?>
<?php endfor; ?> <?php if ($page < $totalPages): ?>
<a href="?page=<?php echo $page + 1; ?>">下一页</a>
<?php if ($page < $totalPages): ?> <?php endif; ?>
<a href="?page=<?php echo $page + 1; ?>">下一页</a> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php endif; ?>
</div> <!-- 查看邮件模态框 -->
<div id="emailModal" class="modal">
<!-- 查看邮件模态框 --> <div class="modal-content">
<div id="emailModal" class="modal"> <span class="close" onclick="closeEmailModal()">&times;</span>
<div class="modal-content"> <h3 id="email-subject">邮件详情</h3>
<span class="close" onclick="closeEmailModal()">&times;</span> <div>
<h3 id="email-subject">邮件详情</h3> <strong>发件人:</strong><span id="email-sender"></span><br>
<div> <strong>收件人:</strong><span id="email-recipient"></span><br>
<strong>发件人:</strong><span id="email-sender"></span><br> <strong>时间:</strong><span id="email-time"></span><br>
<strong>收件人:</strong><span id="email-recipient"></span><br> <strong>主题:</strong><span id="email-subject-text"></span>
<strong>时间:</strong><span id="email-time"></span><br> </div>
<strong>主题:</strong><span id="email-subject-text"></span> <div class="email-body" id="email-body"></div>
</div> </div>
<div class="email-body" id="email-body"></div> </div>
</div>
</div> <script>
function viewEmail(email) {
<script> document.getElementById('email-subject').textContent = email.subject || '(无主题)';
function viewEmail(email) { document.getElementById('email-subject-text').textContent = email.subject || '(无主题)';
document.getElementById('email-subject').textContent = email.subject || '(无主题)'; document.getElementById('email-sender').textContent = email.sender_name || email.sender || '未知';
document.getElementById('email-subject-text').textContent = email.subject || '(无主题)'; document.getElementById('email-recipient').textContent = email.recipient_name || email.recipient || '未知';
document.getElementById('email-sender').textContent = email.sender_name || email.sender || '未知'; document.getElementById('email-time').textContent = email.created_at;
document.getElementById('email-recipient').textContent = email.recipient_name || email.recipient || '未知'; document.getElementById('email-body').textContent = email.body || '(无内容)';
document.getElementById('email-time').textContent = email.created_at; document.getElementById('emailModal').style.display = 'block';
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');
window.onclick = function(event) { if (event.target == modal) {
var modal = document.getElementById('emailModal'); closeEmailModal();
if (event.target == modal) { }
closeEmailModal(); }
} </script>
} </body>
</script> </html>
</body>
</html>

@ -1,243 +1,257 @@
<?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'])) {
header('Location: index.php'); session_start();
exit;
} // 身份验证
if (!isset($_SESSION['user_id'])) {
$filterRepo = new FilterRepository(); header('Location: index.php');
$message = ''; exit;
$error = ''; }
// 处理创建过滤规则 $filterRepo = new FilterRepository();
if (isset($_POST['create_filter'])) { $message = '';
$ruleType = $_POST['rule_type'] ?? ''; $error = '';
$ruleValue = trim($_POST['rule_value'] ?? '');
$action = $_POST['action'] ?? 'block'; if (isset($_POST['toggle_id'])) {
$description = trim($_POST['description'] ?? ''); $id = (int)$_POST['toggle_id'];
$row = $filterRepo->getById($id);
if (empty($ruleValue)) { if ($row) {
$error = "规则值不能为空"; $filterRepo->updateStatus($id, !(bool)$row['is_active']);
} else { }
if ($ruleType === 'email') { header('Location: filters.php'); // 302 跳回干净地址
if (!Validator::validateEmail($ruleValue)) { exit;
$error = "邮箱格式无效"; }
}
} elseif ($ruleType === 'ip') {
if (!Validator::validateIP($ruleValue)) {
$error = "IP地址格式无效"; // 处理创建过滤规则
} if (isset($_POST['create_filter'])) {
} else { $ruleType = $_POST['rule_type'] ?? '';
$error = "规则类型无效"; $ruleValue = trim($_POST['rule_value'] ?? '');
} $action = $_POST['action'] ?? 'block';
$description = trim($_POST['description'] ?? '');
if (empty($error)) {
try { if (empty($ruleValue)) {
if ($filterRepo->create($ruleType, $ruleValue, $action, $description)) { $error = "规则值不能为空";
$message = "过滤规则创建成功"; } else {
} else { if ($ruleType === 'email') {
$error = "创建失败,可能已存在相同规则"; if (!Validator::validateEmail($ruleValue)) {
} $error = "邮箱格式无效";
} catch (Exception $e) { }
$error = "创建失败: " . $e->getMessage(); } elseif ($ruleType === 'ip') {
} if (!Validator::validateIP($ruleValue)) {
} $error = "IP地址格式无效";
} }
} } else {
$error = "规则类型无效";
// 处理删除规则 }
if (isset($_GET['delete'])) {
$id = (int)$_GET['delete']; if (empty($error)) {
if ($filterRepo->delete($id)) { try {
$message = "规则删除成功"; if ($filterRepo->create($ruleType, $ruleValue, $action, $description)) {
} else { $message = "过滤规则创建成功";
$error = "删除失败"; } else {
} $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) { // 处理删除规则
$currentRule = $r; if (isset($_GET['delete'])) {
break; $id = (int)$_GET['delete'];
} if ($filterRepo->delete($id)) {
} $message = "规则删除成功";
if ($currentRule) { } else {
$newStatus = !$currentRule['is_active']; $error = "删除失败";
if ($filterRepo->updateStatus($id, $newStatus)) { }
$message = "规则状态已更新"; }
} else {
$error = "更新失败"; /*------------------------------------
} // 处理切换规则状态
} if (isset($_GET['toggle'])) {
} $id = (int)$_GET['toggle'];
$row = $filterRepo->getById($id); // 改用 public 方法
// 获取所有规则 if ($row) {
$rules = $filterRepo->getAll(); $newStatus = !(bool)$row['is_active'];
?> if ($filterRepo->updateStatus($id, $newStatus)) {
<!DOCTYPE html> $message = "规则状态已更新";
<html> } else {
<head> $error = "更新失败";
<title>过滤规则 - 邮件服务器</title> }
<meta charset="UTF-8"> } else {
<style> $error = "规则不存在";
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); } $rules = $filterRepo->getAll();
.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; } ?>
.form-group { margin-bottom: 15px; } <!DOCTYPE html>
.form-group label { display: block; margin-bottom: 5px; font-weight: 500; } <html>
.form-group input, .form-group select, .form-group textarea { width: 100%; max-width: 500px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; } <head>
.form-inline { display: flex; gap: 10px; align-items: flex-end; } <title>过滤规则 - 邮件服务器</title>
.form-inline .form-group { flex: 1; margin-bottom: 0; } <meta charset="UTF-8">
.btn { padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; text-decoration: none; display: inline-block; } <style>
.btn-primary { background: #007bff; color: white; } body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
.btn-danger { background: #dc3545; color: white; } .header { background: #007bff; color: white; padding: 15px; margin: -20px -20px 20px -20px; }
.btn-success { background: #28a745; 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-warning { background: #ffc107; color: #000; } .menu a { margin-right: 15px; text-decoration: none; color: #007bff; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; } .container { background: white; padding: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; } .message { background: #d4edda; color: #155724; padding: 12px; border-radius: 5px; margin-bottom: 20px; }
th { background: #f8f9fa; } .error { background: #f8d7da; color: #721c24; padding: 12px; border-radius: 5px; margin-bottom: 20px; }
.badge { padding: 4px 8px; border-radius: 3px; font-size: 12px; font-weight: 500; } .form-group { margin-bottom: 15px; }
.badge-email { background: #17a2b8; color: white; } .form-group label { display: block; margin-bottom: 5px; font-weight: 500; }
.badge-ip { background: #6c757d; 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-block { background: #dc3545; color: white; } .form-inline { display: flex; gap: 10px; align-items: flex-end; }
.badge-allow { background: #28a745; color: white; } .form-inline .form-group { flex: 1; margin-bottom: 0; }
.badge-active { background: #28a745; color: white; } .btn { padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; text-decoration: none; display: inline-block; }
.badge-inactive { background: #6c757d; color: white; } .btn-primary { background: #007bff; color: white; }
</style> .btn-danger { background: #dc3545; color: white; }
</head> .btn-success { background: #28a745; color: white; }
<body> .btn-warning { background: #ffc107; color: #000; }
<div class="header"> table { width: 100%; border-collapse: collapse; margin-top: 20px; }
<h1>邮件服务器管理后台</h1> th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
<div>欢迎, <?php echo htmlspecialchars($_SESSION['username']); ?> th { background: #f8f9fa; }
(<a href="logout.php" style="color: white;">退出</a>) .badge { padding: 4px 8px; border-radius: 3px; font-size: 12px; font-weight: 500; }
</div> .badge-email { background: #17a2b8; color: white; }
</div> .badge-ip { background: #6c757d; color: white; }
.badge-block { background: #dc3545; color: white; }
<div class="menu"> .badge-allow { background: #28a745; color: white; }
<a href="index.php">仪表盘</a> .badge-active { background: #28a745; color: white; }
<?php if ($_SESSION['is_admin'] ?? false): ?> .badge-inactive { background: #6c757d; color: white; }
<a href="users.php">用户管理</a> </style>
<?php endif; ?> </head>
<a href="emails.php">邮件管理</a> <body>
<a href="filters.php">过滤规则</a> <div class="header">
<a href="logs.php">系统日志</a> <h1>邮件服务器管理后台</h1>
<?php if ($_SESSION['is_admin'] ?? false): ?> <div>欢迎, <?php echo htmlspecialchars($_SESSION['username']); ?>
<a href="settings.php">系统设置</a> (<a href="logout.php" style="color: white;">退出</a>)
<?php endif; ?> </div>
</div> </div>
<div class="container"> <div class="menu">
<h2>过滤规则管理</h2> <a href="index.php">仪表盘</a>
<a href="users.php">用户管理</a>
<?php if ($message): ?> <a href="broadcast.php">群发邮件</a>
<div class="message"><?php echo $message; ?></div> <a href="filters.php">过滤规则</a>
<?php endif; ?> <a href="logs.php">系统日志</a>
<a href="services.php">服务管理</a>
<?php if ($error): ?> <a href="settings.php">系统设置</a>
<div class="error"><?php echo $error; ?></div> <a href="help.php">帮助</a>
<?php endif; ?> </div>
<!-- 创建过滤规则 --> <div class="container">
<h3>创建过滤规则</h3> <h2>过滤规则管理</h2>
<form method="POST" class="form-inline">
<div class="form-group"> <?php if ($message): ?>
<label>规则类型</label> <div class="message"><?php echo $message; ?></div>
<select name="rule_type" required> <?php endif; ?>
<option value="email">邮箱过滤</option>
<option value="ip">IP地址过滤</option> <?php if ($error): ?>
</select> <div class="error"><?php echo $error; ?></div>
</div> <?php endif; ?>
<div class="form-group">
<label>规则值</label> <!-- 创建过滤规则 -->
<input type="text" name="rule_value" placeholder="邮箱或IP地址" required> <h3>创建过滤规则</h3>
</div> <form method="POST" class="form-inline">
<div class="form-group"> <div class="form-group">
<label>动作</label> <label>规则类型</label>
<select name="action" required> <select name="rule_type" required>
<option value="block">阻止</option> <option value="email">邮箱过滤</option>
<option value="allow">允许</option> <option value="ip">IP地址过滤</option>
</select> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>描述</label> <label>规则值</label>
<input type="text" name="description" placeholder="规则描述(可选)"> <input type="text" name="rule_value" placeholder="邮箱或IP地址" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<button type="submit" name="create_filter" class="btn btn-primary">创建规则</button> <label>动作</label>
</div> <select name="action" required>
</form> <option value="block">阻止</option>
<option value="allow">允许</option>
<!-- 规则列表 --> </select>
<h3>过滤规则列表 (<?php echo count($rules); ?>)</h3> </div>
<table> <div class="form-group">
<thead> <label>描述</label>
<tr> <input type="text" name="description" placeholder="规则描述(可选)">
<th>ID</th> </div>
<th>类型</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>
</tr> <table>
</thead> <thead>
<tbody> <tr>
<?php if (empty($rules)): ?> <th>ID</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> <th>操作</th>
<td> </tr>
<span class="badge badge-<?php echo $rule['rule_type']; ?>"> </thead>
<?php echo $rule['rule_type'] === 'email' ? '邮箱' : 'IP'; ?> <tbody>
</span> <?php if (empty($rules)): ?>
</td> <tr>
<td><?php echo htmlspecialchars($rule['rule_value']); ?></td> <td colspan="8" style="text-align: center; padding: 40px;">暂无过滤规则</td>
<td> </tr>
<span class="badge badge-<?php echo $rule['action']; ?>"> <?php else: ?>
<?php echo $rule['action'] === 'block' ? '阻止' : '允许'; ?> <?php foreach ($rules as $rule): ?>
</span> <tr>
</td> <td><?php echo $rule['id']; ?></td>
<td><?php echo htmlspecialchars($rule['description'] ?? '-'); ?></td> <td>
<td> <span class="badge badge-<?php echo $rule['rule_type']; ?>">
<span class="badge badge-<?php echo $rule['is_active'] ? 'active' : 'inactive'; ?>"> <?php echo $rule['rule_type'] === 'email' ? '邮箱' : 'IP'; ?>
<?php echo $rule['is_active'] ? '激活' : '禁用'; ?> </span>
</span> </td>
</td> <td><?php echo htmlspecialchars($rule['rule_value']); ?></td>
<td><?php echo $rule['created_at']; ?></td> <td>
<td> <span class="badge badge-<?php echo $rule['action']; ?>">
<a href="?toggle=<?php echo $rule['id']; ?>" class="btn btn-warning"> <?php echo $rule['action'] === 'block' ? '阻止' : '允许'; ?>
<?php echo $rule['is_active'] ? '禁用' : '启用'; ?> </span>
</a> </td>
<a href="?delete=<?php echo $rule['id']; ?>" class="btn btn-danger" onclick="return confirm('确定要删除此规则吗?');">删除</a> <td><?php echo htmlspecialchars($rule['description'] ?? '-'); ?></td>
</td> <td>
</tr> <span class="badge badge-<?php echo $rule['is_active'] ? 'active' : 'inactive'; ?>">
<?php endforeach; ?> <?php echo $rule['is_active'] ? '激活' : '禁用'; ?>
<?php endif; ?> </span>
</tbody> </td>
</table> <td><?php echo $rule['created_at']; ?></td>
</div> <td>
</body> <form method="post" style="display:inline;">
</html> <input type="hidden" name="toggle_id" value="<?php echo $rule['id']; ?>">
<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,196 +1,186 @@
<?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>
<?php if ($_SESSION['is_admin'] ?? false): ?> <a href="users.php">用户管理</a>
<a href="users.php">用户管理</a> <a href="broadcast.php">群发邮件</a>
<?php endif; ?> <a href="filters.php">过滤规则</a>
<a href="emails.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>
<?php if ($_SESSION['is_admin'] ?? false): ?> <a href="help.php">帮助</a>
<a href="settings.php">系统设置</a> </div>
<?php endif; ?>
<a href="help.php">帮助</a> <div class="container">
</div> <h2>使用帮助</h2>
<div class="container"> <div class="section">
<h2>使用帮助</h2> <h3>系统概述</h3>
<p>这是一个基于POP3和SMTP协议的邮件服务器管理系统支持用户注册、邮件收发、系统管理等功能。</p>
<div class="section"> </div>
<h3>系统概述</h3>
<p>这是一个基于POP3和SMTP协议的邮件服务器管理系统支持用户注册、邮件收发、系统管理等功能。</p> <div class="section">
</div> <h3>功能模块</h3>
<div class="section"> <h4>1. 用户管理</h4>
<h3>功能模块</h3> <ul>
<li><strong>创建用户:</strong>可以创建新的用户账号,设置密码、管理员权限和激活状态</li>
<h4>1. 用户管理(管理员功能)</h4> <li><strong>编辑用户:</strong>可以修改用户密码、权限和状态</li>
<ul> <li><strong>删除用户:</strong>可以删除用户账号(不能删除自己)</li>
<li><strong>创建用户:</strong>可以创建新的用户账号,设置密码、管理员权限和激活状态</li> <li><strong>用户列表:</strong>查看所有注册用户及其状态</li>
<li><strong>编辑用户:</strong>可以修改用户密码、权限和状态</li> </ul>
<li><strong>删除用户:</strong>可以删除用户账号(不能删除自己)</li>
<li><strong>用户列表:</strong>查看所有注册用户及其状态</li>
</ul> <h4>2. 群发邮件</h4>
<ul>
<h4>2. 邮件管理</h4> <li><strong>发送给所有用户:</strong>可以一次性向所有激活用户发送通知邮件</li>
<ul> <li><strong>发送给指定用户:</strong>可以选择特定用户进行群发</li>
<li><strong>查看邮件:</strong>管理员可以查看所有邮件,普通用户只能查看自己的收件箱</li> <li><strong>邮件内容:</strong>支持自定义主题和内容</li>
<li><strong>标记已读:</strong>将未读邮件标记为已读</li> </ul>
<li><strong>删除邮件:</strong>删除不需要的邮件(软删除)</li>
<li><strong>邮件详情:</strong>点击邮件主题查看完整内容</li> <h4>3. 过滤规则</h4>
</ul> <ul>
<li><strong>邮箱过滤:</strong>可以阻止或允许特定邮箱地址</li>
<h4>3. 群发邮件(管理员功能)</h4> <li><strong>IP过滤</strong>可以阻止或允许特定IP地址</li>
<ul> <li><strong>规则管理:</strong>可以启用、禁用或删除过滤规则</li>
<li><strong>发送给所有用户:</strong>可以一次性向所有激活用户发送通知邮件</li> </ul>
<li><strong>发送给指定用户:</strong>可以选择特定用户进行群发</li>
<li><strong>邮件内容:</strong>支持自定义主题和内容</li> <h4>4. 系统设置</h4>
</ul> <ul>
<li><strong>端口设置:</strong>配置SMTP端口默认25和POP3端口默认110</li>
<h4>4. 过滤规则</h4> <li><strong>域名设置:</strong>设置邮件服务器域名默认test.com</li>
<ul> <li><strong>邮箱管理:</strong>设置用户邮箱大小限制</li>
<li><strong>邮箱过滤:</strong>可以阻止或允许特定邮箱地址</li> <li><strong>日志设置:</strong>配置日志存储路径和最大大小</li>
<li><strong>IP过滤</strong>可以阻止或允许特定IP地址</li> <li><strong>密码修改:</strong>管理员可以修改自己的密码</li>
<li><strong>规则管理:</strong>可以启用、禁用或删除过滤规则</li> </ul>
</ul>
<h4>5. 服务管理</h4>
<h4>5. 系统设置(管理员功能)</h4> <ul>
<ul> <li><strong>SMTP服务</strong>查看和管理SMTP服务状态</li>
<li><strong>端口设置:</strong>配置SMTP端口默认25和POP3端口默认110</li> <li><strong>POP3服务</strong>查看和管理POP3服务状态</li>
<li><strong>域名设置:</strong>设置邮件服务器域名默认test.com</li> <li><strong>服务起停:</strong>启动或停止邮件服务</li>
<li><strong>邮箱管理:</strong>设置用户邮箱大小限制</li> </ul>
<li><strong>日志设置:</strong>配置日志存储路径和最大大小</li>
<li><strong>密码修改:</strong>管理员可以修改自己的密码</li> <h4>6. 日志管理</h4>
</ul> <ul>
<li><strong>查看日志:</strong>查看SMTP和POP3服务器日志</li>
<h4>6. 服务管理(管理员功能)</h4> <li><strong>日志过滤:</strong>按类型过滤日志(全部/SMTP/POP3</li>
<ul> <li><strong>清除日志:</strong>管理员可以清除日志记录</li>
<li><strong>SMTP服务</strong>查看和管理SMTP服务状态</li> </ul>
<li><strong>POP3服务</strong>查看和管理POP3服务状态</li> </div>
<li><strong>服务起停:</strong>启动或停止邮件服务</li>
</ul> <div class="section">
<h3>启动服务器</h3>
<h4>7. 日志管理</h4> <p>要启动邮件服务器,需要在命令行执行以下命令:</p>
<ul> <div class="code-block">
<li><strong>查看日志:</strong>查看SMTP和POP3服务器日志</li> # 启动SMTP服务器需要sudo权限<br>
<li><strong>日志过滤:</strong>按类型过滤日志(全部/SMTP/POP3</li> sudo php scripts/start_smtp.php<br><br>
<li><strong>清除日志:</strong>管理员可以清除日志记录</li> # 启动POP3服务器需要sudo权限<br>
</ul> sudo php scripts/start_pop3.php
</div> </div>
<p><span class="highlight">注意:</span>两个服务器需要分别在两个终端运行。</p>
<div class="section"> </div>
<h3>启动服务器</h3>
<p>要启动邮件服务器,需要在命令行执行以下命令:</p> <div class="section">
<div class="code-block"> <h3>测试邮件服务器</h3>
# 启动SMTP服务器需要sudo权限<br> <h4>测试SMTP发送邮件</h4>
sudo php scripts/start_smtp.php<br><br> <div class="code-block">
# 启动POP3服务器需要sudo权限<br> telnet localhost 25<br>
sudo php scripts/start_pop3.php HELO test<br>
</div> MAIL FROM: &lt;user1@test.com&gt;<br>
<p><span class="highlight">注意:</span>两个服务器需要分别在两个终端运行。</p> RCPT TO: &lt;admin@test.com&gt;<br>
</div> DATA<br>
Subject: 测试邮件<br>
<div class="section"> From: user1@test.com<br>
<h3>测试邮件服务器</h3> To: admin@test.com<br><br>
<h4>测试SMTP发送邮件</h4> 这是一封测试邮件!<br>
<div class="code-block"> .<br>
telnet localhost 25<br> QUIT
HELO test<br> </div>
MAIL FROM: &lt;user1@test.com&gt;<br>
RCPT TO: &lt;admin@test.com&gt;<br> <h4>测试POP3接收邮件</h4>
DATA<br> <div class="code-block">
Subject: 测试邮件<br> telnet localhost 110<br>
From: user1@test.com<br> USER admin@test.com<br>
To: admin@test.com<br><br> PASS 123456<br>
这是一封测试邮件!<br> STAT<br>
.<br> LIST<br>
QUIT RETR 1<br>
</div> QUIT
</div>
<h4>测试POP3接收邮件</h4> </div>
<div class="code-block">
telnet localhost 110<br> <div class="section">
USER admin@test.com<br> <h3>常见问题</h3>
PASS 123456<br>
STAT<br> <h4>Q: 端口被占用怎么办?</h4>
LIST<br> <p>A: 检查端口占用情况:</p>
RETR 1<br> <div class="code-block">
QUIT sudo netstat -tlnp | grep 25 # 检查SMTP端口<br>
</div> sudo netstat -tlnp | grep 110 # 检查POP3端口
</div> </div>
<div class="section"> <h4>Q: 数据库连接失败?</h4>
<h3>常见问题</h3> <p>A: 确保Docker容器正在运行</p>
<div class="code-block">
<h4>Q: 端口被占用怎么办?</h4> docker-compose ps<br>
<p>A: 检查端口占用情况:</p> docker-compose up -d mysql
<div class="code-block"> </div>
sudo netstat -tlnp | grep 25 # 检查SMTP端口<br>
sudo netstat -tlnp | grep 110 # 检查POP3端口 <h4>Q: 如何重置数据库?</h4>
</div> <p>A: 执行以下命令:</p>
<div class="code-block">
<h4>Q: 数据库连接失败?</h4> docker-compose down -v<br>
<p>A: 确保Docker容器正在运行</p> docker-compose up -d<br>
<div class="code-block"> sleep 15
docker-compose ps<br> </div>
docker-compose up -d mysql </div>
</div>
<div class="section">
<h4>Q: 如何重置数据库?</h4> <h3>默认账号</h3>
<p>A: 执行以下命令:</p> <ul>
<div class="code-block"> <li><strong>管理员:</strong>admin@test.com / 123456</li>
docker-compose down -v<br> <li><strong>普通用户:</strong>user1@test.com / 123456</li>
docker-compose up -d<br> </ul>
sleep 15 </div>
</div> </div>
</div> </body>
</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,214 +1,217 @@
<?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'])) {
header('Location: index.php'); if (basename($_SERVER['PHP_SELF']) !== 'index.php') {
exit; header('Location: index.php');
} exit;
} }
}
// 登录检查 }
if (isset($_POST['login'])) {
$username = trim($_POST['username'] ?? ''); // 登录检查
$password = $_POST['password'] ?? ''; if (isset($_POST['login'])) {
$username = trim($_POST['username'] ?? '');
try { $password = $_POST['password'] ?? '';
// 检查登录尝试次数(防止暴力破解)
if (!Security::checkLoginAttempts($username)) { try {
$error = "登录失败次数过多请5分钟后再试"; // 检查登录尝试次数(防止暴力破解)
} else { if (!Security::checkLoginAttempts($username)) {
$userRepo = new UserRepository(); $error = "登录失败次数过多请5分钟后再试";
$user = $userRepo->verifyPassword($username, $password); } else {
$userRepo = new UserRepository();
if ($user && $user['is_active']) { $user = $userRepo->verifyPassword($username, $password);
// 登录成功,清除尝试记录
Security::clearLoginAttempts($username); if ($user && $user['is_active'] && $user['is_admin']) {
// 登录成功,清除尝试记录
$_SESSION['user_id'] = $user['id']; Security::clearLoginAttempts($username);
$_SESSION['username'] = $user['username'];
$_SESSION['is_admin'] = $user['is_admin']; $_SESSION['user_id'] = $user['id'];
header('Location: index.php'); $_SESSION['username'] = $user['username'];
exit; $_SESSION['is_admin'] = $user['is_admin'];
} else { header('Location: index.php');
// 登录失败,记录尝试 exit;
Security::recordLoginAttempt($username); }else if($user && !$user['is_active']){
$error = "用户名或密码错误"; $error = "用户被禁用";
} }else if($user && $user['is_active'] && !$user['is_admin']){
} $error = "没有权限";
} catch (Exception $e) { }else{
$error = "登录失败: " . $e->getMessage(); // 登录失败,记录尝试
} Security::recordLoginAttempt($username);
} $error = "用户名或密码错误";
}
// 如果是登录页面 }
if (basename($_SERVER['PHP_SELF']) === 'index.php' && !isset($_SESSION['user_id'])) { } catch (Exception $e) {
?> $error = "登录失败: " . $e->getMessage();
<!DOCTYPE html> }
<html> }
<head>
<title>邮件服务器管理后台 - 登录</title> // 如果是登录页面
<style> if (basename($_SERVER['PHP_SELF']) === 'index.php' && !isset($_SESSION['user_id'])) {
body { font-family: Arial, sans-serif; max-width: 400px; margin: 50px auto; padding: 20px; } ?>
.login-box { border: 1px solid #ddd; padding: 20px; border-radius: 5px; } <!DOCTYPE html>
input { width: 100%; padding: 8px; margin: 5px 0 15px 0; } <html>
button { background: #007bff; color: white; padding: 10px; border: none; width: 100%; } <head>
.error { color: red; margin-bottom: 15px; } <meta charset="UTF-8">
</style> <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head> <title>邮件服务器管理后台 - 登录</title>
<body> <style>
<div class="login-box"> body { font-family: Arial, sans-serif; max-width: 400px; margin: 50px auto; padding: 20px; }
<h2>邮件服务器管理后台</h2> .login-box { border: 1px solid #ddd; padding: 20px; border-radius: 5px; }
<?php if (isset($error)) echo "<div class='error'>$error</div>"; ?> input { width: 100%; padding: 8px; margin: 5px 0 15px 0; }
<form method="POST"> button { background: #007bff; color: white; padding: 10px; border: none; width: 100%; }
<div> .error { color: red; margin-bottom: 15px; }
<label>用户名:</label> </style>
<input type="text" name="username" value="admin@test.com" required> </head>
</div> <body>
<div> <div class="login-box">
<label>密码:</label> <h2>邮件服务器管理后台</h2>
<input type="password" name="password" value="123456" required> <?php if (isset($error)) echo "<div class='error'>$error</div>"; ?>
</div> <form method="POST">
<button type="submit" name="login">登录</button> <div>
</form> <label>用户名:</label>
<p style="margin-top: 20px; font-size: 12px; color: #666; text-align: center;"> <input type="text" name="username" value="admin@test.com" required>
还没有账号?<a href="register.php" style="color: #007bff; text-decoration: none;">立即注册</a> </div>
</p> <div>
<p style="margin-top: 10px; font-size: 12px; color: #666;"> <label>密码:</label>
测试账号: admin@test.com / 123456<br> <input type="password" name="password" value="123456" required>
普通账号: user1@test.com / 123456 </div>
</p> <button type="submit" name="login">登录</button>
</div> </form>
</body> <p style="margin-top: 10px; font-size: 12px; color: #666;">
</html> 测试账号: admin@test.com / 123456<br>
<?php 普通账号: user1@test.com / 123456
exit; </p>
} </div>
</body>
requireAuth(); </html>
?> <?php
<!DOCTYPE html> exit;
<html> }
<head>
<title>邮件服务器管理后台</title> requireAuth();
<style> ?>
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; } <!DOCTYPE html>
.header { background: #007bff; color: white; padding: 15px; margin: -20px -20px 20px -20px; } <html>
.menu { background: #f8f9fa; padding: 10px; margin-bottom: 20px; } <head>
.menu a { margin-right: 15px; text-decoration: none; color: #007bff; } <title>邮件服务器管理后台</title>
.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; } body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }
table { width: 100%; border-collapse: collapse; } .header { background: #007bff; color: white; padding: 15px; margin: -20px -20px 20px -20px; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; } .menu { background: #f8f9fa; padding: 10px; margin-bottom: 20px; }
th { background: #f8f9fa; } .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; }
</head> .stat-box { border: 1px solid #ddd; padding: 15px; text-align: center; }
<body> table { width: 100%; border-collapse: collapse; }
<div class="header"> th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
<h1>邮件服务器管理后台</h1> th { background: #f8f9fa; }
<div>欢迎, <?php echo htmlspecialchars($_SESSION['username']); ?> </style>
(<a href="logout.php" style="color: white;">退出</a>) </head>
</div> <body>
</div> <div class="header">
<h1>邮件服务器管理后台</h1>
<div class="menu"> <div>欢迎, <?php echo htmlspecialchars($_SESSION['username']); ?>
<a href="index.php">仪表盘</a> (<a href="logout.php" style="color: white;">退出</a>)
<?php if ($_SESSION['is_admin']) { ?> </div>
<a href="users.php">用户管理</a> </div>
<?php } ?>
<a href="emails.php">邮件管理</a> <div class="menu">
<?php if ($_SESSION['is_admin']) { ?> <a href="index.php">仪表盘</a>
<a href="broadcast.php">群发邮件</a> <?php?>
<?php } ?> <a href="users.php">用户管理</a>
<a href="filters.php">过滤规则</a> <?php ?>
<a href="logs.php">系统日志</a> <a href="broadcast.php">群发邮件</a>
<?php if ($_SESSION['is_admin']) { ?> <?php ?>
<a href="services.php">服务管理</a> <a href="filters.php">过滤规则</a>
<a href="settings.php">系统设置</a> <a href="logs.php">系统日志</a>
<?php } ?> <?php ?>
<a href="help.php">帮助</a> <a href="services.php">服务管理</a>
</div> <a href="settings.php">系统设置</a>
<?php ?>
<div class="stats"> <a href="help.php">帮助</a>
<?php </div>
$db = Database::getInstance();
<div class="stats">
// 统计用户数 <?php
$stmt = $db->query("SELECT COUNT(*) as count FROM users"); $db = Database::getInstance();
$userCount = $stmt->fetch()['count'];
// 统计用户数
// 统计邮件数 $stmt = $db->query("SELECT COUNT(*) as count FROM users");
$stmt = $db->query("SELECT COUNT(*) as count FROM emails WHERE is_deleted = 0"); $userCount = $stmt->fetch()['count'];
$emailCount = $stmt->fetch()['count'];
// 统计邮件数
// 统计今日日志 $stmt = $db->query("SELECT COUNT(*) as count FROM emails WHERE is_deleted = 0");
$stmt = $db->query("SELECT COUNT(*) as count FROM server_logs WHERE DATE(created_at) = CURDATE()"); $emailCount = $stmt->fetch()['count'];
$logCount = $stmt->fetch()['count'];
// 统计今日日志
// 统计活跃会话(简化版) $stmt = $db->query("SELECT COUNT(*) as count FROM server_logs WHERE DATE(created_at) = CURDATE()");
$activeConnections = 0; $logCount = $stmt->fetch()['count'];
?>
// 统计活跃会话(简化版)
<div class="stat-box"> $activeConnections = 0;
<h3><?php echo $userCount; ?></h3> ?>
<p>注册用户</p>
</div> <div class="stat-box">
<div class="stat-box"> <h3><?php echo $userCount; ?></h3>
<h3><?php echo $emailCount; ?></h3> <p>注册用户</p>
<p>总邮件数</p> </div>
</div> <div class="stat-box">
<div class="stat-box"> <h3><?php echo $emailCount; ?></h3>
<h3><?php echo $logCount; ?></h3> <p>总邮件数</p>
<p>今日日志</p> </div>
</div> <div class="stat-box">
<div class="stat-box"> <h3><?php echo $logCount; ?></h3>
<h3><?php echo $activeConnections; ?></h3> <p>今日日志</p>
<p>活跃连接</p> </div>
</div> <div class="stat-box">
</div> <h3><?php echo $activeConnections; ?></h3>
<p>活跃连接</p>
<h2>最近邮件</h2> </div>
<table> </div>
<thead>
<tr> <h2>最近邮件</h2>
<th>ID</th> <table>
<th>发件人</th> <thead>
<th>收件人</th> <tr>
<th>主题</th> <th>ID</th>
<th>时间</th> <th>发件人</th>
</tr> <th>收件人</th>
</thead> <th>主题</th>
<tbody> <th>时间</th>
<?php </tr>
$stmt = $db->query(" </thead>
SELECT e.*, <tbody>
COALESCE(u1.username, e.sender) as sender_name, <?php
COALESCE(u2.username, e.recipient) as recipient_name $stmt = $db->query("
FROM emails e SELECT e.*,
LEFT JOIN users u1 ON e.sender_id = u1.id COALESCE(u1.username, e.sender) as sender_name,
LEFT JOIN users u2 ON e.recipient_id = u2.id COALESCE(u2.username, e.recipient) as recipient_name
WHERE e.is_deleted = 0 FROM emails e
ORDER BY e.created_at DESC LEFT JOIN users u1 ON e.sender_id = u1.id
LIMIT 10 LEFT JOIN users u2 ON e.recipient_id = u2.id
"); WHERE e.is_deleted = 0
ORDER BY e.created_at DESC
while ($email = $stmt->fetch()) { LIMIT 10
echo "<tr>"; ");
echo "<td>{$email['id']}</td>";
echo "<td>" . htmlspecialchars($email['sender_name'] ?? '未知') . "</td>"; while ($email = $stmt->fetch()) {
echo "<td>" . htmlspecialchars($email['recipient_name'] ?? '未知') . "</td>"; echo "<tr>";
echo "<td>" . htmlspecialchars($email['subject'] ?? '(无主题)') . "</td>"; echo "<td>{$email['id']}</td>";
echo "<td>{$email['created_at']}</td>"; echo "<td>" . htmlspecialchars($email['sender_name'] ?? '未知') . "</td>";
echo "</tr>"; echo "<td>" . htmlspecialchars($email['recipient_name'] ?? '未知') . "</td>";
} echo "<td>" . htmlspecialchars($email['subject'] ?? '(无主题)') . "</td>";
?> echo "<td>{$email['created_at']}</td>";
</tbody> echo "</tr>";
</table> }
</body> ?>
</tbody>
</table>
</body>
</html> </html>

@ -0,0 +1,428 @@
<?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,256 +1,257 @@
<?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';
//
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');
$db = Database::getInstance(); exit;
$settingsRepo = new SystemSettingsRepository(); }
$message = '';
$error = ''; $db = Database::getInstance();
$settingsRepo = new SystemSettingsRepository();
// 处理清除日志 $message = '';
if (isset($_GET['clear_logs'])) { $error = '';
$logType = $_GET['clear_logs'] ?? '';
// 处理清除日志
if ($logType === 'all') { if (isset($_GET['clear_logs'])) {
$stmt = $db->prepare("DELETE FROM server_logs"); $logType = $_GET['clear_logs'] ?? '';
$stmt->execute();
$message = "所有日志已清除"; if ($logType === 'all') {
} elseif ($logType === 'smtp') { $stmt = $db->prepare("DELETE FROM server_logs");
$stmt = $db->prepare("DELETE FROM server_logs WHERE log_type = 'SMTP'"); $stmt->execute();
$stmt->execute(); $message = "所有日志已清除";
$message = "SMTP日志已清除"; } elseif ($logType === 'smtp') {
} elseif ($logType === 'pop3') { $stmt = $db->prepare("DELETE FROM server_logs WHERE log_type = 'SMTP'");
$stmt = $db->prepare("DELETE FROM server_logs WHERE log_type = 'POP3'"); $stmt->execute();
$stmt->execute(); $message = "SMTP日志已清除";
$message = "POP3日志已清除"; } elseif ($logType === 'pop3') {
} $stmt = $db->prepare("DELETE FROM server_logs WHERE log_type = 'POP3'");
} $stmt->execute();
$message = "POP3日志已清除";
// 获取日志统计 }
$stmt = $db->query("SELECT COUNT(*) as count FROM server_logs"); }
$totalLogs = $stmt->fetch()['count'];
// 获取日志统计
$stmt = $db->query("SELECT COUNT(*) as count FROM server_logs WHERE log_type = 'SMTP'"); $stmt = $db->query("SELECT COUNT(*) as count FROM server_logs");
$smtpLogs = $stmt->fetch()['count']; $totalLogs = $stmt->fetch()['count'];
$stmt = $db->query("SELECT COUNT(*) as count FROM server_logs WHERE log_type = 'POP3'"); $stmt = $db->query("SELECT COUNT(*) as count FROM server_logs WHERE log_type = 'SMTP'");
$pop3Logs = $stmt->fetch()['count']; $smtpLogs = $stmt->fetch()['count'];
// 分页参数 $stmt = $db->query("SELECT COUNT(*) as count FROM server_logs WHERE log_type = 'POP3'");
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1; $pop3Logs = $stmt->fetch()['count'];
$perPage = 50;
$offset = ($page - 1) * $perPage; // 分页参数
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
// 过滤参数 $perPage = 50;
$filterType = $_GET['type'] ?? 'all'; $offset = ($page - 1) * $perPage;
// 获取日志列表 // 过滤参数
if ($filterType === 'smtp') { $filterType = $_GET['type'] ?? 'all';
$stmt = $db->prepare("
SELECT l.*, u.username // 获取日志列表
FROM server_logs l if ($filterType === 'smtp') {
LEFT JOIN users u ON l.user_id = u.id $stmt = $db->prepare("
WHERE l.log_type = 'SMTP' 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
"); WHERE l.log_type = 'SMTP'
$stmt->execute([$perPage, $offset]); ORDER BY l.created_at DESC
LIMIT ? OFFSET ?
$countStmt = $db->prepare("SELECT COUNT(*) as count FROM server_logs WHERE log_type = 'SMTP'"); ");
$countStmt->execute(); $stmt->execute([$perPage, $offset]);
$totalLogs = $countStmt->fetch()['count'];
} elseif ($filterType === 'pop3') { $countStmt = $db->prepare("SELECT COUNT(*) as count FROM server_logs WHERE log_type = 'SMTP'");
$stmt = $db->prepare(" $countStmt->execute();
SELECT l.*, u.username $totalLogs = $countStmt->fetch()['count'];
FROM server_logs l } elseif ($filterType === 'pop3') {
LEFT JOIN users u ON l.user_id = u.id $stmt = $db->prepare("
WHERE l.log_type = 'POP3' 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
"); WHERE l.log_type = 'POP3'
$stmt->execute([$perPage, $offset]); ORDER BY l.created_at DESC
LIMIT ? OFFSET ?
$countStmt = $db->prepare("SELECT COUNT(*) as count FROM server_logs WHERE log_type = 'POP3'"); ");
$countStmt->execute(); $stmt->execute([$perPage, $offset]);
$totalLogs = $countStmt->fetch()['count'];
} else { $countStmt = $db->prepare("SELECT COUNT(*) as count FROM server_logs WHERE log_type = 'POP3'");
$stmt = $db->prepare(" $countStmt->execute();
SELECT l.*, u.username $totalLogs = $countStmt->fetch()['count'];
FROM server_logs l } else {
LEFT JOIN users u ON l.user_id = u.id $stmt = $db->prepare("
ORDER BY l.created_at DESC SELECT l.*, u.username
LIMIT ? OFFSET ? FROM server_logs l
"); LEFT JOIN users u ON l.user_id = u.id
$stmt->execute([$perPage, $offset]); ORDER BY l.created_at DESC
} LIMIT ? OFFSET ?
");
$logs = $stmt->fetchAll(); $stmt->execute([$perPage, $offset]);
$totalPages = ceil($totalLogs / $perPage); }
// 获取日志设置 $logs = $stmt->fetchAll();
$logPath = $settingsRepo->get('log_path', '/var/log/mailserver'); $totalPages = ceil($totalLogs / $perPage);
$logMaxSize = $settingsRepo->get('log_max_size', 10485760);
?> // 获取日志设置
<!DOCTYPE html> $logPath = $settingsRepo->get('log_path', '/var/log/mailserver');
<html> $logMaxSize = $settingsRepo->get('log_max_size', 10485760);
<head> ?>
<title>系统日志 - 邮件服务器</title> <!DOCTYPE html>
<meta charset="UTF-8"> <html>
<style> <head>
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; } <title>系统日志 - 邮件服务器</title>
.header { background: #007bff; color: white; padding: 15px; margin: -20px -20px 20px -20px; } <meta charset="UTF-8">
.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; } body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
.container { background: white; padding: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .header { background: #007bff; color: white; padding: 15px; margin: -20px -20px 20px -20px; }
.message { background: #d4edda; color: #155724; 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); }
.error { background: #f8d7da; color: #721c24; padding: 12px; border-radius: 5px; margin-bottom: 20px; } .menu a { margin-right: 15px; text-decoration: none; color: #007bff; }
.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin-bottom: 20px; } .container { background: white; padding: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.stat-box { border: 1px solid #ddd; padding: 15px; text-align: center; border-radius: 5px; } .message { background: #d4edda; color: #155724; padding: 12px; border-radius: 5px; margin-bottom: 20px; }
.stat-box h3 { margin: 0; font-size: 24px; color: #007bff; } .error { background: #f8d7da; color: #721c24; padding: 12px; border-radius: 5px; margin-bottom: 20px; }
.stat-box p { margin: 5px 0 0 0; color: #666; } .stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin-bottom: 20px; }
.filters { margin-bottom: 20px; } .stat-box { border: 1px solid #ddd; padding: 15px; text-align: center; border-radius: 5px; }
.filters a { display: inline-block; padding: 8px 16px; margin-right: 10px; text-decoration: none; border: 1px solid #ddd; border-radius: 4px; } .stat-box h3 { margin: 0; font-size: 24px; color: #007bff; }
.filters a.active { background: #007bff; color: white; border-color: #007bff; } .stat-box p { margin: 5px 0 0 0; color: #666; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; } .filters { margin-bottom: 20px; }
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; } .filters a { display: inline-block; padding: 8px 16px; margin-right: 10px; text-decoration: none; border: 1px solid #ddd; border-radius: 4px; }
th { background: #f8f9fa; } .filters a.active { background: #007bff; color: white; border-color: #007bff; }
.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-danger { background: #dc3545; color: white; } th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
.pagination { margin-top: 20px; text-align: center; } th { background: #f8f9fa; }
.pagination a { display: inline-block; padding: 8px 12px; margin: 0 4px; text-decoration: none; border: 1px solid #ddd; border-radius: 4px; } .btn { padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; text-decoration: none; display: inline-block; }
.pagination a:hover { background: #f8f9fa; } .btn-danger { background: #dc3545; color: white; }
.pagination .current { background: #007bff; color: white; border-color: #007bff; } .pagination { margin-top: 20px; text-align: center; }
.log-info { background: #f8f9fa; padding: 15px; border-radius: 5px; margin-bottom: 20px; } .pagination a { display: inline-block; padding: 8px 12px; margin: 0 4px; text-decoration: none; border: 1px solid #ddd; border-radius: 4px; }
</style> .pagination a:hover { background: #f8f9fa; }
</head> .pagination .current { background: #007bff; color: white; border-color: #007bff; }
<body> .log-info { background: #f8f9fa; padding: 15px; border-radius: 5px; margin-bottom: 20px; }
<div class="header"> </style>
<h1>邮件服务器管理后台</h1> </head>
<div>欢迎, <?php echo htmlspecialchars($_SESSION['username']); ?> <body>
(<a href="logout.php" style="color: white;">退出</a>) <div class="header">
</div> <h1>邮件服务器管理后台</h1>
</div> <div>欢迎, <?php echo htmlspecialchars($_SESSION['username']); ?>
(<a href="logout.php" style="color: white;">退出</a>)
<div class="menu"> </div>
<a href="index.php">仪表盘</a> </div>
<?php if ($_SESSION['is_admin'] ?? false): ?>
<a href="users.php">用户管理</a> <div class="menu">
<?php endif; ?> <a href="index.php">仪表盘</a>
<a href="emails.php">邮件管理</a> <a href="users.php">用户管理</a>
<a href="filters.php">过滤规则</a> <a href="broadcast.php">群发邮件</a>
<a href="logs.php">系统日志</a> <a href="filters.php">过滤规则</a>
<?php if ($_SESSION['is_admin'] ?? false): ?> <a href="logs.php">系统日志</a>
<a href="settings.php">系统设置</a> <a href="services.php">服务管理</a>
<?php endif; ?> <a href="settings.php">系统设置</a>
</div> <a href="help.php">帮助</a>
</div>
<div class="container">
<h2>系统日志管理</h2> <div class="container">
<h2>系统日志管理</h2>
<?php if ($message): ?>
<div class="message"><?php echo $message; ?></div> <?php if ($message): ?>
<?php endif; ?> <div class="message"><?php echo $message; ?></div>
<?php endif; ?>
<?php if ($error): ?>
<div class="error"><?php echo $error; ?></div> <?php if ($error): ?>
<?php endif; ?> <div class="error"><?php echo $error; ?></div>
<?php endif; ?>
<!-- 日志统计 -->
<div class="stats"> <!-- 日志统计 -->
<div class="stat-box"> <div class="stats">
<h3><?php echo $totalLogs; ?></h3> <div class="stat-box">
<p>总日志数</p> <h3><?php echo $totalLogs; ?></h3>
</div> <p>总日志数</p>
<div class="stat-box"> </div>
<h3><?php echo $smtpLogs; ?></h3> <div class="stat-box">
<p>SMTP日志</p> <h3><?php echo $smtpLogs; ?></h3>
</div> <p>SMTP日志</p>
<div class="stat-box"> </div>
<h3><?php echo $pop3Logs; ?></h3> <div class="stat-box">
<p>POP3日志</p> <h3><?php echo $pop3Logs; ?></h3>
</div> <p>POP3日志</p>
</div> </div>
</div>
<!-- 日志信息 -->
<div class="log-info"> <!-- 日志信息 -->
<strong>日志存储路径:</strong><?php echo htmlspecialchars($logPath); ?><br> <div class="log-info">
<strong>日志文件最大大小:</strong><?php echo round($logMaxSize / 1048576, 2); ?> MB <strong>日志存储路径:</strong><?php echo htmlspecialchars($logPath); ?><br>
</div> <strong>日志文件最大大小:</strong><?php echo round($logMaxSize / 1048576, 2); ?> MB
</div>
<!-- 过滤和操作 -->
<div class="filters"> <!-- 过滤和操作 -->
<a href="?type=all" class="<?php echo $filterType === 'all' ? 'active' : ''; ?>">全部</a> <div class="filters">
<a href="?type=smtp" class="<?php echo $filterType === 'smtp' ? 'active' : ''; ?>">SMTP日志</a> <a href="?type=all" class="<?php echo $filterType === 'all' ? 'active' : ''; ?>">全部</a>
<a href="?type=pop3" class="<?php echo $filterType === 'pop3' ? 'active' : ''; ?>">POP3日志</a> <a href="?type=smtp" class="<?php echo $filterType === 'smtp' ? 'active' : ''; ?>">SMTP日志</a>
<?php if ($_SESSION['is_admin'] ?? false): ?> <a href="?type=pop3" class="<?php echo $filterType === 'pop3' ? 'active' : ''; ?>">POP3日志</a>
<a href="?clear_logs=smtp" class="btn btn-danger" onclick="return confirm('确定要清除SMTP日志吗');">清除SMTP日志</a> <?php if ($_SESSION['is_admin'] ?? false): ?>
<a href="?clear_logs=pop3" class="btn btn-danger" onclick="return confirm('确定要清除POP3日志吗');">清除POP3日志</a> <a href="?clear_logs=smtp" class="btn btn-danger" onclick="return confirm('确定要清除SMTP日志吗');">清除SMTP日志</a>
<a href="?clear_logs=all" class="btn btn-danger" onclick="return confirm('确定要清除所有日志吗?');">清除所有日志</a> <a href="?clear_logs=pop3" class="btn btn-danger" onclick="return confirm('确定要清除POP3日志吗');">清除POP3日志</a>
<?php endif; ?> <a href="?clear_logs=all" class="btn btn-danger" onclick="return confirm('确定要清除所有日志吗?');">清除所有日志</a>
</div> <?php endif; ?>
</div>
<!-- 日志列表 -->
<table> <!-- 日志列表 -->
<thead> <table>
<tr> <thead>
<th>ID</th> <tr>
<th>类型</th> <th>ID</th>
<th>消息</th> <th>类型</th>
<th>用户</th> <th>消息</th>
<th>IP地址</th> <th>用户</th>
<th>时间</th> <th>IP地址</th>
</tr> <th>时间</th>
</thead> </tr>
<tbody> </thead>
<?php if (empty($logs)): ?> <tbody>
<tr> <?php if (empty($logs)): ?>
<td colspan="6" style="text-align: center; padding: 40px;">暂无日志</td> <tr>
</tr> <td colspan="6" style="text-align: center; padding: 40px;">暂无日志</td>
<?php else: ?> </tr>
<?php foreach ($logs as $log): ?> <?php else: ?>
<tr> <?php foreach ($logs as $log): ?>
<td><?php echo $log['id']; ?></td> <tr>
<td><?php echo htmlspecialchars($log['log_type'] ?? '-'); ?></td> <td><?php echo $log['id']; ?></td>
<td><?php echo htmlspecialchars($log['message'] ?? '-'); ?></td> <td><?php echo htmlspecialchars($log['log_type'] ?? '-'); ?></td>
<td><?php echo htmlspecialchars($log['username'] ?? '-'); ?></td> <td><?php echo htmlspecialchars($log['message'] ?? '-'); ?></td>
<td><?php echo htmlspecialchars($log['client_ip'] ?? '-'); ?></td> <td><?php echo htmlspecialchars($log['username'] ?? '-'); ?></td>
<td><?php echo $log['created_at']; ?></td> <td><?php echo htmlspecialchars($log['client_ip'] ?? '-'); ?></td>
</tr> <td><?php echo $log['created_at']; ?></td>
<?php endforeach; ?> </tr>
<?php endif; ?> <?php endforeach; ?>
</tbody> <?php endif; ?>
</table> </tbody>
</table>
<!-- 分页 -->
<?php if ($totalPages > 1): ?> <!-- 分页 -->
<div class="pagination"> <?php if ($totalPages > 1): ?>
<?php if ($page > 1): ?> <div class="pagination">
<a href="?page=<?php echo $page - 1; ?>&type=<?php echo $filterType; ?>">上一页</a> <?php if ($page > 1): ?>
<?php endif; ?> <a href="?page=<?php echo $page - 1; ?>&type=<?php echo $filterType; ?>">上一页</a>
<?php endif; ?>
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
<?php if ($i == $page): ?> <?php for ($i = 1; $i <= $totalPages; $i++): ?>
<span class="current"><?php echo $i; ?></span> <?php if ($i == $page): ?>
<?php else: ?> <span class="current"><?php echo $i; ?></span>
<a href="?page=<?php echo $i; ?>&type=<?php echo $filterType; ?>"><?php echo $i; ?></a> <?php else: ?>
<?php endif; ?> <a href="?page=<?php echo $i; ?>&type=<?php echo $filterType; ?>"><?php echo $i; ?></a>
<?php endfor; ?> <?php endif; ?>
<?php endfor; ?>
<?php if ($page < $totalPages): ?>
<a href="?page=<?php echo $page + 1; ?>&type=<?php echo $filterType; ?>">下一页</a> <?php if ($page < $totalPages): ?>
<?php endif; ?> <a href="?page=<?php echo $page + 1; ?>&type=<?php echo $filterType; ?>">下一页</a>
</div> <?php endif; ?>
<?php endif; ?> </div>
</div> <?php endif; ?>
</body> </div>
</html> </body>
</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,166 +1,361 @@
<?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'])) {
header('Location: index.php'); session_start();
exit;
} // 登录验证
if (!isset($_SESSION['user_id'])) {
// 检查管理员权限 header('Location: index.php');
if (!$_SESSION['is_admin']) { exit;
die('权限不足:只有管理员可以访问此页面'); }
}
$serviceRepo = new ServiceRepository();
$serviceRepo = new ServiceRepository(); $settingsRepo = new SystemSettingsRepository();
$settingsRepo = new SystemSettingsRepository(); $message = '';
$message = ''; $error = '';
$error = '';
// 获取端口设置(放在函数定义之前)
// 处理服务起停 $smtpPort = $settingsRepo->get('smtp_port', 25);
if (isset($_GET['action'])) { $pop3Port = $settingsRepo->get('pop3_port', 110);
$serviceName = $_GET['service'] ?? '';
$action = $_GET['action'] ?? ''; function startService($serviceName, $port) {
$scriptPath = __DIR__ . "/../scripts/start_{$serviceName}.php";
if ($serviceName === 'smtp' || $serviceName === 'pop3') { $logFile = __DIR__ . "/../logs/{$serviceName}.log";
if ($action === 'start') {
// 启动服务(实际应该通过系统服务管理,这里只是更新状态) error_log("尝试启动服务: $serviceName , 端口: $port");
$pid = null; // 实际应该获取进程ID
$serviceRepo->updateStatus($serviceName, true, $pid); if (!file_exists($scriptPath)) {
$message = strtoupper($serviceName) . "服务已启动"; return ['success' => false, 'message' => '启动脚本不存在'];
} elseif ($action === 'stop') { }
// 停止服务
$status = $serviceRepo->getStatus($serviceName);
if ($status && $status['pid']) { if (isServiceRunning($serviceName, $port)) {
// 尝试终止进程 return ['success' => false, 'message' => '服务已在运行'];
@exec("kill {$status['pid']} 2>/dev/null"); }
}
$serviceRepo->updateStatus($serviceName, false, null); @file_put_contents($logFile, '');
$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'); ));
$pop3Status = $serviceRepo->getStatus('pop3'); /*$raw = shell_exec(sprintf(
$smtpRunning = $serviceRepo->isRunning('smtp'); 'cd %s && (%s > /dev/null 2>&1 </dev/null & echo $!)',
$pop3Running = $serviceRepo->isRunning('pop3'); escapeshellarg(dirname($scriptPath, 2)),
'nohup php ' . escapeshellarg($scriptPath),
// 获取端口设置 escapeshellarg($logFile) // 这一行现在仅用于占位,实际不再写
$smtpPort = $settingsRepo->get('smtp_port', 25); ));*/
$pop3Port = $settingsRepo->get('pop3_port', 110);
?> // 修复trim(null)问题
<!DOCTYPE html> $raw = (string)$raw; // 强制转换为字符串
<html> $pid = trim($raw); // 现在可以安全trim
<head>
<title>服务管理 - 邮件服务器</title> error_log("Shell命令执行结果: " . var_export($raw, true));
<meta charset="UTF-8"> error_log("获取的PID: " . $pid); // 修复:这里$pid已定义
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; } if (!is_numeric($pid) || $pid < 1) {
.header { background: #007bff; color: white; padding: 15px; margin: -20px -20px 20px -20px; } error_log("PID无效: $pid");
.menu { background: white; padding: 10px; margin-bottom: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } return ['success' => false, 'message' => '未能获取有效进程号'];
.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; } sleep(1); // 先等待1秒
.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; } $processExists = file_exists("/proc/$pid");
.status-running { background: #28a745; color: white; } error_log("进程是否存在: " . ($processExists ? "是" : "否"));
.status-stopped { background: #dc3545; color: white; }
.btn { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; text-decoration: none; display: inline-block; } if (!$processExists) {
.btn-success { background: #28a745; color: white; } // 查看脚本为什么退出了
.btn-danger { background: #dc3545; color: white; } if (file_exists($logFile)) {
.info { color: #666; font-size: 14px; margin-top: 10px; } $logContent = file_get_contents($logFile);
.note { background: #fff3cd; border: 1px solid #ffc107; padding: 15px; border-radius: 5px; margin-top: 20px; } error_log("脚本输出日志:\n" . $logContent);
</style> }
</head> return ['success' => false, 'message' => '进程已退出'];
<body> }
<div class="header">
<h1>邮件服务器管理后台</h1> /* ===== 修复:更长的等待时间 ===== */
<div>欢迎, <?php echo htmlspecialchars($_SESSION['username']); ?> $maxChecks = 6;
(<a href="logout.php" style="color: white;">退出</a>) for ($i = 0; $i < $maxChecks; $i++) {
</div> sleep(1); // 每次检查等待1秒
</div>
if (isServiceRunning($serviceName, $port)) {
<div class="menu"> // 再次确认PID仍然有效
<a href="index.php">仪表盘</a> if (file_exists("/proc/$pid")) {
<a href="users.php">用户管理</a> return ['success' => true, 'pid' => $pid];
<a href="emails.php">邮件管理</a> } else {
<a href="broadcast.php">群发邮件</a> // 进程已退出
<a href="filters.php">过滤规则</a> return ['success' => false, 'message' => '进程已退出'];
<a href="logs.php">系统日志</a> }
<a href="services.php">服务管理</a> }
<a href="settings.php">系统设置</a>
</div> error_log("第 " . ($i+1) . " 次检查:端口未监听");
}
<div class="container">
<h2>服务管理</h2> // 启动失败 → 清理孤儿
if (file_exists("/proc/$pid")) {
<?php if ($message): ?> @exec("kill -9 {$pid} 2>/dev/null");
<div class="message"><?php echo $message; ?></div> }
<?php endif; ?>
// 输出失败原因
<?php if ($error): ?> if (file_exists($logFile)) {
<div class="error"><?php echo $error; ?></div> // 只读取最后10行避免读取大文件
<?php endif; ?> $logContent = shell_exec("tail -10 " . escapeshellarg($logFile));
error_log("最终日志内容(最后100行):\n" . $logContent);
<!-- SMTP服务 --> }
<div class="service-box">
<h3>SMTP服务邮件发送</h3> return ['success' => false, 'message' => '服务端口未监听,启动失败'];
<p> }
<span class="status status-<?php echo $smtpRunning ? 'running' : 'stopped'; ?>">
<?php echo $smtpRunning ? '运行中' : '已停止'; ?>
</span> function stopService($serviceName, $pid, $port) {
<?php if ($smtpRunning): ?> // ===== 改:优先用 DB 里的 PID没有再现场嗅探 =====
<a href="?service=smtp&action=stop" class="btn btn-danger" onclick="return confirm('确定要停止SMTP服务吗');">停止服务</a> if (empty($pid) || !is_numeric($pid)) {
<?php else: ?> $pid = getServicePid($serviceName, $port);
<a href="?service=smtp&action=start" class="btn btn-success" onclick="return confirm('确定要启动SMTP服务吗');">启动服务</a> }
<?php endif; ?> if ($pid) {
</p> @exec("kill {$pid} 2>/dev/null");
<div class="info"> sleep(1);
<strong>端口:</strong><?php echo $smtpPort; ?><br> if (isServiceRunning($serviceName, $port)) {
<strong>进程ID</strong><?php echo $smtpStatus['pid'] ?? '-'; ?><br> @exec("kill -9 {$pid} 2>/dev/null");
<strong>最后启动:</strong><?php echo $smtpStatus['last_started_at'] ?? '-'; ?><br> sleep(1);
<strong>最后停止:</strong><?php echo $smtpStatus['last_stopped_at'] ?? '-'; ?> }
</div> }
</div> return !isServiceRunning($serviceName, $port);
}
<!-- POP3服务 -->
<div class="service-box"> function isServiceRunning($serviceName, $port) {
<h3>POP3服务邮件接收</h3> // 方法1: 使用单引号确保变量正确展开
<p> $cmd1 = "netstat -tlnp 2>/dev/null | grep ':" . $port . "' | grep LISTEN";
<span class="status status-<?php echo $pop3Running ? 'running' : 'stopped'; ?>"> $result1 = shell_exec($cmd1);
<?php echo $pop3Running ? '运行中' : '已停止'; ?>
</span> // 方法2: 使用ss命令更可靠
<?php if ($pop3Running): ?> $cmd2 = "ss -tlnp 2>/dev/null | grep ':" . $port . "'";
<a href="?service=pop3&action=stop" class="btn btn-danger" onclick="return confirm('确定要停止POP3服务吗');">停止服务</a> $result2 = shell_exec($cmd2);
<?php else: ?>
<a href="?service=pop3&action=start" class="btn btn-success" onclick="return confirm('确定要启动POP3服务吗');">启动服务</a> // 方法3: 使用lsof你已验证有效
<?php endif; ?> $cmd3 = "lsof -i :" . $port . " 2>/dev/null";
</p> $result3 = shell_exec($cmd3);
<div class="info">
<strong>端口:</strong><?php echo $pop3Port; ?><br> // 方法4: 直接socket连接测试最可靠
<strong>进程ID</strong><?php echo $pop3Status['pid'] ?? '-'; ?><br> $fp = @fsockopen('127.0.0.1', $port, $errno, $errstr, 1);
<strong>最后启动:</strong><?php echo $pop3Status['last_started_at'] ?? '-'; ?><br> if ($fp) {
<strong>最后停止:</strong><?php echo $pop3Status['last_stopped_at'] ?? '-'; ?> fclose($fp);
</div> return true;
</div> }
<div class="note"> // 任意一个方法有结果都算服务在运行
<strong>注意:</strong>此页面仅用于管理服务状态。实际启动服务需要使用命令行: return !empty(trim($result1 ?? '')) ||
<ul> !empty(trim($result2 ?? '')) ||
<li>SMTP服务<code>sudo php scripts/start_smtp.php</code></li> !empty(trim($result3 ?? ''));
<li>POP3服务<code>sudo php scripts/start_pop3.php</code></li> }
</ul>
</div> function getServicePid($serviceName, $port) {
</div> return trim(shell_exec("lsof -ti: {$port} 2>/dev/null | head -1"));
</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,320 +1,341 @@
<?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'])) {
header('Location: index.php'); session_start();
exit;
} // 是否登录验证
if (!isset($_SESSION['user_id'])) {
// 检查管理员权限 header('Location: index.php');
if (!$_SESSION['is_admin']) { exit;
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'])) { // 处理系统设置更新
// SMTP端口 if (isset($_POST['update_settings'])) {
if (isset($_POST['smtp_port'])) { // SMTP端口
$port = (int)$_POST['smtp_port']; if (isset($_POST['smtp_port'])) {
if (Validator::validatePort($port)) { $port = (int)$_POST['smtp_port'];
$settingsRepo->set('smtp_port', $port); if (Validator::validatePort($port)) {
} else { $settingsRepo->set('smtp_port', $port);
$error = "SMTP端口无效1-65535"; } else {
} $error = "SMTP端口无效1-65535";
} }
}
// POP3端口
if (isset($_POST['pop3_port'])) { // POP3端口
$port = (int)$_POST['pop3_port']; if (isset($_POST['pop3_port'])) {
if (Validator::validatePort($port)) { $port = (int)$_POST['pop3_port'];
$settingsRepo->set('pop3_port', $port); if (Validator::validatePort($port)) {
} else { $settingsRepo->set('pop3_port', $port);
$error = "POP3端口无效1-65535"; } else {
} $error = "POP3端口无效1-65535";
} }
}
// 域名
if (isset($_POST['domain'])) { // 域名
$domain = trim($_POST['domain']); if (isset($_POST['domain'])) {
if (!empty($domain)) { $newDomain = trim($_POST['domain']);
$settingsRepo->set('domain', $domain); if (!empty($newDomain)) {
} /* **** 调试开始 **** */
} $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 ($size > 0) { if ($newDomain !== $oldDomain) {
$settingsRepo->set('mailbox_size_limit', $size); $settingsRepo->set('domain', $newDomain);
}
} require_once __DIR__ . '/../src/admin/SyncDomainService.php';
$sync = new SyncDomainService();
// 日志路径 $count = $sync->run($oldDomain, $newDomain);
if (isset($_POST['log_path'])) { $message .= " 已同步 $count 个用户邮箱后缀。";
$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'])) {
if (empty($error)) { $size = (int)$_POST['mailbox_size_limit'];
$message = "系统设置已更新"; if ($size > 0) {
} $settingsRepo->set('mailbox_size_limit', $size);
} }
}
// 处理管理员密码修改
if (isset($_POST['change_admin_password'])) { // 日志路径
$oldPassword = $_POST['old_password'] ?? ''; if (isset($_POST['log_path'])) {
$newPassword = $_POST['new_password'] ?? ''; $settingsRepo->set('log_path', trim($_POST['log_path']));
$confirmPassword = $_POST['confirm_password'] ?? ''; }
$user = $userRepo->findById($_SESSION['user_id']); // 日志最大大小
if (isset($_POST['log_max_size'])) {
if (!Security::verifyPassword($oldPassword, $user['password_hash'])) { $size = (int)$_POST['log_max_size'];
$error = "原密码错误"; if ($size > 0) {
} else { $settingsRepo->set('log_max_size', $size);
$passwordValidation = Validator::validatePassword($newPassword, 6); }
if (!$passwordValidation['valid']) { }
$error = implode('<br>', $passwordValidation['errors']);
} elseif ($newPassword !== $confirmPassword) { if (empty($error)) {
$error = "两次输入的密码不一致"; $message = "系统设置已更新";
} else { }
if ($userRepo->update($_SESSION['user_id'], ['password' => $newPassword])) { }
$message = "管理员密码已更新";
} else { // 处理管理员密码修改
$error = "密码更新失败"; if (isset($_POST['change_admin_password'])) {
} $oldPassword = $_POST['old_password'] ?? '';
} $newPassword = $_POST['new_password'] ?? '';
} $confirmPassword = $_POST['confirm_password'] ?? '';
}
$user = $userRepo->findById($_SESSION['user_id']);
// 处理用户邮箱大小设置
if (isset($_POST['set_mailbox_size'])) { if (!Security::verifyPassword($oldPassword, $user['password_hash'])) {
$userId = (int)$_POST['user_id']; $error = "原密码错误";
$sizeBytes = (int)$_POST['mailbox_size']; } else {
$passwordValidation = Validator::validatePassword($newPassword, 6);
if ($sizeBytes > 0) { if (!$passwordValidation['valid']) {
if ($mailboxRepo->setSizeLimit($userId, $sizeBytes)) { $error = implode('<br>', $passwordValidation['errors']);
$message = "邮箱大小限制已更新"; } elseif ($newPassword !== $confirmPassword) {
} else { $error = "两次输入的密码不一致";
$error = "更新失败"; } else {
} if ($userRepo->update($_SESSION['user_id'], ['password' => $newPassword])) {
} else { $message = "管理员密码已更新";
$error = "邮箱大小必须大于0"; } else {
} $error = "密码更新失败";
} }
}
// 获取当前设置 }
$settings = $settingsRepo->getAll(); }
$users = $userRepo->getAll();
?> // 处理用户邮箱大小设置
<!DOCTYPE html> if (isset($_POST['set_mailbox_size'])) {
<html> $userId = (int)$_POST['user_id'];
<head> $sizeBytes = (int)$_POST['mailbox_size'];
<title>系统设置 - 邮件服务器</title>
<meta charset="UTF-8"> if ($sizeBytes > 0) {
<style> if ($mailboxRepo->setSizeLimit($userId, $sizeBytes)) {
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; } $message = "邮箱大小限制已更新";
.header { background: #007bff; color: white; padding: 15px; margin: -20px -20px 20px -20px; } } else {
.menu { background: white; padding: 10px; margin-bottom: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } $error = "更新失败";
.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); } } else {
.section { margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid #ddd; } $error = "邮箱大小必须大于0";
.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; } // 获取当前设置
.form-group { margin-bottom: 15px; } $settings = $settingsRepo->getAll();
.form-group label { display: block; margin-bottom: 5px; font-weight: 500; } $users = $userRepo->getAll();
.form-group input, .form-group select { width: 100%; max-width: 400px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; } ?>
.form-group small { color: #666; font-size: 12px; } <!DOCTYPE html>
.btn { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; } <html>
.btn-primary { background: #007bff; color: white; } <head>
.btn-success { background: #28a745; color: white; } <title>系统设置 - 邮件服务器</title>
table { width: 100%; border-collapse: collapse; margin-top: 15px; } <meta charset="UTF-8">
th, td { border: 1px solid #ddd; padding: 10px; text-align: left; } <style>
th { background: #f8f9fa; } body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
.size-input { width: 150px; } .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); }
</head> .menu a { margin-right: 15px; text-decoration: none; color: #007bff; }
<body> .container { background: white; padding: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
<div class="header"> .section { margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid #ddd; }
<h1>邮件服务器管理后台</h1> .section:last-child { border-bottom: none; }
<div>欢迎, <?php echo htmlspecialchars($_SESSION['username']); ?> .section h3 { margin-top: 0; color: #333; }
(<a href="logout.php" style="color: white;">退出</a>) .message { background: #d4edda; color: #155724; padding: 12px; border-radius: 5px; margin-bottom: 20px; }
</div> .error { background: #f8d7da; color: #721c24; padding: 12px; border-radius: 5px; margin-bottom: 20px; }
</div> .form-group { margin-bottom: 15px; }
.form-group label { display: block; margin-bottom: 5px; font-weight: 500; }
<div class="menu"> .form-group input, .form-group select { width: 100%; max-width: 400px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
<a href="index.php">仪表盘</a> .form-group small { color: #666; font-size: 12px; }
<a href="users.php">用户管理</a> .btn { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; }
<a href="emails.php">邮件管理</a> .btn-primary { background: #007bff; color: white; }
<a href="broadcast.php">群发邮件</a> .btn-success { background: #28a745; color: white; }
<a href="filters.php">过滤规则</a> table { width: 100%; border-collapse: collapse; margin-top: 15px; }
<a href="logs.php">系统日志</a> th, td { border: 1px solid #ddd; padding: 10px; text-align: left; }
<a href="settings.php">系统设置</a> th { background: #f8f9fa; }
</div> .size-input { width: 150px; }
</style>
<div class="container"> </head>
<h2>系统设置</h2> <body>
<div class="header">
<?php if ($message): ?> <h1>邮件服务器管理后台</h1>
<div class="message"><?php echo $message; ?></div> <div>欢迎, <?php echo htmlspecialchars($_SESSION['username']); ?>
<?php endif; ?> (<a href="logout.php" style="color: white;">退出</a>)
</div>
<?php if ($error): ?> </div>
<div class="error"><?php echo $error; ?></div>
<?php endif; ?> <div class="menu">
<a href="index.php">仪表盘</a>
<!-- 服务器端口设置 --> <a href="users.php">用户管理</a>
<div class="section"> <a href="broadcast.php">群发邮件</a>
<h3>服务器端口设置</h3> <a href="filters.php">过滤规则</a>
<form method="POST"> <a href="logs.php">系统日志</a>
<div class="form-group"> <a href="services.php">服务管理</a>
<label>SMTP端口默认25</label> <a href="settings.php">系统设置</a>
<input type="number" name="smtp_port" value="<?php echo htmlspecialchars($settings['smtp_port'] ?? '25'); ?>" min="1" max="65535" required> <a href="help.php">帮助</a>
<small>SMTP服务器监听端口</small> </div>
</div>
<div class="form-group"> <div class="container">
<label>POP3端口默认110</label> <h2>系统设置</h2>
<input type="number" name="pop3_port" value="<?php echo htmlspecialchars($settings['pop3_port'] ?? '110'); ?>" min="1" max="65535" required>
<small>POP3服务器监听端口</small> <?php if ($message): ?>
</div> <div class="message"><?php echo $message; ?></div>
<button type="submit" name="update_settings" class="btn btn-primary">保存端口设置</button> <?php endif; ?>
</form>
</div> <?php if ($error): ?>
<div class="error"><?php echo $error; ?></div>
<!-- 域名设置 --> <?php endif; ?>
<div class="section">
<h3>域名设置</h3> <!-- 服务器端口设置 -->
<form method="POST"> <div class="section">
<div class="form-group"> <h3>服务器端口设置</h3>
<label>服务器域名默认test.com</label> <form method="POST">
<input type="text" name="domain" value="<?php echo htmlspecialchars($settings['domain'] ?? 'test.com'); ?>" required> <div class="form-group">
<small>邮件服务器域名,用户邮箱必须使用此域名</small> <label>SMTP端口默认25</label>
</div> <input type="number" name="smtp_port" value="<?php echo htmlspecialchars($settings['smtp_port'] ?? '25'); ?>" min="1" max="65535" required>
<button type="submit" name="update_settings" class="btn btn-primary">保存域名设置</button> <small>SMTP服务器监听端口</small>
</form> </div>
</div> <div class="form-group">
<label>POP3端口默认110</label>
<!-- 邮箱管理 --> <input type="number" name="pop3_port" value="<?php echo htmlspecialchars($settings['pop3_port'] ?? '110'); ?>" min="1" max="65535" required>
<div class="section"> <small>POP3服务器监听端口</small>
<h3>邮箱大小管理</h3> </div>
<form method="POST"> <button type="submit" name="update_settings" class="btn btn-primary">保存端口设置</button>
<div class="form-group"> </form>
<label>默认邮箱大小限制(字节)</label> </div>
<input type="number" name="mailbox_size_limit" value="<?php echo htmlspecialchars($settings['mailbox_size_limit'] ?? '104857600'); ?>" min="1" required>
<small>默认值104857600 (100MB)</small> <!-- 域名设置 -->
</div> <div class="section">
<button type="submit" name="update_settings" class="btn btn-primary">保存默认大小</button> <h3>域名设置</h3>
</form> <form method="POST">
<div class="form-group">
<h4>用户邮箱大小设置</h4> <label>服务器域名默认test.com</label>
<table> <input type="text" name="domain" value="<?php echo htmlspecialchars($settings['domain'] ?? 'test.com'); ?>" >
<thead> <small>邮件服务器域名,用户邮箱必须使用此域名</small>
<tr> </div>
<th>用户</th>
<th>当前使用</th> <button type="submit" name="update_settings" class="btn btn-primary">保存域名设置</button>
<th>限制大小</th> </form>
<th>使用率</th> </div>
<th>操作</th>
</tr> <!-- 邮箱管理 -->
</thead> <div class="section">
<tbody> <h3>邮箱大小管理</h3>
<?php foreach ($users as $user): ?> <form method="POST">
<?php <div class="form-group">
$usage = $mailboxRepo->getUsage($user['id']); <label>默认邮箱大小限制(字节)</label>
$usedMB = round($usage['used'] / 1048576, 2); <input type="number" name="mailbox_size_limit" value="<?php echo htmlspecialchars($settings['mailbox_size_limit'] ?? '104857600'); ?>" min="1" required>
$limitMB = round($usage['limit'] / 1048576, 2); <small>默认值104857600 (100MB)</small>
?> </div>
<tr> <button type="submit" name="update_settings" class="btn btn-primary">保存默认大小</button>
<td><?php echo htmlspecialchars($user['username']); ?></td> </form>
<td><?php echo $usedMB; ?> MB</td>
<td><?php echo $limitMB; ?> MB</td> <h4>用户邮箱大小设置</h4>
<td><?php echo $usage['percentage']; ?>%</td> <table>
<td> <thead>
<form method="POST" style="display: inline;"> <tr>
<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> <th>操作</th>
</tr> </tr>
<?php endforeach; ?> </thead>
</tbody> <tbody>
</table> <?php foreach ($users as $user): ?>
</div> <?php
$usage = $mailboxRepo->getUsage($user['id']);
<!-- 日志设置 --> $usedMB = round($usage['used'] / 1048576, 2);
<div class="section"> $limitMB = round($usage['limit'] / 1048576, 2);
<h3>日志设置</h3> ?>
<form method="POST"> <tr>
<div class="form-group"> <td><?php echo htmlspecialchars($user['username']); ?></td>
<label>日志文件存储路径</label> <td><?php echo $usedMB; ?> MB</td>
<input type="text" name="log_path" value="<?php echo htmlspecialchars($settings['log_path'] ?? '/var/log/mailserver'); ?>" required> <td><?php echo $limitMB; ?> MB</td>
<small>日志文件存储的目录路径</small> <td><?php echo $usage['percentage']; ?>%</td>
</div> <td>
<div class="form-group"> <form method="POST" style="display: inline;">
<label>日志文件最大大小(字节)</label> <input type="hidden" name="user_id" value="<?php echo $user['id']; ?>">
<input type="number" name="log_max_size" value="<?php echo htmlspecialchars($settings['log_max_size'] ?? '10485760'); ?>" min="1" required> <input type="number" name="mailbox_size" value="<?php echo $usage['limit']; ?>" class="size-input" min="1" required>
<small>默认值10485760 (10MB)</small> <button type="submit" name="set_mailbox_size" class="btn btn-success">设置</button>
</div> </form>
<button type="submit" name="update_settings" class="btn btn-primary">保存日志设置</button> </td>
</form> </tr>
</div> <?php endforeach; ?>
</tbody>
<!-- 管理员密码修改 --> </table>
<div class="section"> </div>
<h3>修改管理员密码</h3>
<form method="POST"> <!-- 日志设置 -->
<div class="form-group"> <div class="section">
<label>原密码</label> <h3>日志设置</h3>
<input type="password" name="old_password" required> <form method="POST">
</div> <div class="form-group">
<div class="form-group"> <label>日志文件存储路径</label>
<label>新密码</label> <input type="text" name="log_path" value="<?php echo htmlspecialchars($settings['log_path'] ?? '/var/log/mailserver'); ?>" required>
<input type="password" name="new_password" required minlength="6"> <small>日志文件存储的目录路径</small>
<small>密码长度至少6个字符</small> </div>
</div> <div class="form-group">
<div class="form-group"> <label>日志文件最大大小(字节)</label>
<label>确认新密码</label> <input type="number" name="log_max_size" value="<?php echo htmlspecialchars($settings['log_max_size'] ?? '10485760'); ?>" min="1" required>
<input type="password" name="confirm_password" required minlength="6"> <small>默认值10485760 (10MB)</small>
</div> </div>
<button type="submit" name="change_admin_password" class="btn btn-primary">修改密码</button> <button type="submit" name="update_settings" class="btn btn-primary">保存日志设置</button>
</form> </form>
</div> </div>
</div>
</body> <!-- 管理员密码修改 -->
</html> <div class="section">
<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,292 +1,299 @@
<?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'])) { // 身份验证
header('Location: index.php'); if (!isset($_SESSION['user_id'])) {
exit; header('Location: index.php');
} exit;
}
// 检查管理员权限
if (!$_SESSION['is_admin']) { $settingsRepo = new SystemSettingsRepository();
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, 'test.com')) { if (!Validator::validateEmailDomain($username, $domain)) {
$error = "邮箱域名必须是 @test.com"; $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 {
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'];
} }**/
// 管理员权限总是更新
if (empty($error) && !empty($data)) { $data['is_admin'] = isset($_POST['is_admin']) ? 1 : 0;
if ($userRepo->update($userId, $data)) {
$message = "用户更新成功"; // 激活状态也是
} else { $data['is_active'] = isset($_POST['is_active']) ? 1 : 0;
$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 = "用户删除成功"; // 处理删除用户
} else { if (isset($_GET['delete'])) {
$error = "删除失败"; $userId = (int)$_GET['delete'];
} if ($userId != $_SESSION['user_id']) { // 不能删除自己
} else { if ($userRepo->delete($userId)) {
$error = "不能删除自己的账号"; $message = "用户删除成功";
} } else {
} $error = "删除失败";
}
// 获取所有用户 } else {
$users = $userRepo->getAll(); $error = "不能删除自己的账号";
?> }
<!DOCTYPE html> }
<html>
<head> // 获取所有用户
<title>用户管理 - 邮件服务器</title> $users = $userRepo->getAll();
<meta charset="UTF-8"> ?>
<style> <!DOCTYPE html>
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; } <html>
.header { background: #007bff; color: white; padding: 15px; margin: -20px -20px 20px -20px; } <head>
.menu { background: white; padding: 10px; margin-bottom: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } <title>用户管理 - 邮件服务器</title>
.menu a { margin-right: 15px; text-decoration: none; color: #007bff; } <meta charset="UTF-8">
.container { background: white; padding: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } <style>
.message { background: #d4edda; color: #155724; padding: 12px; border-radius: 5px; margin-bottom: 20px; } body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
.error { background: #f8d7da; color: #721c24; padding: 12px; border-radius: 5px; margin-bottom: 20px; } .header { background: #007bff; color: white; padding: 15px; margin: -20px -20px 20px -20px; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; } .menu { background: white; padding: 10px; margin-bottom: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; } .menu a { margin-right: 15px; text-decoration: none; color: #007bff; }
th { background: #f8f9fa; font-weight: 600; } .container { background: white; padding: 20px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
tr:hover { background: #f8f9fa; } .message { background: #d4edda; color: #155724; 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; } .error { background: #f8d7da; color: #721c24; padding: 12px; border-radius: 5px; margin-bottom: 20px; }
.btn-primary { background: #007bff; color: white; } table { width: 100%; border-collapse: collapse; margin-top: 20px; }
.btn-danger { background: #dc3545; color: white; } th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
.btn-success { background: #28a745; color: white; } th { background: #f8f9fa; font-weight: 600; }
.btn-small { padding: 4px 8px; font-size: 12px; } tr:hover { background: #f8f9fa; }
.form-group { margin-bottom: 15px; } .btn { padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; text-decoration: none; display: inline-block; }
.form-group label { display: block; margin-bottom: 5px; font-weight: 500; } .btn-primary { background: #007bff; color: white; }
.form-group input, .form-group select { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; } .btn-danger { background: #dc3545; color: white; }
.form-inline { display: flex; gap: 10px; align-items: flex-end; } .btn-success { background: #28a745; color: white; }
.form-inline .form-group { flex: 1; margin-bottom: 0; } .btn-small { padding: 4px 8px; font-size: 12px; }
.badge { padding: 4px 8px; border-radius: 3px; font-size: 12px; font-weight: 500; } .form-group { margin-bottom: 15px; }
.badge-admin { background: #ffc107; color: #000; } .form-group label { display: block; margin-bottom: 5px; font-weight: 500; }
.badge-active { background: #28a745; color: white; } .form-group input, .form-group select { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
.badge-inactive { background: #6c757d; color: white; } .form-inline { display: flex; gap: 10px; align-items: flex-end; }
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); } .form-inline .form-group { flex: 1; margin-bottom: 0; }
.modal-content { background: white; margin: 50px auto; padding: 20px; width: 500px; border-radius: 5px; } .badge { padding: 4px 8px; border-radius: 3px; font-size: 12px; font-weight: 500; }
.close { float: right; font-size: 28px; font-weight: bold; cursor: pointer; } .badge-admin { background: #ffc107; color: #000; }
</style> .badge-active { background: #28a745; color: white; }
</head> .badge-inactive { background: #6c757d; color: white; }
<body> .modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); }
<div class="header"> .modal-content { background: white; margin: 50px auto; padding: 20px; width: 500px; border-radius: 5px; }
<h1>邮件服务器管理后台</h1> .close { float: right; font-size: 28px; font-weight: bold; cursor: pointer; }
<div>欢迎, <?php echo htmlspecialchars($_SESSION['username']); ?> </style>
(<a href="logout.php" style="color: white;">退出</a>) </head>
</div> <body>
</div> <div class="header">
<h1>邮件服务器管理后台</h1>
<div class="menu"> <div>欢迎, <?php echo htmlspecialchars($_SESSION['username']); ?>
<a href="index.php">仪表盘</a> (<a href="logout.php" style="color: white;">退出</a>)
<a href="users.php">用户管理</a> </div>
<a href="emails.php">邮件管理</a> </div>
<a href="filters.php">过滤规则</a>
<a href="logs.php">系统日志</a> <div class="menu">
<a href="settings.php">系统设置</a> <a href="index.php">仪表盘</a>
</div> <a href="users.php">用户管理</a>
<a href="broadcast.php">群发邮件</a>
<div class="container"> <a href="filters.php">过滤规则</a>
<h2>用户管理</h2> <a href="logs.php">系统日志</a>
<a href="services.php">服务管理</a>
<?php if ($message): ?> <a href="settings.php">系统设置</a>
<div class="message"><?php echo $message; ?></div> <a href="help.php">帮助</a>
<?php endif; ?> </div>
<?php if ($error): ?> <div class="container">
<div class="error"><?php echo $error; ?></div> <h2>用户管理</h2>
<?php endif; ?>
<?php if ($message): ?>
<!-- 创建用户表单 --> <div class="message"><?php echo $message; ?></div>
<h3>创建新用户</h3> <?php endif; ?>
<form method="POST" class="form-inline">
<div class="form-group"> <?php if ($error): ?>
<label>邮箱地址</label> <div class="error"><?php echo $error; ?></div>
<input type="email" name="username" placeholder="user@test.com" required> <?php endif; ?>
</div>
<div class="form-group"> <!-- 创建用户表单 -->
<label>密码</label> <h3>创建新用户</h3>
<input type="password" name="password" placeholder="至少6个字符" required minlength="6"> <form method="POST" class="form-inline">
</div> <div class="form-group">
<div class="form-group"> <label>邮箱地址</label>
<label>管理员</label> <input type="email" name="username" placeholder="user@<?= htmlspecialchars($domain) ?>" required>
<input type="checkbox" name="is_admin" value="1"> </div>
</div> <div class="form-group">
<div class="form-group"> <label>密码</label>
<label>激活</label> <input type="password" name="password" placeholder="至少6个字符" required minlength="6">
<input type="checkbox" name="is_active" value="1" checked> </div>
</div> <div class="form-group">
<div class="form-group"> <label>管理员</label>
<button type="submit" name="create_user" class="btn btn-primary">创建用户</button> <input type="checkbox" name="is_admin" value="1">
</div> </div>
</form> <div class="form-group">
<label>激活</label>
<!-- 用户列表 --> <input type="checkbox" name="is_active" value="1" checked>
<h3>用户列表 (<?php echo count($users); ?>)</h3> </div>
<table> <div class="form-group">
<thead> <button type="submit" name="create_user" class="btn btn-primary">创建用户</button>
<tr> </div>
<th>ID</th> </form>
<th>用户名</th>
<th>角色</th> <!-- 用户列表 -->
<th>状态</th> <h3>用户列表 (<?php echo count($users); ?>)</h3>
<th>创建时间</th> <table>
<th>操作</th> <thead>
</tr> <tr>
</thead> <th>ID</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> <th>操作</th>
<td> </tr>
<?php if ($user['is_admin']): ?> </thead>
<span class="badge badge-admin">管理员</span> <tbody>
<?php else: ?> <?php foreach ($users as $user): ?>
<span>普通用户</span> <tr>
<?php endif; ?> <td><?php echo $user['id']; ?></td>
</td> <td><?php echo htmlspecialchars($user['username']); ?></td>
<td> <td>
<?php if ($user['is_active']): ?> <?php if ($user['is_admin']): ?>
<span class="badge badge-active">激活</span> <span class="badge badge-admin">管理员</span>
<?php else: ?> <?php else: ?>
<span class="badge badge-inactive">禁用</span> <span>普通用户</span>
<?php endif; ?> <?php endif; ?>
</td> </td>
<td><?php echo $user['created_at']; ?></td> <td>
<td> <?php if ($user['is_active']): ?>
<a href="#" onclick="editUser(<?php echo htmlspecialchars(json_encode($user)); ?>); return false;" class="btn btn-primary btn-small">编辑</a> <span class="badge badge-active">激活</span>
<?php if ($user['id'] != $_SESSION['user_id']): ?> <?php else: ?>
<a href="?delete=<?php echo $user['id']; ?>" class="btn btn-danger btn-small" onclick="return confirm('确定要删除此用户吗?');">删除</a> <span class="badge badge-inactive">禁用</span>
<?php endif; ?> <?php endif; ?>
</td> </td>
</tr> <td><?php echo $user['created_at']; ?></td>
<?php endforeach; ?> <td>
</tbody> <a href="#" onclick="editUser(<?php echo htmlspecialchars(json_encode($user)); ?>); return false;" class="btn btn-primary btn-small">编辑</a>
</table> <?php if ($user['id'] != $_SESSION['user_id']): ?>
</div> <a href="?delete=<?php echo $user['id']; ?>" class="btn btn-danger btn-small" onclick="return confirm('确定要删除此用户吗?');">删除</a>
<?php endif; ?>
<!-- 编辑用户模态框 --> </td>
<div id="editModal" class="modal"> </tr>
<div class="modal-content"> <?php endforeach; ?>
<span class="close" onclick="closeModal()">&times;</span> </tbody>
<h3>编辑用户</h3> </table>
<form method="POST"> </div>
<input type="hidden" name="user_id" id="edit_user_id">
<div class="form-group"> <!-- 编辑用户模态框 -->
<label>用户名</label> <div id="editModal" class="modal">
<input type="text" id="edit_username" readonly style="background: #f5f5f5;"> <div class="modal-content">
</div> <span class="close" onclick="closeModal()">&times;</span>
<div class="form-group"> <h3>编辑用户</h3>
<label>新密码(留空则不修改)</label> <form method="POST">
<input type="password" name="new_password" placeholder="留空则不修改"> <input type="hidden" name="user_id" id="edit_user_id">
</div> <div class="form-group">
<div class="form-group"> <label>用户名</label>
<label> <input type="text" id="edit_username" readonly style="background: #f5f5f5;">
<input type="checkbox" name="is_admin" id="edit_is_admin" value="1"> 管理员 </div>
</label> <div class="form-group">
</div> <label>新密码(留空则不修改)</label>
<div class="form-group"> <input type="password" name="new_password" placeholder="留空则不修改">
<label> </div>
<input type="checkbox" name="is_active" id="edit_is_active" value="1"> 激活 <div class="form-group">
</label> <label>
</div> <input type="checkbox" name="is_admin" id="edit_is_admin" value="1"> 管理员
<button type="submit" name="update_user" class="btn btn-success">保存</button> </label>
<button type="button" onclick="closeModal()" class="btn">取消</button> </div>
</form> <div class="form-group">
</div> <label>
</div> <input type="checkbox" name="is_active" id="edit_is_active" value="1"> 激活
</label>
<script> </div>
function editUser(user) { <button type="submit" name="update_user" class="btn btn-success">保存</button>
document.getElementById('edit_user_id').value = user.id; <button type="button" onclick="closeModal()" class="btn">取消</button>
document.getElementById('edit_username').value = user.username; </form>
document.getElementById('edit_is_admin').checked = user.is_admin == 1; </div>
document.getElementById('edit_is_active').checked = user.is_active == 1; </div>
document.getElementById('editModal').style.display = 'block';
} <script>
function editUser(user) {
function closeModal() { document.getElementById('edit_user_id').value = user.id;
document.getElementById('editModal').style.display = 'none'; document.getElementById('edit_username').value = user.username;
} document.getElementById('edit_is_admin').checked = user.is_admin == 1;
document.getElementById('edit_is_active').checked = user.is_active == 1;
window.onclick = function(event) { document.getElementById('editModal').style.display = 'block';
var modal = document.getElementById('editModal'); }
if (event.target == modal) {
closeModal(); function closeModal() {
} document.getElementById('editModal').style.display = 'none';
} }
</script>
</body> window.onclick = function(event) {
</html> var modal = document.getElementById('editModal');
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,18 +1,19 @@
#!/usr/bin/env php #!/usr/bin/env php
<?php <?php
require_once __DIR__ . '/../src/protocol/Pop3Server.php'; // 打印当前目录
error_log('start_smtp.php CWD: ' . getcwd());
echo "启动最简POP3邮件服务器\n"; error_log('start_smtp.php __DIR__: ' . __DIR__);
echo "==============================\n\n";
require_once __DIR__.'/../src/storage/SystemSettingsRepository.php';
// 检查是否有权限监听110端口需要sudo require_once __DIR__ . '/../src/protocol/Pop3Server.php';
if (posix_getuid() != 0) {
echo "错误需要管理员权限监听110端口\n"; $settingsRepo = new SystemSettingsRepository();
echo "请使用sudo php " . __FILE__ . "\n"; // 获取端口设置(放在函数定义之前)
echo "或者使用其他端口(需要修改代码)\n"; $pop3Port = $settingsRepo->get('pop3_port', 110);
exit(1);
} echo "启动最简POP3邮件服务器\n";
echo "==============================\n\n";
$server = new SimplePop3Server();
$server->start(); $server = new SimplePop3Server('0.0.0.0',$pop3Port);
$server->start();
?> ?>

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

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

@ -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
]); ]);
} }
} }

@ -0,0 +1,32 @@
<?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();
}
}

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

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

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