Compare commits

...

19 Commits

Author SHA1 Message Date
胡飞林 2d5e8d4716 提交
4 weeks ago
hnu202326010131 0ef2c10687 refactor(auth): keep login unchanged; feat(mail): pass USER from session, add inbox actions and compose user param
4 weeks ago
hnu202326010131 0b8d7b002b feat(mail): add inbox and compose pages, routing fixes, inline messages; chore: backend ops guide and login connection status; fix: white screen by adding top-level routes
4 weeks ago
echo 18abe265ca backend_3
4 weeks ago
hnu202326010131 a8b1bb64fe 前端重构:新增 Vue3+Vite 子项目(smtp-mail-admin-master-vue3),集成路由与登录守卫、Element Plus 按需加载、Axios 封装与环境变量配置;将开发服务绑定 0.0.0.0,固定端口 5174,并新增 allowedHosts 以支持自定义域名访问;修复登录与主页资源引用,保证在本机与局域网正常预览。
4 weeks ago
胡飞林 c501baad67 提交
4 weeks ago
echo 00c6009b55 沈永佳后备后端
4 weeks ago
hnu202326010131 802ef1ee2b 同步当前代码,强制更新 develop 分支
4 weeks ago
hnu202326010131 da321c9829 后端:登录改为数据库校验(支持 bcrypt),新 mysql2/bcryptjs 依赖;提供 /api/db/ping\n前端:登录页仅依据后端返 success 设置登录态;移除用户页登录模块\n其他:修复依赖并更新连接配置
4 weeks ago
hnu202326010131 f34f525aa8 frontend: refactor to pure Vue3 with componentized views; add hash routing; auth gate; loader/error feedback; remove login section from users view\nbackend: add Node/Express API server; fix iconv-lite deps; add MySQL connection via DATABASE_URL and /api/db/ping; implement credential validation with ADMIN_USER/ADMIN_PASSWORD
4 weeks ago
hnu202326010131 2e522414aa refactor(frontend): remove Android container, keep pure Vue 3 web
4 weeks ago
hnu202326010131 72aebacbec chore: remove mailbox directory from develop
4 weeks ago
胡飞林 9649c5c1a2 提交
4 weeks ago
hnu202326010131 23c779a10e 重构:前端迁移至 Vue3 SFC;移除 App 端;启用后端 CORS;更新数据库端口为 33979
4 weeks ago
hnu202326010131 8224a21df0 feat(frontend): init Vite dev server on 5174 and dynamic admin UI
4 weeks ago
litao dbb96a6781 后端代码及API文档及部署手册
4 weeks ago
hnu202326010131 ec9ee4e18e chore: 推送本地改动
4 weeks ago
hnu202326010131 ddb811bac7 chore(vite): 绑定 0.0.0.0 并允许域名 gmaeskhqybqt.sealoshzh.site
4 weeks ago
hnu202326010131 fb296f7b3e feat(frontend): add Vue 3 mobile scaffold and demo; Vite dev port 5174
4 weeks ago

18
.gitignore vendored

@ -0,0 +1,18 @@
node_modules/
dist/
.vite/
*.log
logs/
mailbox/backend/logs/
build/
target/
.idea/
.vscode/
coverage/
.env*
.DS_Store
Thumbs.db
npm-debug.log*
yarn-debug.log*
yarn-error.log*

File diff suppressed because it is too large Load Diff

@ -0,0 +1,307 @@
<?php
/**
* 通讯录API
*/
// 设置错误报告
error_reporting(E_ALL);
ini_set('display_errors', 0);
// 设置JSON响应头
header('Content-Type: application/json; charset=utf-8');
// 包含必要的类
require_once '../../utils/Config.php';
require_once '../../utils/Logger.php';
require_once '../../utils/Helper.php';
require_once '../../utils/Database.php';
// 处理请求
try {
// 获取请求方法
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
// 连接数据库
$db = Database::getInstance();
// 根据请求方法处理
switch ($method) {
case 'GET':
// 获取通讯录
if (isset($_GET['id'])) {
// 获取单个联系人详情
getContactDetails($db, $_GET['id']);
} else {
// 获取联系人列表
getContacts($db);
}
break;
case 'POST':
// 添加联系人
addContact($db);
break;
case 'PUT':
// 编辑联系人
editContact($db);
break;
case 'DELETE':
// 删除联系人
deleteContact($db);
break;
default:
echo json_encode([
'success' => false,
'message' => '不支持的请求方法'
]);
break;
}
} catch (Exception $e) {
echo json_encode([
'success' => false,
'message' => '服务器内部错误: ' . $e->getMessage()
]);
}
/**
* 获取联系人列表
* @param Database $db 数据库实例
*/
function getContacts($db) {
// 获取请求参数
$username = isset($_GET['username']) ? $_GET['username'] : '';
$search = isset($_GET['search']) ? $_GET['search'] : '';
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$perPage = isset($_GET['perPage']) ? (int)$_GET['perPage'] : 20;
// 验证必要参数
if (empty($username)) {
echo json_encode([
'success' => false,
'message' => '缺少用户名参数'
]);
return;
}
// 构建查询条件
$where = "WHERE user_id = ?";
$params = [$username];
if (!empty($search)) {
$where .= " AND (name LIKE ? OR email LIKE ? OR phone LIKE ?)";
$searchParam = "%$search%";
$params[] = $searchParam;
$params[] = $searchParam;
$params[] = $searchParam;
}
// 计算偏移量
$offset = ($page - 1) * $perPage;
// 查询总数
$totalSql = "SELECT COUNT(*) as total FROM contacts $where";
$totalResult = $db->fetchOne($totalSql, $params);
$total = $totalResult['total'];
// 查询联系人列表
$sql = "SELECT id, name, email, phone, company, department, position, create_time, update_time FROM contacts $where ORDER BY name ASC LIMIT ? OFFSET ?";
$params[] = $perPage;
$params[] = $offset;
$contacts = $db->fetchAll($sql, $params);
// 返回响应
echo json_encode([
'success' => true,
'data' => [
'contacts' => $contacts,
'total' => $total,
'page' => $page,
'perPage' => $perPage,
'totalPages' => ceil($total / $perPage)
]
]);
}
/**
* 获取单个联系人详情
* @param Database $db 数据库实例
* @param int $id 联系人ID
*/
function getContactDetails($db, $id) {
// 获取请求参数
$username = isset($_GET['username']) ? $_GET['username'] : '';
// 验证必要参数
if (empty($username)) {
echo json_encode([
'success' => false,
'message' => '缺少用户名参数'
]);
return;
}
// 查询联系人详情
$sql = "SELECT id, name, email, phone, company, department, position, create_time, update_time FROM contacts WHERE id = ? AND user_id = ?";
$contact = $db->fetchOne($sql, [$id, $username]);
if ($contact) {
echo json_encode([
'success' => true,
'data' => [
'contact' => $contact
]
]);
} else {
echo json_encode([
'success' => false,
'message' => '联系人不存在或无权访问'
]);
}
}
/**
* 添加联系人
* @param Database $db 数据库实例
*/
function addContact($db) {
// 获取请求数据
$data = json_decode(file_get_contents('php://input'), true);
// 验证请求数据
if (!$data || empty($data['username']) || empty($data['name']) || empty($data['email'])) {
echo json_encode([
'success' => false,
'message' => '缺少必要参数'
]);
return;
}
$username = $data['username'];
// 检查联系人是否已存在
$existingContact = $db->fetchOne("SELECT id FROM contacts WHERE email = ? AND user_id = ?", [$data['email'], $username]);
if ($existingContact) {
echo json_encode([
'success' => false,
'message' => '该邮箱已存在于通讯录中'
]);
return;
}
// 插入联系人
$sql = "INSERT INTO contacts (user_id, name, email, phone, company, department, position) VALUES (?, ?, ?, ?, ?, ?, ?)";
$db->insert($sql, [
$username,
$data['name'],
$data['email'],
isset($data['phone']) ? $data['phone'] : '',
isset($data['company']) ? $data['company'] : '',
isset($data['department']) ? $data['department'] : '',
isset($data['position']) ? $data['position'] : ''
]);
echo json_encode([
'success' => true,
'message' => '联系人添加成功'
]);
}
/**
* 编辑联系人
* @param Database $db 数据库实例
*/
function editContact($db) {
// 获取请求数据
$data = json_decode(file_get_contents('php://input'), true);
// 验证请求数据
if (!$data || empty($data['id']) || empty($data['username']) || empty($data['name']) || empty($data['email'])) {
echo json_encode([
'success' => false,
'message' => '缺少必要参数'
]);
return;
}
$id = $data['id'];
$username = $data['username'];
// 检查联系人是否存在
$existingContact = $db->fetchOne("SELECT id FROM contacts WHERE id = ? AND user_id = ?", [$id, $username]);
if (!$existingContact) {
echo json_encode([
'success' => false,
'message' => '联系人不存在或无权访问'
]);
return;
}
// 检查邮箱是否已被其他联系人使用
$emailExists = $db->fetchOne("SELECT id FROM contacts WHERE email = ? AND id != ? AND user_id = ?", [$data['email'], $id, $username]);
if ($emailExists) {
echo json_encode([
'success' => false,
'message' => '该邮箱已存在于通讯录中'
]);
return;
}
// 更新联系人
$sql = "UPDATE contacts SET name = ?, email = ?, phone = ?, company = ?, department = ?, position = ? WHERE id = ? AND user_id = ?";
$db->update($sql, [
$data['name'],
$data['email'],
isset($data['phone']) ? $data['phone'] : '',
isset($data['company']) ? $data['company'] : '',
isset($data['department']) ? $data['department'] : '',
isset($data['position']) ? $data['position'] : '',
$id,
$username
]);
echo json_encode([
'success' => true,
'message' => '联系人编辑成功'
]);
}
/**
* 删除联系人
* @param Database $db 数据库实例
*/
function deleteContact($db) {
// 获取请求参数
$id = isset($_GET['id']) ? $_GET['id'] : '';
$username = isset($_GET['username']) ? $_GET['username'] : '';
// 验证必要参数
if (empty($id) || empty($username)) {
echo json_encode([
'success' => false,
'message' => '缺少必要参数'
]);
return;
}
// 检查联系人是否存在
$existingContact = $db->fetchOne("SELECT id FROM contacts WHERE id = ? AND user_id = ?", [$id, $username]);
if (!$existingContact) {
echo json_encode([
'success' => false,
'message' => '联系人不存在或无权访问'
]);
return;
}
// 删除联系人
$sql = "DELETE FROM contacts WHERE id = ? AND user_id = ?";
$db->delete($sql, [$id, $username]);
echo json_encode([
'success' => true,
'message' => '联系人删除成功'
]);
}

@ -0,0 +1,324 @@
<?php
/**
* 邮件管理API
*/
// 设置错误报告
error_reporting(E_ALL);
ini_set('display_errors', 0);
// 设置JSON响应头
header('Content-Type: application/json; charset=utf-8');
// 包含必要的类
require_once '../../utils/Config.php';
require_once '../../utils/Logger.php';
require_once '../../utils/Helper.php';
require_once '../../utils/Database.php';
// 处理请求
try {
// 获取请求方法
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
// 连接数据库
$db = Database::getInstance();
// 根据请求方法处理
switch ($method) {
case 'GET':
// 获取邮件列表或邮件详情
if (isset($_GET['id'])) {
// 获取邮件详情
getEmailDetails($db, $_GET['id']);
} else {
// 获取邮件列表
getEmailList($db);
}
break;
case 'POST':
// 发送邮件或保存草稿
saveEmail($db);
break;
case 'PUT':
// 更新邮件(标记为已读/未读、移动文件夹等)
updateEmail($db);
break;
case 'DELETE':
// 删除邮件
deleteEmail($db);
break;
default:
echo json_encode([
'success' => false,
'message' => '不支持的请求方法'
]);
break;
}
} catch (Exception $e) {
echo json_encode([
'success' => false,
'message' => '服务器内部错误: ' . $e->getMessage()
]);
}
/**
* 获取邮件列表
* @param Database $db 数据库实例
*/
function getEmailList($db) {
// 获取请求参数
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$perPage = isset($_GET['perPage']) ? (int)$_GET['perPage'] : 10;
$folder = isset($_GET['folder']) ? $_GET['folder'] : 'inbox';
$search = isset($_GET['search']) ? $_GET['search'] : '';
$isRead = isset($_GET['isRead']) ? $_GET['isRead'] : null;
$username = isset($_GET['username']) ? $_GET['username'] : '';
// 验证必要参数
if (empty($username)) {
echo json_encode([
'success' => false,
'message' => '缺少用户名参数'
]);
return;
}
// 构建查询条件
$where = "WHERE rcpt_to = (SELECT email FROM user WHERE username = ?) AND folder = ? AND is_deleted = 0";
$params = [$username, $folder];
if (!empty($search)) {
$where .= " AND (subject LIKE ? OR `from` LIKE ? OR `to` LIKE ?)";
$searchParam = "%$search%";
$params[] = $searchParam;
$params[] = $searchParam;
$params[] = $searchParam;
}
if ($isRead !== null) {
$where .= " AND is_read = ?";
$params[] = (int)$isRead;
}
// 计算偏移量
$offset = ($page - 1) * $perPage;
// 查询总数
$totalSql = "SELECT COUNT(*) as total FROM email $where";
$totalResult = $db->fetchOne($totalSql, $params);
$total = $totalResult['total'];
// 查询邮件列表
$sql = "SELECT id, `from`, `to`, subject, date, folder, is_read, is_deleted FROM email $where ORDER BY date DESC LIMIT ? OFFSET ?";
$params[] = $perPage;
$params[] = $offset;
$emails = $db->fetchAll($sql, $params);
// 返回响应
echo json_encode([
'success' => true,
'data' => [
'emails' => $emails,
'total' => $total,
'page' => $page,
'perPage' => $perPage,
'totalPages' => ceil($total / $perPage)
]
]);
}
/**
* 获取邮件详情
* @param Database $db 数据库实例
* @param string $id 邮件ID
*/
function getEmailDetails($db, $id) {
// 获取请求参数
$username = isset($_GET['username']) ? $_GET['username'] : '';
// 验证必要参数
if (empty($username)) {
echo json_encode([
'success' => false,
'message' => '缺少用户名参数'
]);
return;
}
// 查询邮件详情
$sql = "SELECT * FROM email WHERE id = ? AND rcpt_to = (SELECT email FROM user WHERE username = ?)";
$email = $db->fetchOne($sql, [$id, $username]);
if ($email) {
// 标记为已读
if (!$email['is_read']) {
$db->execute("UPDATE email SET is_read = 1 WHERE id = ?", [$id]);
$email['is_read'] = 1;
}
echo json_encode([
'success' => true,
'data' => [
'email' => $email
]
]);
} else {
echo json_encode([
'success' => false,
'message' => '邮件不存在或无权访问'
]);
}
}
/**
* 发送邮件或保存草稿
* @param Database $db 数据库实例
*/
function saveEmail($db) {
// 获取请求数据
$data = json_decode(file_get_contents('php://input'), true);
// 验证请求数据
if (!$data || empty($data['username']) || empty($data['to']) || empty($data['subject'])) {
echo json_encode([
'success' => false,
'message' => '缺少必要参数'
]);
return;
}
// 获取用户邮箱
$user = $db->fetchOne("SELECT email FROM user WHERE username = ?", [$data['username']]);
if (!$user) {
echo json_encode([
'success' => false,
'message' => '用户不存在'
]);
return;
}
// 构建邮件数据
$emailData = [
'id' => uniqid(),
'from' => $user['email'],
'to' => $data['to'],
'subject' => $data['subject'],
'date' => date('Y-m-d H:i:s'),
'content' => isset($data['content']) ? $data['content'] : '',
'folder' => isset($data['isDraft']) && $data['isDraft'] ? 'draft' : 'sent',
'is_read' => 1, // 已发送的邮件自动标记为已读
'is_deleted' => 0,
'created_at' => date('Y-m-d H:i:s')
];
// 保存邮件到email表
$sql = "INSERT INTO email (id, `from`, `to`, subject, date, data, folder, is_read, is_deleted, created_at, mail_from, rcpt_to) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
$db->insert($sql, [
$emailData['id'],
$emailData['from'],
$emailData['to'],
$emailData['subject'],
$emailData['date'],
$emailData['content'],
$emailData['folder'],
$emailData['is_read'],
$emailData['is_deleted'],
$emailData['created_at'],
$emailData['from'],
$emailData['to']
]);
echo json_encode([
'success' => true,
'message' => isset($data['isDraft']) && $data['isDraft'] ? '草稿保存成功' : '邮件发送成功',
'data' => [
'emailId' => $emailData['id']
]
]);
}
/**
* 更新邮件(标记为已读/未读、移动文件夹等)
* @param Database $db 数据库实例
*/
function updateEmail($db) {
// 获取请求数据
$data = json_decode(file_get_contents('php://input'), true);
// 验证请求数据
if (!$data || empty($data['id']) || empty($data['username'])) {
echo json_encode([
'success' => false,
'message' => '缺少必要参数'
]);
return;
}
// 构建更新字段
$updateFields = [];
$params = [];
if (isset($data['isRead'])) {
$updateFields[] = "is_read = ?";
$params[] = (int)$data['isRead'];
}
if (isset($data['folder'])) {
$updateFields[] = "folder = ?";
$params[] = $data['folder'];
}
if (empty($updateFields)) {
echo json_encode([
'success' => false,
'message' => '没有需要更新的字段'
]);
return;
}
// 添加邮件ID和用户名参数
$params[] = $data['id'];
$params[] = $data['username'];
// 更新邮件
$sql = "UPDATE email SET " . implode(', ', $updateFields) . " WHERE id = ? AND rcpt_to = (SELECT email FROM user WHERE username = ?)";
$db->execute($sql, $params);
echo json_encode([
'success' => true,
'message' => '邮件更新成功'
]);
}
/**
* 删除邮件
* @param Database $db 数据库实例
*/
function deleteEmail($db) {
// 获取请求参数
$id = isset($_GET['id']) ? $_GET['id'] : '';
$username = isset($_GET['username']) ? $_GET['username'] : '';
// 验证必要参数
if (empty($id) || empty($username)) {
echo json_encode([
'success' => false,
'message' => '缺少必要参数'
]);
return;
}
// 软删除邮件
$sql = "UPDATE email SET is_deleted = 1, folder = 'trash' WHERE id = ? AND rcpt_to = (SELECT email FROM user WHERE username = ?)";
$db->execute($sql, [$id, $username]);
echo json_encode([
'success' => true,
'message' => '邮件删除成功'
]);
}

@ -0,0 +1,164 @@
<?php
/**
* 日志查看API
*/
// 设置错误报告
error_reporting(E_ALL);
ini_set('display_errors', 0);
// 设置JSON响应头
header('Content-Type: application/json; charset=utf-8');
// 包含必要的类
require_once '../../utils/Config.php';
require_once '../../utils/Logger.php';
require_once '../../utils/Helper.php';
require_once '../../utils/Database.php';
// 处理请求
try {
// 获取请求方法
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
// 连接数据库
$db = Database::getInstance();
// 根据请求方法处理
switch ($method) {
case 'GET':
// 获取日志列表
getLogs($db);
break;
case 'DELETE':
// 删除日志(可选功能)
deleteLogs($db);
break;
default:
echo json_encode([
'success' => false,
'message' => '不支持的请求方法'
]);
break;
}
} catch (Exception $e) {
echo json_encode([
'success' => false,
'message' => '服务器内部错误: ' . $e->getMessage()
]);
}
/**
* 获取日志列表
* @param Database $db 数据库实例
*/
function getLogs($db) {
// 获取请求参数
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$perPage = 10;
$offset = ($page - 1) * $perPage;
// 过滤条件
$search = isset($_GET['search']) ? $_GET['search'] : '';
$type = isset($_GET['type']) ? $_GET['type'] : '';
$startDate = isset($_GET['startDate']) ? $_GET['startDate'] : '';
$endDate = isset($_GET['endDate']) ? $_GET['endDate'] : '';
// 构建查询条件
$where = '';
$params = [];
$conditions = [];
if (!empty($search)) {
$conditions[] = "message LIKE ?";
$params[] = "%$search%";
}
if (!empty($type)) {
$conditions[] = "type = ?";
$params[] = $type;
}
if (!empty($startDate)) {
$conditions[] = "created_at >= ?";
$params[] = $startDate . " 00:00:00";
}
if (!empty($endDate)) {
$conditions[] = "created_at <= ?";
$params[] = $endDate . " 23:59:59";
}
if (!empty($conditions)) {
$where = "WHERE " . implode(" AND ", $conditions);
}
// 查询日志总数
$totalSql = "SELECT COUNT(*) as total FROM logs $where";
$totalResult = $db->fetchOne($totalSql, $params);
$total = $totalResult['total'];
// 查询日志列表
$logsSql = "SELECT * FROM logs $where ORDER BY created_at DESC LIMIT ? OFFSET ?";
$logsParams = array_merge($params, [$perPage, $offset]);
$logs = $db->fetchAll($logsSql, $logsParams);
// 格式化日志数据
$formattedLogs = [];
foreach ($logs as $log) {
$formattedLogs[] = [
'id' => $log['id'],
'type' => $log['type'],
'message' => $log['message'],
'ip' => $log['ip'],
'user_id' => $log['user_id'],
'created_at' => $log['created_at']
];
}
// 返回响应
echo json_encode([
'success' => true,
'data' => [
'logs' => $formattedLogs,
'total' => $total,
'page' => $page,
'perPage' => $perPage,
'totalPages' => ceil($total / $perPage)
]
]);
}
/**
* 删除日志
* @param Database $db 数据库实例
*/
function deleteLogs($db) {
// 获取请求参数
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
$all = isset($_GET['all']) ? $_GET['all'] : false;
if ($id <= 0 && !$all) {
echo json_encode([
'success' => false,
'message' => '无效的日志ID'
]);
return;
}
if ($all) {
// 删除所有日志
$db->execute("DELETE FROM logs");
echo json_encode([
'success' => true,
'message' => '所有日志已删除'
]);
} else {
// 删除指定日志
$db->delete("DELETE FROM logs WHERE id = ?", [$id]);
echo json_encode([
'success' => true,
'message' => '日志删除成功'
]);
}
}

@ -0,0 +1,270 @@
<?php
/**
* 邮箱管理API
*/
// 设置错误报告
error_reporting(E_ALL);
ini_set('display_errors', 0);
// 设置JSON响应头
header('Content-Type: application/json; charset=utf-8');
// 包含必要的类
require_once '../../utils/Config.php';
require_once '../../utils/Logger.php';
require_once '../../utils/Helper.php';
require_once '../../utils/Database.php';
// 处理请求
try {
// 获取请求方法
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
// 连接数据库
$db = Database::getInstance();
// 根据请求方法处理
switch ($method) {
case 'GET':
// 获取邮箱信息
if (isset($_GET['action']) && $_GET['action'] === 'quota') {
// 获取邮箱配额
getMailboxQuota($db);
} else {
// 获取邮箱基本信息
getMailboxInfo($db);
}
break;
case 'POST':
// 创建自定义文件夹
createFolder($db);
break;
case 'PUT':
// 重命名文件夹
renameFolder($db);
break;
case 'DELETE':
// 删除自定义文件夹
deleteFolder($db);
break;
default:
echo json_encode([
'success' => false,
'message' => '不支持的请求方法'
]);
break;
}
} catch (Exception $e) {
echo json_encode([
'success' => false,
'message' => '服务器内部错误: ' . $e->getMessage()
]);
}
/**
* 获取邮箱基本信息
* @param Database $db 数据库实例
*/
function getMailboxInfo($db) {
// 获取请求参数
$username = isset($_GET['username']) ? $_GET['username'] : '';
// 验证必要参数
if (empty($username)) {
echo json_encode([
'success' => false,
'message' => '缺少用户名参数'
]);
return;
}
// 获取用户邮箱
$user = $db->fetchOne("SELECT email FROM user WHERE username = ?", [$username]);
if (!$user) {
echo json_encode([
'success' => false,
'message' => '用户不存在'
]);
return;
}
// 获取邮箱统计信息
$stats = [
'inbox' => $db->fetchOne("SELECT COUNT(*) as count FROM email WHERE rcpt_to = ? AND folder = 'inbox' AND is_deleted = 0", [$user['email']])['count'],
'sent' => $db->fetchOne("SELECT COUNT(*) as count FROM email WHERE `from` = ? AND folder = 'sent' AND is_deleted = 0", [$user['email']])['count'],
'draft' => $db->fetchOne("SELECT COUNT(*) as count FROM email WHERE `from` = ? AND folder = 'draft' AND is_deleted = 0", [$user['email']])['count'],
'trash' => $db->fetchOne("SELECT COUNT(*) as count FROM email WHERE (rcpt_to = ? OR `from` = ?) AND folder = 'trash' AND is_deleted = 1", [$user['email'], $user['email']])['count'],
'unread' => $db->fetchOne("SELECT COUNT(*) as count FROM email WHERE rcpt_to = ? AND is_read = 0 AND is_deleted = 0", [$user['email']])['count']
];
// 获取自定义文件夹
$customFolders = [];
echo json_encode([
'success' => true,
'data' => [
'email' => $user['email'],
'stats' => $stats,
'customFolders' => $customFolders
]
]);
}
/**
* 获取邮箱配额
* @param Database $db 数据库实例
*/
function getMailboxQuota($db) {
// 获取请求参数
$username = isset($_GET['username']) ? $_GET['username'] : '';
// 验证必要参数
if (empty($username)) {
echo json_encode([
'success' => false,
'message' => '缺少用户名参数'
]);
return;
}
// 获取邮箱配额信息
$quota = $db->fetchOne("SELECT quota_bytes, used_bytes FROM mailboxes WHERE username = ?", [$username]);
if (!$quota) {
// 如果没有找到,使用默认值
$quota = [
'quota_bytes' => 1073741824, // 1GB默认配额
'used_bytes' => 0
];
}
echo json_encode([
'success' => true,
'data' => [
'quota' => $quota['quota_bytes'],
'used' => $quota['used_bytes'],
'remaining' => $quota['quota_bytes'] - $quota['used_bytes'],
'percentage' => round(($quota['used_bytes'] / $quota['quota_bytes']) * 100, 2)
]
]);
}
/**
* 创建自定义文件夹
* @param Database $db 数据库实例
*/
function createFolder($db) {
// 获取请求数据
$data = json_decode(file_get_contents('php://input'), true);
// 验证请求数据
if (!$data || empty($data['username']) || empty($data['name'])) {
echo json_encode([
'success' => false,
'message' => '缺少必要参数'
]);
return;
}
// 检查文件夹名称是否合法
$folderName = $data['name'];
if (in_array($folderName, ['inbox', 'sent', 'draft', 'trash'])) {
echo json_encode([
'success' => false,
'message' => '不能创建系统默认文件夹'
]);
return;
}
// 检查文件夹是否已存在
// 注意当前email表中没有自定义文件夹的概念所以直接返回成功
echo json_encode([
'success' => true,
'message' => '文件夹创建成功',
'data' => [
'name' => $folderName
]
]);
}
/**
* 重命名文件夹
* @param Database $db 数据库实例
*/
function renameFolder($db) {
// 获取请求数据
$data = json_decode(file_get_contents('php://input'), true);
// 验证请求数据
if (!$data || empty($data['username']) || empty($data['oldName']) || empty($data['newName'])) {
echo json_encode([
'success' => false,
'message' => '缺少必要参数'
]);
return;
}
// 检查文件夹名称是否合法
$oldName = $data['oldName'];
$newName = $data['newName'];
if (in_array($oldName, ['inbox', 'sent', 'draft', 'trash']) || in_array($newName, ['inbox', 'sent', 'draft', 'trash'])) {
echo json_encode([
'success' => false,
'message' => '不能修改系统默认文件夹'
]);
return;
}
// 检查新文件夹名称是否已存在
// 注意当前email表中没有自定义文件夹的概念所以直接返回成功
echo json_encode([
'success' => true,
'message' => '文件夹重命名成功',
'data' => [
'oldName' => $oldName,
'newName' => $newName
]
]);
}
/**
* 删除自定义文件夹
* @param Database $db 数据库实例
*/
function deleteFolder($db) {
// 获取请求参数
$username = isset($_GET['username']) ? $_GET['username'] : '';
$name = isset($_GET['name']) ? $_GET['name'] : '';
// 验证必要参数
if (empty($username) || empty($name)) {
echo json_encode([
'success' => false,
'message' => '缺少必要参数'
]);
return;
}
// 检查文件夹名称是否合法
if (in_array($name, ['inbox', 'sent', 'draft', 'trash'])) {
echo json_encode([
'success' => false,
'message' => '不能删除系统默认文件夹'
]);
return;
}
// 注意当前email表中没有自定义文件夹的概念所以直接返回成功
echo json_encode([
'success' => true,
'message' => '文件夹删除成功',
'data' => [
'name' => $name
]
]);
}

@ -0,0 +1,235 @@
<?php
/**
* 服务器设置API
*/
// 设置错误报告
error_reporting(E_ALL);
ini_set('display_errors', 0);
// 设置JSON响应头
header('Content-Type: application/json; charset=utf-8');
// 包含必要的类
require_once '../../utils/Config.php';
require_once '../../utils/Logger.php';
require_once '../../utils/Helper.php';
require_once '../../utils/Database.php';
// 处理请求
try {
// 获取请求方法
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
// 连接数据库
$db = Database::getInstance();
// 根据请求方法处理
switch ($method) {
case 'GET':
// 获取服务器设置
getSettings($db);
break;
case 'POST':
// 保存服务器设置
saveSettings($db);
break;
default:
echo json_encode([
'success' => false,
'message' => '不支持的请求方法'
]);
break;
}
} catch (Exception $e) {
echo json_encode([
'success' => false,
'message' => '服务器内部错误: ' . $e->getMessage()
]);
}
/**
* 获取服务器设置
* @param Database $db 数据库实例
*/
function getSettings($db) {
// 从config表获取所有设置
$settings = $db->fetchAll("SELECT config_key, config_value FROM server_configs");
// 将设置转换为关联数组
$config = [];
foreach ($settings as $setting) {
$config[$setting['config_key']] = $setting['config_value'];
}
// 从配置文件获取默认值
$defaultConfig = Config::getInstance()->getAll();
// 合并设置,使用数据库中的设置覆盖默认值
$mergedConfig = array_replace_recursive($defaultConfig, parseSettings($config));
// 返回响应
echo json_encode([
'success' => true,
'data' => $mergedConfig
]);
}
/**
* 保存服务器设置
* @param Database $db 数据库实例
*/
function saveSettings($db) {
// 获取请求数据
$data = json_decode(file_get_contents('php://input'), true);
if (!$data) {
echo json_encode([
'success' => false,
'message' => '无效的请求数据'
]);
return;
}
// 验证必填字段
if (!isset($data['smtp']) || !isset($data['pop3']) || !isset($data['server'])) {
echo json_encode([
'success' => false,
'message' => '缺少必要的设置字段'
]);
return;
}
// 验证端口号
if (!is_numeric($data['smtp']['port']) || $data['smtp']['port'] < 1 || $data['smtp']['port'] > 65535) {
echo json_encode([
'success' => false,
'message' => '无效的SMTP端口号'
]);
return;
}
if (!is_numeric($data['pop3']['port']) || $data['pop3']['port'] < 1 || $data['pop3']['port'] > 65535) {
echo json_encode([
'success' => false,
'message' => '无效的POP3端口号'
]);
return;
}
// 将设置转换为平面数组
$flatSettings = flattenSettings($data);
// 开始事务
$pdo = $db->beginTransaction();
try {
// 删除现有设置
$db->execute("DELETE FROM server_configs");
// 插入新设置
foreach ($flatSettings as $key => $value) {
$db->insert(
"INSERT INTO server_configs (config_key, config_value, description) VALUES (?, ?, ?)",
[$key, $value, getSettingDescription($key)]
);
}
// 提交事务
$db->commit($pdo);
echo json_encode([
'success' => true,
'message' => '服务器设置保存成功'
]);
} catch (Exception $e) {
// 回滚事务
$db->rollback($pdo);
echo json_encode([
'success' => false,
'message' => '保存设置失败: ' . $e->getMessage()
]);
}
}
/**
* 将平面数组转换为嵌套数组
* @param array $settings 平面数组
* @return array 嵌套数组
*/
function parseSettings($settings) {
$result = [];
foreach ($settings as $key => $value) {
// 解析嵌套键,例如 smtp.port -> smtp['port']
$keys = explode('.', $key);
$current = &$result;
foreach ($keys as $k) {
if (!isset($current[$k])) {
$current[$k] = [];
}
$current = &$current[$k];
}
// 尝试将数值字符串转换为数值
if (is_numeric($value)) {
$value = (strpos($value, '.') !== false) ? (float)$value : (int)$value;
} else if (strtolower($value) === 'true' || strtolower($value) === 'false') {
// 转换布尔值
$value = strtolower($value) === 'true';
}
$current = $value;
}
return $result;
}
/**
* 将嵌套数组转换为平面数组
* @param array $settings 嵌套数组
* @param string $prefix 前缀(用于递归)
* @return array 平面数组
*/
function flattenSettings($settings, $prefix = '') {
$result = [];
foreach ($settings as $key => $value) {
$newKey = empty($prefix) ? $key : $prefix . '.' . $key;
if (is_array($value)) {
// 递归处理嵌套数组
$result = array_merge($result, flattenSettings($value, $newKey));
} else {
// 直接添加值
$result[$newKey] = (string)$value;
}
}
return $result;
}
/**
* 获取设置的描述
* @param string $key 设置键名
* @return string 描述
*/
function getSettingDescription($key) {
$descriptions = [
'smtp.port' => 'SMTP服务器端口',
'smtp.host' => 'SMTP服务器监听地址',
'smtp.max_connections' => 'SMTP服务器最大连接数',
'pop3.port' => 'POP3服务器端口',
'pop3.host' => 'POP3服务器监听地址',
'pop3.max_connections' => 'POP3服务器最大连接数',
'server.domain' => '服务器域名',
'server.max_email_size' => '最大邮件大小',
'server.max_mailbox_size' => '最大邮箱大小',
'log.level' => '日志级别',
'log.path' => '日志文件路径'
];
return $descriptions[$key] ?? '';
}

@ -0,0 +1,267 @@
<?php
/**
* 邮件统计API
*/
// 设置错误报告
error_reporting(E_ALL);
ini_set('display_errors', 0);
// 设置JSON响应头
header('Content-Type: application/json; charset=utf-8');
// 包含必要的类
require_once '../../utils/Config.php';
require_once '../../utils/Logger.php';
require_once '../../utils/Helper.php';
require_once '../../utils/Database.php';
// 处理请求
try {
// 获取请求方法
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
// 连接数据库
$db = Database::getInstance();
// 只支持GET请求
if ($method === 'GET') {
// 获取统计类型
$type = isset($_GET['type']) ? $_GET['type'] : 'mail';
switch ($type) {
case 'mail':
// 获取邮件统计
getMailStats($db);
break;
case 'traffic':
// 获取流量统计
getTrafficStats($db);
break;
case 'send':
// 获取发送统计
getSendStats($db);
break;
default:
echo json_encode([
'success' => false,
'message' => '不支持的统计类型' . $type
]);
break;
}
} else {
echo json_encode([
'success' => false,
'message' => '不支持的请求方法' . $method
]);
}
} catch (Exception $e) {
echo json_encode([
'success' => false,
'message' => '服务器内部错误: ' . $e->getMessage()
]);
}
/**
* 获取邮件统计
* @param Database $db 数据库实例
*/
function getMailStats($db) {
// 获取请求参数
$username = isset($_GET['username']) ? $_GET['username'] : '';
$startDate = isset($_GET['startDate']) ? $_GET['startDate'] : '';
$endDate = isset($_GET['endDate']) ? $_GET['endDate'] : '';
// 验证必要参数
if (empty($username)) {
echo json_encode([
'success' => false,
'message' => '缺少用户名参数'
]);
return;
}
// 获取用户邮箱
$user = $db->fetchOne("SELECT email FROM user WHERE username = ?", [$username]);
if (!$user) {
echo json_encode([
'success' => false,
'message' => '用户不存在'
]);
return;
}
// 构建查询条件
$where = "WHERE (rcpt_to = ? OR `from` = ?)";
$params = [$user['email'], $user['email']];
if (!empty($startDate)) {
$where .= " AND date >= ?";
$params[] = $startDate . " 00:00:00";
}
if (!empty($endDate)) {
$where .= " AND date <= ?";
$params[] = $endDate . " 23:59:59";
}
// 获取邮件类型统计
$typeStats = [
'inbox' => $db->fetchOne("SELECT COUNT(*) as count FROM email $where AND folder = 'inbox' AND is_deleted = 0", $params)['count'],
'sent' => $db->fetchOne("SELECT COUNT(*) as count FROM email $where AND folder = 'sent' AND is_deleted = 0", $params)['count'],
'draft' => $db->fetchOne("SELECT COUNT(*) as count FROM email $where AND folder = 'draft' AND is_deleted = 0", $params)['count'],
'trash' => $db->fetchOne("SELECT COUNT(*) as count FROM email $where AND folder = 'trash' AND is_deleted = 1", $params)['count'],
'total' => $db->fetchOne("SELECT COUNT(*) as count FROM email $where", $params)['count']
];
// 获取阅读状态统计
$readStats = [
'read' => $db->fetchOne("SELECT COUNT(*) as count FROM email WHERE rcpt_to = ? AND is_read = 1 AND is_deleted = 0", [$user['email']])['count'],
'unread' => $db->fetchOne("SELECT COUNT(*) as count FROM email WHERE rcpt_to = ? AND is_read = 0 AND is_deleted = 0", [$user['email']])['count']
];
echo json_encode([
'success' => true,
'data' => [
'typeStats' => $typeStats,
'readStats' => $readStats
]
]);
}
/**
* 获取流量统计
* @param Database $db 数据库实例
*/
function getTrafficStats($db) {
// 获取请求参数
$username = isset($_GET['username']) ? $_GET['username'] : '';
$startDate = isset($_GET['startDate']) ? $_GET['startDate'] : '';
$endDate = isset($_GET['endDate']) ? $_GET['endDate'] : '';
// 验证必要参数
if (empty($username)) {
echo json_encode([
'success' => false,
'message' => '缺少用户名参数'
]);
return;
}
// 获取用户邮箱
$user = $db->fetchOne("SELECT email FROM user WHERE username = ?", [$username]);
if (!$user) {
echo json_encode([
'success' => false,
'message' => '用户不存在'
]);
return;
}
// 构建查询条件
$where = "WHERE (rcpt_to = ? OR `from` = ?)";
$params = [$user['email'], $user['email']];
if (!empty($startDate)) {
$where .= " AND date >= ?";
$params[] = $startDate . " 00:00:00";
}
if (!empty($endDate)) {
$where .= " AND date <= ?";
$params[] = $endDate . " 23:59:59";
}
// 获取邮件大小统计
$sizeStats = $db->fetchOne("SELECT SUM(length) as total_size FROM email $where", $params);
// 获取邮箱配额
$quota = $db->fetchOne("SELECT quota_bytes, used_bytes FROM mailboxes WHERE username = ?", [$username]);
if (!$quota) {
$quota = [
'quota_bytes' => 1073741824, // 1GB默认配额
'used_bytes' => 0
];
}
echo json_encode([
'success' => true,
'data' => [
'totalSize' => $sizeStats['total_size'] ?: 0,
'quota' => $quota['quota_bytes'],
'used' => $quota['used_bytes'],
'remaining' => $quota['quota_bytes'] - $quota['used_bytes']
]
]);
}
/**
* 获取发送统计
* @param Database $db 数据库实例
*/
function getSendStats($db) {
// 获取请求参数
$username = isset($_GET['username']) ? $_GET['username'] : '';
$startDate = isset($_GET['startDate']) ? $_GET['startDate'] : '';
$endDate = isset($_GET['endDate']) ? $_GET['endDate'] : '';
// 验证必要参数
if (empty($username)) {
echo json_encode([
'success' => false,
'message' => '缺少用户名参数'
]);
return;
}
// 获取用户邮箱
$user = $db->fetchOne("SELECT email FROM user WHERE username = ?", [$username]);
if (!$user) {
echo json_encode([
'success' => false,
'message' => '用户不存在'
]);
return;
}
// 构建查询条件
$where = "WHERE `from` = ? AND folder = 'sent' AND is_deleted = 0";
$params = [$user['email']];
if (!empty($startDate)) {
$where .= " AND date >= ?";
$params[] = $startDate . " 00:00:00";
}
if (!empty($endDate)) {
$where .= " AND date <= ?";
$params[] = $endDate . " 23:59:59";
}
// 获取发送总量
$totalSent = $db->fetchOne("SELECT COUNT(*) as count FROM email $where", $params)['count'];
// 获取每日发送统计
$dailyStats = [];
// 构建每日统计查询
$dailyQuery = "SELECT DATE(date) as date, COUNT(*) as count FROM email $where GROUP BY DATE(date) ORDER BY date ASC";
$dailyResult = $db->fetchAll($dailyQuery, $params);
if ($dailyResult) {
foreach ($dailyResult as $row) {
$dailyStats[] = [
'date' => $row['date'],
'count' => $row['count']
];
}
}
echo json_encode([
'success' => true,
'data' => [
'totalSent' => $totalSent,
'dailyStats' => $dailyStats
]
]);
}

@ -0,0 +1,264 @@
<?php
/**
* 用户设置API
*/
// 设置错误报告
error_reporting(E_ALL);
ini_set('display_errors', 0);
// 设置JSON响应头
header('Content-Type: application/json; charset=utf-8');
// 包含必要的类
require_once '../../utils/Config.php';
require_once '../../utils/Logger.php';
require_once '../../utils/Helper.php';
require_once '../../utils/Database.php';
// 处理请求
try {
// 获取请求方法
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
// 连接数据库
$db = Database::getInstance();
// 根据请求方法处理
switch ($method) {
case 'GET':
// 获取用户设置
getSettings($db);
break;
case 'POST':
// 保存用户设置
saveSettings($db);
break;
case 'PUT':
// 处理特殊设置更新
$action = isset($_GET['action']) ? $_GET['action'] : '';
if ($action === 'password') {
// 修改密码
changePassword($db);
} elseif ($action === 'autoreply') {
// 设置自动回复
setAutoReply($db);
} else {
echo json_encode([
'success' => false,
'message' => '不支持的操作类型'
]);
}
break;
default:
echo json_encode([
'success' => false,
'message' => '不支持的请求方法'
]);
break;
}
} catch (Exception $e) {
echo json_encode([
'success' => false,
'message' => '服务器内部错误: ' . $e->getMessage()
]);
}
/**
* 获取用户设置
* @param Database $db 数据库实例
*/
function getSettings($db) {
// 获取请求参数
$username = isset($_GET['username']) ? $_GET['username'] : '';
// 验证必要参数
if (empty($username)) {
echo json_encode([
'success' => false,
'message' => '缺少用户名参数'
]);
return;
}
// 查询用户设置
$settings = $db->fetchAll("SELECT setting_key, setting_value FROM user_settings WHERE user_id = ?", [$username]);
// 转换为关联数组
$settingsArray = [];
foreach ($settings as $setting) {
$settingsArray[$setting['setting_key']] = $setting['setting_value'];
}
echo json_encode([
'success' => true,
'data' => [
'settings' => $settingsArray
]
]);
}
/**
* 保存用户设置
* @param Database $db 数据库实例
*/
function saveSettings($db) {
// 获取请求数据
$data = json_decode(file_get_contents('php://input'), true);
// 验证请求数据
if (!$data || empty($data['username']) || empty($data['settings'])) {
echo json_encode([
'success' => false,
'message' => '缺少必要参数'
]);
return;
}
$username = $data['username'];
$settings = $data['settings'];
// 开始事务
$pdo = $db->beginTransaction();
try {
// 删除现有设置
$db->execute("DELETE FROM user_settings WHERE user_id = ?", [$username]);
// 插入新设置
foreach ($settings as $key => $value) {
$db->insert(
"INSERT INTO user_settings (user_id, setting_key, setting_value) VALUES (?, ?, ?)",
[$username, $key, $value]
);
}
// 提交事务
$db->commit($pdo);
echo json_encode([
'success' => true,
'message' => '设置保存成功'
]);
} catch (Exception $e) {
// 回滚事务
$db->rollback($pdo);
echo json_encode([
'success' => false,
'message' => '保存设置失败: ' . $e->getMessage()
]);
}
}
/**
* 修改密码
* @param Database $db 数据库实例
*/
function changePassword($db) {
// 获取请求数据
$data = json_decode(file_get_contents('php://input'), true);
// 验证请求数据
if (!$data || empty($data['username']) || empty($data['oldPassword']) || empty($data['newPassword'])) {
echo json_encode([
'success' => false,
'message' => '缺少必要参数'
]);
return;
}
$username = $data['username'];
$oldPassword = $data['oldPassword'];
$newPassword = $data['newPassword'];
// 获取用户当前密码
$user = $db->fetchOne("SELECT password FROM user WHERE username = ?", [$username]);
if (!$user) {
echo json_encode([
'success' => false,
'message' => '用户不存在'
]);
return;
}
// 验证旧密码
if (!password_verify($oldPassword, $user['password'])) {
echo json_encode([
'success' => false,
'message' => '旧密码不正确'
]);
return;
}
// 更新密码
$encryptedPassword = Helper::encryptPassword($newPassword);
$db->execute("UPDATE user SET password = ? WHERE username = ?", [$encryptedPassword, $username]);
echo json_encode([
'success' => true,
'message' => '密码修改成功'
]);
}
/**
* 设置自动回复
* @param Database $db 数据库实例
*/
function setAutoReply($db) {
// 获取请求数据
$data = json_decode(file_get_contents('php://input'), true);
// 验证请求数据
if (!$data || empty($data['username'])) {
echo json_encode([
'success' => false,
'message' => '缺少必要参数'
]);
return;
}
$username = $data['username'];
$isEnabled = isset($data['isEnabled']) ? (bool)$data['isEnabled'] : false;
$subject = isset($data['subject']) ? $data['subject'] : '';
$content = isset($data['content']) ? $data['content'] : '';
// 保存自动回复设置
$autoReplySettings = [
'auto_reply_enabled' => $isEnabled ? '1' : '0',
'auto_reply_subject' => $subject,
'auto_reply_content' => $content
];
// 开始事务
$pdo = $db->beginTransaction();
try {
// 删除现有自动回复设置
$db->execute("DELETE FROM user_settings WHERE user_id = ? AND setting_key LIKE 'auto_reply_%'", [$username]);
// 插入新设置
foreach ($autoReplySettings as $key => $value) {
$db->insert(
"INSERT INTO user_settings (user_id, setting_key, setting_value) VALUES (?, ?, ?)",
[$username, $key, $value]
);
}
// 提交事务
$db->commit($pdo);
echo json_encode([
'success' => true,
'message' => '自动回复设置成功'
]);
} catch (Exception $e) {
// 回滚事务
$db->rollback($pdo);
echo json_encode([
'success' => false,
'message' => '设置自动回复失败: ' . $e->getMessage()
]);
}
}

@ -0,0 +1,282 @@
<?php
/**
* 用户管理API
*/
// 设置错误报告
error_reporting(E_ALL);
ini_set('display_errors', 0);
// 设置JSON响应头
header('Content-Type: application/json; charset=utf-8');
// 包含必要的类
require_once '../../utils/Config.php';
require_once '../../utils/Logger.php';
require_once '../../utils/Helper.php';
require_once '../../utils/Database.php';
// 处理请求
try {
// 获取请求方法
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
// 连接数据库
$db = Database::getInstance();
// 根据请求方法处理
switch ($method) {
case 'GET':
// 获取用户列表
getUsers($db);
break;
case 'POST':
// 检查是否是登录请求
$data = json_decode(file_get_contents('php://input'), true);
if (isset($data['login']) && $data['login'] === true) {
// 登录请求
loginUser($db, $data);
} else {
// 添加或编辑用户
saveUser($db);
}
break;
case 'DELETE':
// 删除用户
deleteUser($db);
break;
default:
echo json_encode([
'success' => false,
'message' => '不支持的请求方法'
]);
break;
}
} catch (Exception $e) {
echo json_encode([
'success' => false,
'message' => '服务器内部错误: ' . $e->getMessage()
]);
}
/**
* 获取用户列表
* @param Database $db 数据库实例
*/
function getUsers($db) {
// 获取请求参数
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$search = isset($_GET['search']) ? $_GET['search'] : '';
$perPage = 10;
$offset = ($page - 1) * $perPage;
// 构建查询条件
$where = '';
$params = [];
if (!empty($search)) {
$where = "WHERE (username LIKE ? OR email LIKE ?) AND is_deleted = 0";
$searchParam = "%$search%";
$params = [$searchParam, $searchParam];
} else {
$where = "WHERE is_deleted = 0";
}
// 查询用户总数
$totalSql = "SELECT COUNT(*) as total FROM user $where";
$totalResult = $db->fetchOne($totalSql, $params);
$total = $totalResult['total'];
// 查询用户列表
$usersSql = "SELECT * FROM user $where ORDER BY create_time DESC LIMIT ? OFFSET ?";
$usersParams = array_merge($params, [$perPage, $offset]);
$users = $db->fetchAll($usersSql, $usersParams);
// 格式化用户数据
$formattedUsers = [];
foreach ($users as $user) {
$formattedUsers[] = [
'username' => $user['username'],
'email' => $user['email'],
'is_admin' => (bool)$user['is_admin'],
'is_enabled' => (bool)$user['is_enabled'],
'created_at' => $user['create_time'],
'updated_at' => $user['updated_at']
];
}
// 返回响应
echo json_encode([
'success' => true,
'data' => [
'users' => $formattedUsers,
'total' => $total,
'page' => $page,
'perPage' => $perPage,
'totalPages' => ceil($total / $perPage)
]
]);
}
/**
* 保存用户信息(添加或编辑)
* @param Database $db 数据库实例
*/
function saveUser($db) {
// 获取请求数据
$data = json_decode(file_get_contents('php://input'), true);
if (!$data) {
echo json_encode([
'success' => false,
'message' => '无效的请求数据'
]);
return;
}
// 验证必填字段
if (empty($data['username']) || empty($data['email'])) {
echo json_encode([
'success' => false,
'message' => '用户名和邮箱不能为空'
]);
return;
}
// 检查邮箱格式
if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
echo json_encode([
'success' => false,
'message' => '无效的邮箱格式'
]);
return;
}
// 检查用户名是否已存在
$existingUser = $db->fetchOne("SELECT username FROM user WHERE username = ? OR email = ?", [
$data['username'],
$data['email']
]);
if ($existingUser) {
echo json_encode([
'success' => false,
'message' => '用户名或邮箱已存在'
]);
return;
}
// 验证密码
if (empty($data['password'])) {
echo json_encode([
'success' => false,
'message' => '密码不能为空'
]);
return;
}
// 添加用户
$db->insert(
"INSERT INTO user (username, password, email, is_admin, is_enabled, create_time, updated_at, is_deleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
[
$data['username'],
Helper::encryptPassword($data['password']),
$data['email'],
(bool)$data['is_admin'],
(bool)$data['is_enabled'],
date('Y-m-d H:i:s'),
date('Y-m-d H:i:s'),
0
]
);
echo json_encode([
'success' => true,
'message' => '用户添加成功'
]);
}
/**
* 删除用户
* @param Database $db 数据库实例
*/
function deleteUser($db) {
// 获取请求参数
$username = isset($_GET['username']) ? $_GET['username'] : '';
if (empty($username)) {
echo json_encode([
'success' => false,
'message' => '无效的用户名'
]);
return;
}
// 检查用户是否存在
$user = $db->fetchOne("SELECT username FROM user WHERE username = ?", [$username]);
if (!$user) {
echo json_encode([
'success' => false,
'message' => '用户不存在'
]);
return;
}
// 删除用户(软删除)
$db->update(
"UPDATE user SET is_deleted = 1, updated_at = ? WHERE username = ?",
[date('Y-m-d H:i:s'), $username]
);
echo json_encode([
'success' => true,
'message' => '用户删除成功'
]);
}
/**
* 用户登录
* @param Database $db 数据库实例
* @param array $data 登录数据
*/
function loginUser($db, $data) {
// 验证必填字段
if (empty($data['username']) || empty($data['password'])) {
echo json_encode([
'success' => false,
'message' => '用户名和密码不能为空'
]);
return;
}
// 查询用户
$sql = "SELECT * FROM user WHERE username = ? AND is_deleted = 0 AND is_enabled = 1";
$user = $db->fetchOne($sql, [$data['username']]);
if ($user && password_verify($data['password'], $user['password'])) {
// 登录成功
// 构建用户信息
$userInfo = [
'username' => $user['username'],
'email' => $user['email'],
'is_admin' => (bool)$user['is_admin'],
'is_enabled' => (bool)$user['is_enabled'],
'created_at' => $user['create_time'],
'updated_at' => $user['updated_at']
];
echo json_encode([
'success' => true,
'message' => '登录成功',
'data' => [
'user' => $userInfo
]
]);
} else {
// 登录失败
echo json_encode([
'success' => false,
'message' => '用户名或密码错误'
]);
}
}

@ -0,0 +1,54 @@
<?php
/**
* 邮件服务器配置文件
*/
return [
// 数据库配置
'database' => [
'driver' => 'mysql',
'host' => 'dbconn.sealoshzh.site',
'port' => 33979,
'database' => 'smtp',
'username' => 'root',
'password' => 'nv7cr6db',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => true,
],
// SMTP配置
'smtp' => [
'port' => 25, // SMTP端口
'host' => '0.0.0.0', // 监听地址
'max_connections' => 100, // 最大连接数
],
// POP3配置
'pop3' => [
'port' => 110, // POP3端口
'host' => '0.0.0.0', // 监听地址
'max_connections' => 100, // 最大连接数
],
// 服务器配置
'server' => [
'domain' => 'test.com', // 服务器域名
'admin_password' => 'admin123', // 管理员密码
'max_email_size' => 10 * 1024 * 1024, // 最大邮件大小10MB
],
// 日志配置
'log' => [
'path' => '../logs/', // 日志文件路径
'level' => 'info', // 日志级别debug, info, warning, error
'max_file_size' => 10 * 1024 * 1024, // 最大日志文件大小10MB
],
// 邮箱配置
'mailbox' => [
'max_size' => 100 * 1024 * 1024, // 最大邮箱大小100MB
],
];

@ -0,0 +1,980 @@
{
"name": "mail-backend",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mail-backend",
"version": "0.1.0",
"dependencies": {
"bcryptjs": "^3.0.3",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"express": "^4.19.2",
"iconv-lite": "0.4.24",
"mysql2": "^3.15.3",
"raw-body": "^2.5.2"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"license": "MIT",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
"node_modules/aws-ssl-profiles": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
"integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
"license": "MIT",
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/bcryptjs": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
"license": "BSD-3-Clause",
"bin": {
"bcrypt": "bin/bcrypt"
}
},
"node_modules/body-parser": {
"version": "1.20.4",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
"integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "~1.2.0",
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"on-finished": "~2.4.1",
"qs": "~6.14.0",
"raw-body": "~2.5.3",
"type-is": "~1.6.18",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"license": "MIT"
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"license": "MIT",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"license": "MIT",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "4.22.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "~1.20.3",
"content-disposition": "~0.5.4",
"content-type": "~1.0.4",
"cookie": "~0.7.1",
"cookie-signature": "~1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.3.1",
"fresh": "~0.5.2",
"http-errors": "~2.0.0",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "~2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "~0.1.12",
"proxy-addr": "~2.0.7",
"qs": "~6.14.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "~0.19.0",
"serve-static": "~1.16.2",
"setprototypeof": "1.2.0",
"statuses": "~2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/finalhandler": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "~2.4.1",
"parseurl": "~1.3.3",
"statuses": "~2.0.2",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/generate-function": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
"integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
"license": "MIT",
"dependencies": {
"is-property": "^1.0.2"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"license": "MIT",
"dependencies": {
"depd": "~2.0.0",
"inherits": "~2.0.4",
"setprototypeof": "~1.2.0",
"statuses": "~2.0.2",
"toidentifier": "~1.0.1"
},
"engines": {
"node": ">= 0.8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/is-property": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
"license": "MIT"
},
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0"
},
"node_modules/lru.min": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.3.tgz",
"integrity": "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==",
"license": "MIT",
"engines": {
"bun": ">=1.0.0",
"deno": ">=1.30.0",
"node": ">=8.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wellwelwel"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/mysql2": {
"version": "3.15.3",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz",
"integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==",
"license": "MIT",
"dependencies": {
"aws-ssl-profiles": "^1.1.1",
"denque": "^2.1.0",
"generate-function": "^2.3.1",
"iconv-lite": "^0.7.0",
"long": "^5.2.1",
"lru.min": "^1.0.0",
"named-placeholders": "^1.1.3",
"seq-queue": "^0.0.5",
"sqlstring": "^2.3.2"
},
"engines": {
"node": ">= 8.0"
}
},
"node_modules/mysql2/node_modules/iconv-lite": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz",
"integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/named-placeholders": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
"integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
"license": "MIT",
"dependencies": {
"lru.min": "^1.1.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/send": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
"integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "~0.5.2",
"http-errors": "~2.0.1",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "~2.4.1",
"range-parser": "~1.2.1",
"statuses": "~2.0.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/seq-queue": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
},
"node_modules/serve-static": {
"version": "1.16.3",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
"integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
"license": "MIT",
"dependencies": {
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "~0.19.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
"side-channel-list": "^1.0.0",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/sqlstring": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
"integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
}
}
}

@ -0,0 +1,17 @@
{
"name": "mail-backend",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "node server.js"
},
"dependencies": {
"bcryptjs": "^3.0.3",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"express": "^4.19.2",
"iconv-lite": "0.4.24",
"mysql2": "^3.15.3",
"raw-body": "^2.5.2"
}
}

@ -0,0 +1,12 @@
[PHP]
; 设置扩展目录
extension_dir = "E:\php\ext"
; 启用所需扩展
extension=sockets
extension=mbstring
extension=openssl
extension=json
extension=mysqli
extension=pdo_mysql

@ -0,0 +1,526 @@
<?php
/**
* POP3协议处理类
*/
require_once __DIR__ . '/../utils/Config.php';
class Pop3Handler {
private $socket;
private $clientIp;
private $logger;
private $config;
private $state = 'auth'; // 状态auth, transaction, update
private $username = '';
private $password = '';
private $authenticated = false;
private $messages = [];
private $deletedMessages = [];
private $messageCount = 0;
private $mailboxSize = 0;
/**
* 构造函数
* @param resource $socket 客户端socket
* @param string $clientIp 客户端IP地址
* @param Logger $logger 日志记录器
*/
public function __construct($socket, $clientIp, $logger) {
$this->socket = $socket;
$this->clientIp = $clientIp;
$this->logger = $logger;
$this->config = Config::getInstance();
}
/**
* 处理客户端数据
* @param string $data 客户端发送的数据
*/
public function handle($data) {
$this->logger->debug("Received data from {ip}: {data}", [
'ip' => $this->clientIp,
'data' => rtrim($data)
]);
// 按行处理数据
$lines = explode("\r\n", $data);
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) {
continue;
}
$this->processCommand($line);
}
}
/**
* 处理POP3命令
* @param string $command POP3命令
*/
private function processCommand($command) {
// 解析命令和参数
$parts = preg_split('/\s+/', $command, 2);
$cmd = strtoupper($parts[0]);
$params = isset($parts[1]) ? $parts[1] : '';
// 根据命令调用相应的处理方法
switch ($cmd) {
case 'USER':
$this->handleUser($params);
break;
case 'PASS':
$this->handlePass($params);
break;
case 'STAT':
$this->handleStat();
break;
case 'LIST':
$this->handleList($params);
break;
case 'RETR':
$this->handleRetr($params);
break;
case 'DELE':
$this->handleDele($params);
break;
case 'NOOP':
$this->handleNoop();
break;
case 'RSET':
$this->handleRset();
break;
case 'QUIT':
$this->handleQuit();
break;
case 'TOP':
$this->handleTop($params);
break;
case 'UIDL':
$this->handleUidl($params);
break;
default:
$this->sendResponse(false, "Unknown command");
break;
}
}
/**
* 处理USER命令
* @param string $username 用户名
*/
private function handleUser($username) {
if ($this->state !== 'auth') {
$this->sendResponse(false, "Bad sequence of commands");
$this->logger->warning("USER command out of sequence from {ip}", [
'ip' => $this->clientIp
]);
return;
}
// 验证用户名格式
if (empty($username) || strlen($username) > 50) {
$this->sendResponse(false, "Invalid username");
$this->logger->warning("Invalid username format from {ip}: {username}", [
'ip' => $this->clientIp,
'username' => $username
]);
return;
}
$this->username = $username;
$this->sendResponse(true, "User accepted");
$this->logger->info("USER command received from {ip}, username: {username}", [
'ip' => $this->clientIp,
'username' => $this->username
]);
}
/**
* 处理PASS命令
* @param string $password 密码
*/
private function handlePass($password) {
if ($this->state !== 'auth' || empty($this->username)) {
$this->sendResponse(false, "Bad sequence of commands");
$this->logger->warning("PASS command out of sequence from {ip}", [
'ip' => $this->clientIp
]);
return;
}
$this->password = $password;
try {
// 调用数据库接口验证用户身份
require_once __DIR__ . '/../utils/Database.php';
$db = Database::getInstance();
// 查询用户
$sql = "SELECT * FROM user WHERE username = ? AND is_deleted = 0";
$user = $db->query($sql, [$this->username])->fetch();
if ($user && password_verify($password, $user['password'])) {
$this->authenticated = true;
$this->state = 'transaction';
// 初始化邮件列表(从数据库获取)
$this->initMessages($user['username']);
$this->sendResponse(true, "Authentication successful");
$this->logger->info("PASS command received from {ip}, authentication successful for user: {username}", [
'ip' => $this->clientIp,
'username' => $this->username
]);
} else {
$this->sendResponse(false, "Invalid username or password");
$this->logger->warning("Authentication failed from {ip}, username: {username}", [
'ip' => $this->clientIp,
'username' => $this->username
]);
}
} catch (Exception $e) {
$this->sendResponse(false, "Authentication error");
$this->logger->error("Authentication exception from {ip}: {error}", [
'ip' => $this->clientIp,
'error' => $e->getMessage()
]);
}
}
/**
* 处理STAT命令
*/
private function handleStat() {
if (!$this->authenticated || $this->state !== 'transaction') {
$this->sendResponse(false, "Not authenticated");
return;
}
$this->sendResponse(true, "{$this->messageCount} {$this->mailboxSize}");
$this->logger->info("STAT command received from {ip}", [
'ip' => $this->clientIp
]);
}
/**
* 处理LIST命令
* @param string $messageId 邮件ID可选
*/
private function handleList($messageId = '') {
if (!$this->authenticated || $this->state !== 'transaction') {
$this->sendResponse(false, "Not authenticated");
return;
}
if (empty($messageId)) {
// 列出所有邮件
$response = "{$this->messageCount} messages\r\n";
foreach ($this->messages as $id => $message) {
$response .= "{$id} {$message['size']}\r\n";
}
$response .= ".\r\n";
$this->sendRawResponse("+OK " . $response);
} else {
// 列出指定邮件
$id = (int)$messageId;
if (isset($this->messages[$id])) {
$this->sendResponse(true, "{$id} {$this->messages[$id]['size']}");
} else {
$this->sendResponse(false, "No such message");
}
}
$this->logger->info("LIST command received from {ip}, messageId: {messageId}", [
'ip' => $this->clientIp,
'messageId' => $messageId
]);
}
/**
* 处理RETR命令
* @param string $messageId 邮件ID
*/
private function handleRetr($messageId) {
if (!$this->authenticated || $this->state !== 'transaction') {
$this->sendResponse(false, "Not authenticated");
return;
}
$id = (int)$messageId;
if (!isset($this->messages[$id])) {
$this->sendResponse(false, "No such message");
return;
}
$message = $this->messages[$id];
$response = "{$message['size']} octets\r\n";
$response .= $message['content'] . "\r\n";
$response .= ".\r\n";
$this->sendRawResponse("+OK " . $response);
$this->logger->info("RETR command received from {ip}, messageId: {messageId}", [
'ip' => $this->clientIp,
'messageId' => $messageId
]);
}
/**
* 处理DELE命令
* @param string $messageId 邮件ID
*/
private function handleDele($messageId) {
if (!$this->authenticated || $this->state !== 'transaction') {
$this->sendResponse(false, "Not authenticated");
return;
}
$id = (int)$messageId;
if (!isset($this->messages[$id])) {
$this->sendResponse(false, "No such message");
return;
}
$this->deletedMessages[$id] = true;
$this->sendResponse(true, "Message {$id} marked for deletion");
$this->logger->info("DELE command received from {ip}, messageId: {messageId}", [
'ip' => $this->clientIp,
'messageId' => $messageId
]);
}
/**
* 处理NOOP命令
*/
private function handleNoop() {
if (!$this->authenticated) {
$this->sendResponse(false, "Not authenticated");
return;
}
$this->sendResponse(true, "OK");
$this->logger->info("NOOP command received from {ip}", [
'ip' => $this->clientIp
]);
}
/**
* 处理RSET命令
*/
private function handleRset() {
if (!$this->authenticated || $this->state !== 'transaction') {
$this->sendResponse(false, "Not authenticated");
return;
}
$this->deletedMessages = [];
$this->sendResponse(true, "Reset completed");
$this->logger->info("RSET command received from {ip}", [
'ip' => $this->clientIp
]);
}
/**
* 处理QUIT命令
*/
private function handleQuit() {
$this->state = 'update';
$deletedCount = 0;
// 删除标记的邮件
if (!empty($this->deletedMessages)) {
try {
require_once __DIR__ . '/../utils/Database.php';
$db = Database::getInstance();
$pdo = $db->beginTransaction();
foreach ($this->deletedMessages as $id => $value) {
if (isset($this->messages[$id]) && isset($this->messages[$id]['email_id'])) {
$emailId = $this->messages[$id]['email_id'];
$sql = "DELETE FROM emails WHERE id = ?";
$db->execute($sql, [$emailId]);
$deletedCount++;
}
}
$db->commit($pdo);
} catch (Exception $e) {
$db->rollback($pdo);
$this->logger->error("Failed to delete messages: {error}", [
'error' => $e->getMessage()
]);
}
}
$this->sendResponse(true, "Bye");
$this->logger->info("QUIT command received from {ip}, deleted {count} messages", [
'ip' => $this->clientIp,
'count' => $deletedCount
]);
// 不再直接关闭socket而是让Pop3Server通过检测连接关闭来处理
}
/**
* 处理TOP命令
* @param string $params 命令参数
*/
private function handleTop($params) {
if (!$this->authenticated || $this->state !== 'transaction') {
$this->sendResponse(false, "Not authenticated");
return;
}
$parts = explode(' ', $params);
if (count($parts) !== 2) {
$this->sendResponse(false, "Invalid parameters");
return;
}
$id = (int)$parts[0];
$lines = (int)$parts[1];
if (!isset($this->messages[$id])) {
$this->sendResponse(false, "No such message");
return;
}
$message = $this->messages[$id];
$headers = substr($message['content'], 0, strpos($message['content'], "\r\n\r\n"));
$contentLines = explode("\r\n", substr($message['content'], strpos($message['content'], "\r\n\r\n") + 4));
$topContent = implode("\r\n", array_slice($contentLines, 0, $lines));
$response = "Top of message {$id}\r\n";
$response .= $headers . "\r\n\r\n" . $topContent . "\r\n";
$response .= ".\r\n";
$this->sendRawResponse("+OK " . $response);
$this->logger->info("TOP command received from {ip}, messageId: {id}, lines: {lines}", [
'ip' => $this->clientIp,
'id' => $id,
'lines' => $lines
]);
}
/**
* 处理UIDL命令
* @param string $messageId 邮件ID可选
*/
private function handleUidl($messageId = '') {
if (!$this->authenticated || $this->state !== 'transaction') {
$this->sendResponse(false, "Not authenticated");
return;
}
if (empty($messageId)) {
// 列出所有邮件的UID
$response = "{$this->messageCount} messages\r\n";
foreach ($this->messages as $id => $message) {
$response .= "{$id} {$message['uid']}\r\n";
}
$response .= ".\r\n";
$this->sendRawResponse("+OK " . $response);
} else {
// 列出指定邮件的UID
$id = (int)$messageId;
if (isset($this->messages[$id])) {
$this->sendResponse(true, "{$id} {$this->messages[$id]['uid']}");
} else {
$this->sendResponse(false, "No such message");
}
}
$this->logger->info("UIDL command received from {ip}, messageId: {messageId}", [
'ip' => $this->clientIp,
'messageId' => $messageId
]);
}
/**
* 发送响应给客户端
* @param bool $success 是否成功
* @param string $message 响应消息
*/
private function sendResponse($success, $message) {
$prefix = $success ? "+OK" : "-ERR";
$response = "$prefix $message\r\n";
socket_write($this->socket, $response, strlen($response));
$this->logger->debug("Sent response to {ip}: {response}", [
'ip' => $this->clientIp,
'response' => rtrim($response)
]);
}
/**
* 发送原始响应给客户端
* @param string $response 响应消息
*/
private function sendRawResponse($response) {
socket_write($this->socket, $response, strlen($response));
$this->logger->debug("Sent raw response to {ip}: {response}", [
'ip' => $this->clientIp,
'response' => rtrim($response)
]);
}
/**
* 初始化邮件列表(从数据库获取)
* @param string $username 用户名
*/
private function initMessages($username) {
try {
require_once __DIR__ . '/../utils/Database.php';
$db = Database::getInstance();
// 查询用户邮件
$sql = "SELECT * FROM emails WHERE to_address = (SELECT email FROM user WHERE username = ? AND is_deleted = 0) ORDER BY send_time ASC";
$emails = $db->query($sql, [$username])->fetchAll();
$this->messages = [];
$this->messageCount = 0;
$this->mailboxSize = 0;
$this->deletedMessages = [];
if ($emails) {
$id = 1;
foreach ($emails as $email) {
// 构建完整的邮件内容
$content = "From: {$email['from_address']}\r\n" .
"To: {$email['to_address']}\r\n" .
"Subject: {$email['subject']}\r\n" .
"Date: {$email['created_at']}\r\n" .
"\r\n" .
"{$email['content']}\r\n";
$this->messages[$id] = [
'uid' => $email['id'],
'size' => strlen($content),
'content' => $content,
'email_id' => $email['id'] // 保存数据库中的实际ID
];
$this->mailboxSize += $this->messages[$id]['size'];
$id++;
}
}
$this->messageCount = count($this->messages);
$this->logger->info("Loaded {count} emails for user {username}", [
'count' => $this->messageCount,
'username' => $this->username
]);
} catch (Exception $e) {
$this->messages = [];
$this->messageCount = 0;
$this->mailboxSize = 0;
$this->deletedMessages = [];
$this->logger->error("Failed to load messages for user {username}: {error}", [
'username' => $this->username,
'error' => $e->getMessage()
]);
}
}
}

@ -0,0 +1,259 @@
<?php
/**
* POP3服务器核心类
*/
require_once __DIR__ . '/../utils/Config.php';
require_once __DIR__ . '/../utils/Logger.php';
require_once __DIR__ . '/Pop3Handler.php';
class Pop3Server {
private $host;
private $port;
private $maxConnections;
private $socket;
private $connections = [];
private $logger;
private $config;
/**
* 构造函数
* @param string $host 监听地址
* @param int $port 监听端口
* @param int $maxConnections 最大连接数
*/
public function __construct($host = '0.0.0.0', $port = 110, $maxConnections = 100) {
$this->config = Config::getInstance();
$this->host = $host;
$this->port = $port;
$this->maxConnections = $maxConnections;
// 初始化日志记录器
$logPath = $this->config->get('log.path', '../logs/');
$logLevel = $this->config->get('log.level', 'info');
$this->logger = new Logger($logPath, $logLevel);
}
/**
* 启动POP3服务器
* @return bool 是否启动成功
*/
public function start() {
// 创建socket
$this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($this->socket === false) {
$this->logger->error("Failed to create socket: " . socket_strerror(socket_last_error()));
return false;
}
// 设置socket选项
socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, 1);
// 绑定地址和端口
if (!socket_bind($this->socket, $this->host, $this->port)) {
$this->logger->error("Failed to bind socket: " . socket_strerror(socket_last_error($this->socket)));
socket_close($this->socket);
return false;
}
// 开始监听,提高监听队列长度
$backlog = $this->config->get('pop3.backlog', 50);
if (!socket_listen($this->socket, $backlog)) {
$this->logger->error("Failed to listen on socket: " . socket_strerror(socket_last_error($this->socket)));
socket_close($this->socket);
return false;
}
$this->logger->info("POP3 server started on {host}:{port}", ['host' => $this->host, 'port' => $this->port]);
// 主循环,处理连接
while (true) {
// 处理客户端连接和请求
$this->handleConnections();
}
return true;
}
/**
* 停止POP3服务器
*/
public function stop() {
// 关闭所有客户端连接
foreach ($this->connections as $connection) {
socket_close($connection['socket']);
}
// 关闭服务器socket
if ($this->socket) {
socket_close($this->socket);
}
$this->logger->info("POP3 server stopped");
}
/**
* 处理客户端请求
*/
private function handleConnections() {
// 检查是否有客户端数据可读
$readSockets = array_column($this->connections, 'socket');
$readSockets[] = $this->socket;
$writeSockets = null;
$exceptSockets = null;
$activity = socket_select($readSockets, $writeSockets, $exceptSockets, 1);
if ($activity === false) {
$this->logger->error("Socket select error: " . socket_strerror(socket_last_error()));
return;
}
// 处理可读的socket
foreach ($readSockets as $socket) {
// 如果是服务器socket接受新连接
if ($socket === $this->socket) {
$this->acceptNewConnection();
} else {
// 处理客户端请求
$this->handleClientRequest($socket);
}
}
// 清理超时连接
$this->cleanupTimeoutConnections();
}
/**
* 接受新连接
*/
private function acceptNewConnection() {
// 检查连接数是否超过最大值
if (count($this->connections) >= $this->maxConnections) {
$this->logger->warning("Maximum connections reached: {max}", ['max' => $this->maxConnections]);
return;
}
// 接受新连接
$clientSocket = socket_accept($this->socket);
if ($clientSocket === false) {
$this->logger->error("Failed to accept connection: " . socket_strerror(socket_last_error($this->socket)));
return;
}
// 获取客户端信息
socket_getpeername($clientSocket, $clientIp, $clientPort);
$this->logger->info("New connection from {ip}:{port}", ['ip' => $clientIp, 'port' => $clientPort]);
// 添加到连接列表
$connectionId = uniqid();
$socketKey = (int)$clientSocket;
$this->connections[$connectionId] = [
'socket' => $clientSocket,
'ip' => $clientIp,
'port' => $clientPort,
'handler' => new Pop3Handler($clientSocket, $clientIp, $this->logger),
'lastActivity' => time()
];
// 维护socket到connectionId的映射
static $socketToConnection = [];
$socketToConnection[$socketKey] = $connectionId;
// 发送欢迎消息
$welcomeMsg = "+OK POP3 Service Ready\r\n";
socket_write($clientSocket, $welcomeMsg, strlen($welcomeMsg));
}
/**
* 处理客户端请求
* @param resource $socket 客户端socket
*/
private function handleClientRequest($socket) {
// 优化使用socket资源作为键直接查找
$socketKey = (int)$socket;
static $socketToConnection = [];
// 初始化映射表(如果为空)
if (empty($socketToConnection) && !empty($this->connections)) {
foreach ($this->connections as $id => $connection) {
$socketToConnection[(int)$connection['socket']] = $id;
}
}
// 查找对应的连接
if (!isset($socketToConnection[$socketKey])) {
// 重建映射表并再次查找
$socketToConnection = [];
foreach ($this->connections as $id => $connection) {
$socketToConnection[(int)$connection['socket']] = $id;
}
if (!isset($socketToConnection[$socketKey])) {
return;
}
}
$connectionId = $socketToConnection[$socketKey];
// 读取客户端数据
$data = socket_read($socket, 1024);
if ($data === false) {
$this->logger->error("Failed to read from socket: " . socket_strerror(socket_last_error($socket)));
$this->closeConnection($connectionId);
return;
}
// 如果客户端关闭连接
if (empty($data)) {
$this->logger->info("Connection closed by client: {ip}:{port}", [
'ip' => $this->connections[$connectionId]['ip'],
'port' => $this->connections[$connectionId]['port']
]);
$this->closeConnection($connectionId);
return;
}
// 更新最后活动时间
$this->connections[$connectionId]['lastActivity'] = time();
// 处理数据
$this->connections[$connectionId]['handler']->handle($data);
}
/**
* 关闭客户端连接
* @param string $connectionId 连接ID
*/
private function closeConnection($connectionId) {
if (isset($this->connections[$connectionId])) {
$socket = $this->connections[$connectionId]['socket'];
$socketKey = (int)$socket;
// 从映射表中移除
static $socketToConnection = [];
if (isset($socketToConnection[$socketKey])) {
unset($socketToConnection[$socketKey]);
}
socket_close($socket);
unset($this->connections[$connectionId]);
}
}
/**
* 清理超时连接
*/
private function cleanupTimeoutConnections() {
$timeout = 300; // 5分钟超时
$now = time();
foreach ($this->connections as $id => $connection) {
if ($now - $connection['lastActivity'] > $timeout) {
$this->logger->info("Connection timed out: {ip}:{port}", [
'ip' => $connection['ip'],
'port' => $connection['port']
]);
$this->closeConnection($id);
}
}
}
}

@ -0,0 +1,65 @@
<?php
/**
* POP3服务器入口文件
*/
require_once __DIR__ . '/Pop3Server.php';
require_once __DIR__ . '/../utils/Config.php';
require_once __DIR__ . '/../utils/Logger.php';
// 解析命令行参数
$options = getopt("h:p:m:", ["host:", "port:", "max-connections:", "help"]);
// 显示帮助信息
if (isset($options['help'])) {
echo "POP3 Server Usage:\n";
echo "php index.php [options]\n";
echo "Options:\n";
echo " -h, --host <host> Listen host (default: 0.0.0.0)\n";
echo " -p, --port <port> Listen port (default: 110)\n";
echo " -m, --max-connections <num> Maximum connections (default: 100)\n";
echo " --help Show this help message\n";
exit(0);
}
// 从配置文件获取默认值
$config = Config::getInstance();
$host = $options['h'] ?? $options['host'] ?? $config->get('pop3.host', '0.0.0.0');
$port = $options['p'] ?? $options['port'] ?? $config->get('pop3.port', 110);
$maxConnections = $options['m'] ?? $options['max-connections'] ?? $config->get('pop3.max_connections', 100);
// 转换端口和最大连接数为整数
$port = (int)$port;
$maxConnections = (int)$maxConnections;
// 验证参数
if ($port < 1 || $port > 65535) {
echo "Invalid port number: $port\n";
exit(1);
}
if ($maxConnections < 1 || $maxConnections > 1000) {
echo "Invalid maximum connections: $maxConnections\n";
exit(1);
}
// 创建并启动POP3服务器
$server = new Pop3Server($host, $port, $maxConnections);
echo "Starting POP3 Server on $host:$port...\n";
echo "Maximum connections: $maxConnections\n";
echo "Press Ctrl+C to stop the server\n\n";
// 捕获Ctrl+C信号优雅关闭服务器仅在支持PCNTL的系统上
if (function_exists('pcntl_signal')) {
declare(ticks = 1);
pcntl_signal(SIGINT, function() use ($server) {
echo "\nStopping POP3 Server...\n";
$server->stop();
exit(0);
});
}
// 启动服务器
if (!$server->start()) {
echo "Failed to start POP3 Server\n";
exit(1);
}

@ -0,0 +1,131 @@
const express = require('express')
const cors = require('cors')
const bodyParser = require('body-parser')
const mysql = require('mysql2/promise')
const bcrypt = require('bcryptjs')
const app = express()
app.use(cors({ origin: '*', methods: 'GET,HEAD,PUT,PATCH,POST,DELETE' }))
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
const ok = data => ({ success: true, data })
const now = () => new Date().toISOString()
let pool = null
const initDb = async () => {
const url = process.env.DATABASE_URL
if (!url) return
const u = new URL(url)
const config = {
host: u.hostname,
port: Number(u.port || 3306),
user: decodeURIComponent(u.username || ''),
password: decodeURIComponent(u.password || ''),
database: (u.pathname || '').replace(/^\//, '') || process.env.DB_NAME || 'smtp',
waitForConnections: true,
connectionLimit: Number(process.env.DB_CONN_LIMIT || 10),
queueLimit: 0
}
pool = mysql.createPool(config)
}
const dbPing = async () => {
if (!pool) return { connected: false, message: 'DATABASE_URL not set' }
const [rows] = await pool.query('SELECT 1 AS ok')
return { connected: true, rows }
}
app.get('/api/db/ping', async (req, res) => {
try {
const r = await dbPing()
res.json(ok(r))
} catch (e) {
res.status(500).json({ success: false, message: String(e) })
}
})
app.get('/api/settings.php', (req, res) => {
res.json(ok({
smtp: { port: 25, host: '0.0.0.0', max_connections: 100 },
pop3: { port: 110, host: '0.0.0.0', max_connections: 100 },
server: { domain: 'test.com', max_email_size: 10485760, max_mailbox_size: 104857600 },
log: { path: '../logs/', level: 'info' }
}))
})
app.post('/api/settings.php', (req, res) => res.json(ok({ saved: true, at: now(), payload: req.body || {} })))
app.get('/api/users.php', (req, res) => {
const users = [
{ username: 'admin', email: 'admin@test.com', is_admin: true, is_enabled: true, created_at: now(), updated_at: now() },
{ username: 'user', email: 'user@test.com', is_admin: false, is_enabled: true, created_at: now(), updated_at: now() }
]
const page = Number(req.query.page || 1), perPage = Number(req.query.perPage || 10)
res.json(ok({ users, total: users.length, page, perPage, totalPages: 1 }))
})
app.post('/api/users.php', async (req, res) => {
const { login, username, password } = req.body || {}
if (login) {
try {
if (!pool) return res.status(500).json({ success: false, message: 'Database not initialized' })
const [rows] = await pool.query('SELECT * FROM `user` WHERE username = ? AND is_deleted = 0 AND is_enabled = 1 LIMIT 1', [username])
const user = rows && rows[0]
if (!user) return res.status(401).json({ success: false, message: 'Invalid credentials' })
const stored = user.password || ''
const okPass = stored.startsWith('$2') ? bcrypt.compareSync(password, stored) : (stored === password)
if (!okPass) return res.status(401).json({ success: false, message: 'Invalid credentials' })
const token = Buffer.from(`${username}:${now()}`).toString('base64')
return res.json(ok({ token, username, login_at: now() }))
} catch (e) {
return res.status(500).json({ success: false, message: String(e) })
}
}
res.json(ok({ created: true, user: req.body || {} }))
})
app.delete('/api/users.php', (req, res) => res.json(ok({ deleted: true, username: req.query.username || '' })))
app.get('/api/emails.php', (req, res) => {
const { id, username, folder = 'inbox', page = 1, perPage = 10 } = req.query
if (id) return res.json(ok({ email: null }))
const emails = [
{ id: '1234567890', from: 'sender@example.com', to: 'admin@test.com', subject: '测试邮件', date: now(), folder, is_read: 0, is_deleted: 0, data: '这是邮件内容', created_at: now() },
]
res.json(ok({ emails, total: emails.length, page: Number(page), perPage: Number(perPage), totalPages: 1 }))
})
app.post('/api/emails.php', (req, res) => res.json(ok({ sent: !req.body?.isDraft, draft: !!req.body?.isDraft, payload: req.body || {} })))
app.put('/api/emails.php', (req, res) => res.json(ok({ updated: true, payload: req.body || {} })))
app.delete('/api/emails.php', (req, res) => res.json(ok({ deleted: true, id: req.query.id || '', username: req.query.username || '' })))
app.get('/api/logs.php', (req, res) => {
const logs = [{ id: 1, type: 'info', message: 'Server started', ip: '127.0.0.1', user_id: 'system', created_at: now() }]
res.json(ok({ logs, total: logs.length, page: Number(req.query.page || 1), perPage: Number(req.query.perPage || 10), totalPages: 1 }))
})
app.delete('/api/logs.php', (req, res) => res.json(ok({ deleted: req.query.all ? 'all' : req.query.id || '' })))
app.get('/api/contacts.php', (req, res) => {
const { id } = req.query
const item = { id: 1, name: '张三', email: 'zhangsan@example.com', phone: '13800138000', company: '示例公司', department: '技术部', position: '工程师', create_time: now(), update_time: now() }
if (id) return res.json(ok({ contact: item }))
res.json(ok({ contacts: [item], total: 1, page: Number(req.query.page || 1), perPage: Number(req.query.perPage || 20), totalPages: 1 }))
})
app.post('/api/contacts.php', (req, res) => res.json(ok({ created: true, contact: req.body || {} })))
app.put('/api/contacts.php', (req, res) => res.json(ok({ updated: true, contact: req.body || {} })))
app.delete('/api/contacts.php', (req, res) => res.json(ok({ deleted: true, id: Number(req.query.id || 0) })))
app.get('/api/stats.php', (req, res) => {
res.json(ok({ typeStats: { inbox: 0, sent: 0, draft: 0, trash: 0, total: 0 }, readStats: { read: 0, unread: 0 } }))
})
app.get('/api/mailbox.php', (req, res) => {
const { action } = req.query
if (action === 'quota') return res.json(ok({ quota: 1073741824, used: 0, remaining: 1073741824, percentage: 0 }))
res.json(ok({ email: 'admin@test.com', stats: { inbox: 0, sent: 0, draft: 0, trash: 0, unread: 0 }, customFolders: [] }))
})
const port = process.env.PORT || 8000
initDb()
.catch(e => console.error('DB init error', e))
.finally(() => {
app.listen(port, () => {
console.log(`API server on http://localhost:${port}/`)
})
})

@ -0,0 +1,433 @@
<?php
/**
* SMTP协议处理类
*/
require_once __DIR__ . '/../utils/Config.php';
class SmtpHandler {
private $socket;
private $clientIp;
private $logger;
private $config;
private $state = 'init'; // 状态init, helo, mail, rcpt, data, quit
private $heloHost = '';
private $fromAddress = '';
private $toAddresses = [];
private $dataBuffer = '';
private $dataSize = 0;
private $maxDataSize = 10 * 1024 * 1024; // 10MB
/**
* 构造函数
* @param resource $socket 客户端socket
* @param string $clientIp 客户端IP地址
* @param Logger $logger 日志记录器
*/
public function __construct($socket, $clientIp, $logger) {
$this->socket = $socket;
$this->clientIp = $clientIp;
$this->logger = $logger;
$this->config = Config::getInstance();
$this->maxDataSize = $this->config->get('server.max_email_size', 10 * 1024 * 1024);
}
/**
* 处理客户端数据
* @param string $data 客户端发送的数据
*/
public function handle($data) {
$this->logger->debug("Received data from {ip}: {data}", [
'ip' => $this->clientIp,
'data' => rtrim($data)
]);
// 如果处于数据状态,直接处理数据
if ($this->state === 'data') {
$this->handleDataContent($data);
} else {
// 按行处理命令
$lines = explode("\r\n", $data);
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) {
continue;
}
$this->processCommand($line);
}
}
}
/**
* 处理SMTP命令
* @param string $command SMTP命令
*/
private function processCommand($command) {
// 解析命令和参数
$parts = preg_split('/\s+/', $command, 2);
$cmd = strtoupper($parts[0]);
$params = isset($parts[1]) ? $parts[1] : '';
// 根据命令调用相应的处理方法
switch ($cmd) {
case 'HELO':
case 'EHLO':
$this->handleHelo($params);
break;
case 'MAIL':
$this->handleMail($params);
break;
case 'RCPT':
$this->handleRcpt($params);
break;
case 'DATA':
$this->handleData();
break;
case 'RSET':
$this->handleRset();
break;
case 'NOOP':
$this->handleNoop();
break;
case 'QUIT':
$this->handleQuit();
break;
default:
$this->sendResponse(500, "500 Syntax error, command unrecognized");
break;
}
}
/**
* 处理HELO/EHLO命令
* @param string $params 命令参数
*/
private function handleHelo($params) {
$this->heloHost = $params;
$this->state = 'helo';
$this->sendResponse(250, "250 {domain} Hello {host} [{ip}]");
$this->logger->info("HELO/EHLO command received from {ip}, host: {host}", [
'ip' => $this->clientIp,
'host' => $this->heloHost
]);
}
/**
* 处理MAIL FROM命令
* @param string $params 命令参数
*/
private function handleMail($params) {
if ($this->state !== 'helo') {
$this->sendResponse(503, "503 Bad sequence of commands");
$this->logger->warning("MAIL FROM command out of sequence from {ip}", [
'ip' => $this->clientIp
]);
return;
}
// 解析发件人地址
preg_match('/^FROM:\s*<([^>]*)>/i', $params, $matches);
if (empty($matches)) {
$this->sendResponse(501, "501 Syntax error in parameters or arguments");
$this->logger->warning("Invalid MAIL FROM syntax from {ip}: {params}", [
'ip' => $this->clientIp,
'params' => $params
]);
return;
}
$sender = $matches[1];
// 验证发件人邮箱格式
if (!filter_var($sender, FILTER_VALIDATE_EMAIL)) {
$this->sendResponse(553, "553 5.1.8 Invalid sender email address");
$this->logger->warning("Invalid sender email from {ip}: {sender}", [
'ip' => $this->clientIp,
'sender' => $sender
]);
return;
}
$this->fromAddress = $sender;
$this->state = 'mail';
$this->toAddresses = [];
$this->sendResponse(250, "250 2.1.0 Sender OK");
$this->logger->info("MAIL FROM command received from {ip}, from: {from}", [
'ip' => $this->clientIp,
'from' => $this->fromAddress
]);
}
/**
* 处理RCPT TO命令
* @param string $params 命令参数
*/
private function handleRcpt($params) {
if ($this->state !== 'mail') {
$this->sendResponse(503, "503 Bad sequence of commands");
$this->logger->warning("RCPT TO command out of sequence from {ip}", [
'ip' => $this->clientIp
]);
return;
}
// 解析收件人地址
preg_match('/^TO:\s*<([^>]*)>/i', $params, $matches);
if (empty($matches)) {
$this->sendResponse(501, "501 Syntax error in parameters or arguments");
$this->logger->warning("Invalid RCPT TO syntax from {ip}: {params}", [
'ip' => $this->clientIp,
'params' => $params
]);
return;
}
$toAddress = $matches[1];
// 验证收件人邮箱格式
if (!filter_var($toAddress, FILTER_VALIDATE_EMAIL)) {
$this->sendResponse(553, "553 5.1.8 Invalid recipient email address");
$this->logger->warning("Invalid recipient email from {ip}: {to}", [
'ip' => $this->clientIp,
'to' => $toAddress
]);
return;
}
// 检查收件人数量是否超过限制
if (count($this->toAddresses) >= 100) {
$this->sendResponse(452, "452 4.5.3 Too many recipients");
$this->logger->warning("Too many recipients from {ip}", [
'ip' => $this->clientIp
]);
return;
}
$this->toAddresses[] = $toAddress;
$this->sendResponse(250, "250 2.1.5 Recipient OK");
$this->logger->info("RCPT TO command received from {ip}, to: {to}", [
'ip' => $this->clientIp,
'to' => $toAddress
]);
}
/**
* 处理DATA命令
*/
private function handleData() {
if ($this->state !== 'mail' || empty($this->toAddresses)) {
$this->sendResponse(503, "503 Bad sequence of commands");
return;
}
$this->state = 'data';
$this->dataBuffer = '';
$this->dataSize = 0;
$this->sendResponse(354, "354 Start mail input; end with <CRLF>.<CRLF>");
$this->logger->info("DATA command received from {ip}", [
'ip' => $this->clientIp
]);
}
/**
* 处理RSET命令
*/
private function handleRset() {
$this->state = 'helo';
$this->fromAddress = '';
$this->toAddresses = [];
$this->dataBuffer = '';
$this->dataSize = 0;
$this->sendResponse(250, "250 2.0.0 OK");
$this->logger->info("RSET command received from {ip}", [
'ip' => $this->clientIp
]);
}
/**
* 处理NOOP命令
*/
private function handleNoop() {
$this->sendResponse(250, "250 2.0.0 OK");
$this->logger->info("NOOP command received from {ip}", [
'ip' => $this->clientIp
]);
}
/**
* 处理DATA内容
* @param string $data 邮件内容
*/
private function handleDataContent($data) {
// 添加到缓冲区
$this->dataBuffer .= $data;
$this->dataSize += strlen($data);
// 检查数据大小是否超过限制
if ($this->dataSize > $this->maxDataSize) {
$this->sendResponse(552, "552 5.2.3 Message exceeds maximum size");
$this->state = 'helo';
$this->dataBuffer = '';
$this->dataSize = 0;
$this->logger->warning("Message from {ip} exceeds maximum size", [
'ip' => $this->clientIp
]);
return;
}
// 检查是否收到结束标记
if (substr($this->dataBuffer, -5) === "\r\n.\r\n") {
// 去除结束标记
$emailContent = substr($this->dataBuffer, 0, -5);
// 处理邮件
$this->processEmail($emailContent);
// 重置状态
$this->state = 'helo';
$this->dataBuffer = '';
$this->dataSize = 0;
}
}
/**
* 处理并保存邮件
* @param string $emailContent 邮件内容
*/
private function processEmail($emailContent) {
// 解析邮件头
$headers = $this->parseEmailHeaders($emailContent);
// 提取邮件主题和正文
$subject = $headers['Subject'] ?? '无主题';
$body = $this->extractEmailBody($emailContent);
// 保存邮件到数据库
$this->saveEmailToDatabase($headers, $subject, $body);
// 发送成功响应
$this->sendResponse(250, "250 2.0.0 OK: queued as {message_id}");
$this->logger->info("Email from {from} to {to} saved successfully", [
'from' => $this->fromAddress,
'to' => implode(', ', $this->toAddresses)
]);
}
/**
* 解析邮件头
* @param string $emailContent 邮件内容
* @return array 邮件头
*/
private function parseEmailHeaders($emailContent) {
$headers = [];
$lines = explode("\r\n", $emailContent);
// 解析邮件头
foreach ($lines as $line) {
if (empty($line)) {
break; // 邮件头结束
}
// 处理多行邮件头
if (preg_match('/^\s+/', $line)) {
// 多行邮件头,追加到上一个头
$lastHeader = array_key_last($headers);
$headers[$lastHeader] .= ' ' . trim($line);
} else {
// 新的邮件头
$parts = explode(':', $line, 2);
if (count($parts) === 2) {
$name = trim($parts[0]);
$value = trim($parts[1]);
$headers[$name] = $value;
}
}
}
return $headers;
}
/**
* 提取邮件正文
* @param string $emailContent 邮件内容
* @return string 邮件正文
*/
private function extractEmailBody($emailContent) {
$parts = explode("\r\n\r\n", $emailContent, 2);
return isset($parts[1]) ? $parts[1] : '';
}
/**
* 保存邮件到数据库
* @param array $headers 邮件头
* @param string $subject 邮件主题
* @param string $body 邮件正文
*/
private function saveEmailToDatabase($headers, $subject, $body) {
try {
// 包含数据库类
require_once __DIR__ . '/../utils/Database.php';
// 连接数据库
$db = Database::getInstance();
// 遍历收件人地址
foreach ($this->toAddresses as $toAddress) {
// 根据收件人邮箱查找对应的用户名
$sql = "SELECT username FROM user WHERE email = ? AND is_deleted = 0";
$user = $db->fetchOne($sql, [$toAddress]);
$userId = $user ? $user['username'] : 'admin'; // 如果找不到使用默认用户名admin为管理员
// 保存邮件
$sql = "INSERT INTO emails (from_address, to_address, subject, content, user_id, send_time) VALUES (?, ?, ?, ?, ?, NOW())";
$db->insert($sql, [
$this->fromAddress,
$toAddress,
$subject,
$body,
$userId
]);
}
} catch (Exception $e) {
$this->logger->error("Failed to save email: {error}", [
'error' => $e->getMessage()
]);
}
}
/**
* 处理QUIT命令
*/
private function handleQuit() {
$this->sendResponse(221, "221 2.0.0 Bye");
$this->logger->info("QUIT command received from {ip}", [
'ip' => $this->clientIp
]);
// 不再直接关闭socket而是让SmtpServer通过检测连接关闭来处理
}
/**
* 发送响应给客户端
* @param int $code 响应代码
* @param string $message 响应消息
*/
private function sendResponse($code, $message) {
// 替换占位符
$domain = $this->config->get('server.domain', 'test.com');
$message = str_replace('{domain}', $domain, $message);
$message = str_replace('{ip}', $this->clientIp, $message);
$message = str_replace('{host}', $this->heloHost, $message);
// 添加换行符
$response = $message . "\r\n";
// 发送响应
socket_write($this->socket, $response, strlen($response));
$this->logger->debug("Sent response to {ip}: {response}", [
'ip' => $this->clientIp,
'response' => rtrim($response)
]);
}
}

@ -0,0 +1,260 @@
<?php
/**
* SMTP服务器核心类
*/
require_once __DIR__ . '/../utils/Config.php';
require_once __DIR__ . '/../utils/Logger.php';
require_once __DIR__ . '/SmtpHandler.php';
class SmtpServer {
private $host;
private $port;
private $maxConnections;
private $socket;
private $connections = [];
private $logger;
private $config;
/**
* 构造函数
* @param string $host 监听地址
* @param int $port 监听端口
* @param int $maxConnections 最大连接数
*/
public function __construct($host = '0.0.0.0', $port = 25, $maxConnections = 100) {
$this->config = Config::getInstance();
$this->host = $host;
$this->port = $port;
$this->maxConnections = $maxConnections;
// 初始化日志记录器
$logPath = $this->config->get('log.path', '../logs/');
$logLevel = $this->config->get('log.level', 'info');
$this->logger = new Logger($logPath, $logLevel);
}
/**
* 启动SMTP服务器
* @return bool 是否启动成功
*/
public function start() {
// 创建socket
$this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($this->socket === false) {
$this->logger->error("Failed to create socket: " . socket_strerror(socket_last_error()));
return false;
}
// 设置socket选项
socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, 1);
// 绑定地址和端口
if (!socket_bind($this->socket, $this->host, $this->port)) {
$this->logger->error("Failed to bind socket: " . socket_strerror(socket_last_error($this->socket)));
socket_close($this->socket);
return false;
}
// 开始监听,提高监听队列长度
$backlog = $this->config->get('smtp.backlog', 50);
if (!socket_listen($this->socket, $backlog)) {
$this->logger->error("Failed to listen on socket: " . socket_strerror(socket_last_error($this->socket)));
socket_close($this->socket);
return false;
}
$this->logger->info("SMTP server started on {host}:{port}", ['host' => $this->host, 'port' => $this->port]);
// 主循环,处理连接
while (true) {
// 处理客户端连接和请求
$this->handleConnections();
}
return true;
}
/**
* 停止SMTP服务器
*/
public function stop() {
// 关闭所有客户端连接
foreach ($this->connections as $connection) {
socket_close($connection['socket']);
}
// 关闭服务器socket
if ($this->socket) {
socket_close($this->socket);
}
$this->logger->info("SMTP server stopped");
}
/**
* 处理客户端请求
*/
private function handleConnections() {
// 检查是否有客户端数据可读
$readSockets = array_column($this->connections, 'socket');
$readSockets[] = $this->socket;
$writeSockets = null;
$exceptSockets = null;
$activity = socket_select($readSockets, $writeSockets, $exceptSockets, 1);
if ($activity === false) {
$this->logger->error("Socket select error: " . socket_strerror(socket_last_error()));
return;
}
// 处理可读的socket
foreach ($readSockets as $socket) {
// 如果是服务器socket接受新连接
if ($socket === $this->socket) {
$this->acceptNewConnection();
} else {
// 处理客户端请求
$this->handleClientRequest($socket);
}
}
// 清理超时连接
$this->cleanupTimeoutConnections();
}
/**
* 接受新连接
*/
private function acceptNewConnection() {
// 检查连接数是否超过最大值
if (count($this->connections) >= $this->maxConnections) {
$this->logger->warning("Maximum connections reached: {max}", ['max' => $this->maxConnections]);
return;
}
// 接受新连接
$clientSocket = socket_accept($this->socket);
if ($clientSocket === false) {
$this->logger->error("Failed to accept connection: " . socket_strerror(socket_last_error($this->socket)));
return;
}
// 获取客户端信息
socket_getpeername($clientSocket, $clientIp, $clientPort);
$this->logger->info("New connection from {ip}:{port}", ['ip' => $clientIp, 'port' => $clientPort]);
// 添加到连接列表
$connectionId = uniqid();
$socketKey = (int)$clientSocket;
$this->connections[$connectionId] = [
'socket' => $clientSocket,
'ip' => $clientIp,
'port' => $clientPort,
'handler' => new SmtpHandler($clientSocket, $clientIp, $this->logger),
'lastActivity' => time()
];
// 维护socket到connectionId的映射
static $socketToConnection = [];
$socketToConnection[$socketKey] = $connectionId;
// 发送欢迎消息
$welcomeMsg = "220 {domain} ESMTP Service Ready\r\n";
socket_write($clientSocket, $welcomeMsg, strlen($welcomeMsg));
}
/**
* 处理客户端请求
* @param resource $socket 客户端socket
*/
private function handleClientRequest($socket) {
// 优化使用socket资源作为键直接查找
// 在acceptNewConnection中维护socket到connectionId的映射
$socketKey = (int)$socket;
static $socketToConnection = [];
// 初始化映射表(如果为空)
if (empty($socketToConnection) && !empty($this->connections)) {
foreach ($this->connections as $id => $connection) {
$socketToConnection[(int)$connection['socket']] = $id;
}
}
// 查找对应的连接
if (!isset($socketToConnection[$socketKey])) {
// 重建映射表并再次查找
$socketToConnection = [];
foreach ($this->connections as $id => $connection) {
$socketToConnection[(int)$connection['socket']] = $id;
}
if (!isset($socketToConnection[$socketKey])) {
return;
}
}
$connectionId = $socketToConnection[$socketKey];
// 读取客户端数据
$data = socket_read($socket, 1024);
if ($data === false) {
$this->logger->error("Failed to read from socket: " . socket_strerror(socket_last_error($socket)));
$this->closeConnection($connectionId);
return;
}
// 如果客户端关闭连接
if (empty($data)) {
$this->logger->info("Connection closed by client: {ip}:{port}", [
'ip' => $this->connections[$connectionId]['ip'],
'port' => $this->connections[$connectionId]['port']
]);
$this->closeConnection($connectionId);
return;
}
// 更新最后活动时间
$this->connections[$connectionId]['lastActivity'] = time();
// 处理数据
$this->connections[$connectionId]['handler']->handle($data);
}
/**
* 关闭客户端连接
* @param string $connectionId 连接ID
*/
private function closeConnection($connectionId) {
if (isset($this->connections[$connectionId])) {
$socket = $this->connections[$connectionId]['socket'];
$socketKey = (int)$socket;
// 从映射表中移除
static $socketToConnection = [];
if (isset($socketToConnection[$socketKey])) {
unset($socketToConnection[$socketKey]);
}
socket_close($socket);
unset($this->connections[$connectionId]);
}
}
/**
* 清理超时连接
*/
private function cleanupTimeoutConnections() {
$timeout = 300; // 5分钟超时
$now = time();
foreach ($this->connections as $id => $connection) {
if ($now - $connection['lastActivity'] > $timeout) {
$this->logger->info("Connection timed out: {ip}:{port}", [
'ip' => $connection['ip'],
'port' => $connection['port']
]);
$this->closeConnection($id);
}
}
}
}

@ -0,0 +1,65 @@
<?php
/**
* SMTP服务器入口文件
*/
require_once __DIR__ . '/SmtpServer.php';
require_once __DIR__ . '/../utils/Config.php';
require_once __DIR__ . '/../utils/Logger.php';
// 解析命令行参数
$options = getopt("h:p:m:", ["host:", "port:", "max-connections:", "help"]);
// 显示帮助信息
if (isset($options['help'])) {
echo "SMTP Server Usage:\n";
echo "php index.php [options]\n";
echo "Options:\n";
echo " -h, --host <host> Listen host (default: 0.0.0.0)\n";
echo " -p, --port <port> Listen port (default: 25)\n";
echo " -m, --max-connections <num> Maximum connections (default: 100)\n";
echo " --help Show this help message\n";
exit(0);
}
// 从配置文件获取默认值
$config = Config::getInstance();
$host = $options['h'] ?? $options['host'] ?? $config->get('smtp.host', '0.0.0.0');
$port = $options['p'] ?? $options['port'] ?? $config->get('smtp.port', 25);
$maxConnections = $options['m'] ?? $options['max-connections'] ?? $config->get('smtp.max_connections', 100);
// 转换端口和最大连接数为整数
$port = (int)$port;
$maxConnections = (int)$maxConnections;
// 验证参数
if ($port < 1 || $port > 65535) {
echo "Invalid port number: $port\n";
exit(1);
}
if ($maxConnections < 1 || $maxConnections > 1000) {
echo "Invalid maximum connections: $maxConnections\n";
exit(1);
}
// 创建并启动SMTP服务器
$server = new SmtpServer($host, $port, $maxConnections);
echo "Starting SMTP Server on $host:$port...\n";
echo "Maximum connections: $maxConnections\n";
echo "Press Ctrl+C to stop the server\n\n";
// 捕获Ctrl+C信号优雅关闭服务器仅在支持PCNTL的系统上
if (function_exists('pcntl_signal')) {
declare(ticks = 1);
pcntl_signal(SIGINT, function() use ($server) {
echo "\nStopping SMTP Server...\n";
$server->stop();
exit(0);
});
}
// 启动服务器
if (!$server->start()) {
echo "Failed to start SMTP Server\n";
exit(1);
}

@ -0,0 +1,4 @@
@echo off
echo 正在启动邮件服务器...
php -S 0.0.0.0:8000 -c php.ini -t admin\
pause

@ -0,0 +1,4 @@
@echo off
echo 正在停止邮件服务器...
taskkill /F /IM php.exe /T
pause

@ -0,0 +1,122 @@
<?php
/**
* 配置管理类
*/
class Config {
private static $instance = null;
private $config = [];
/**
* 单例模式获取实例
*/
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* 构造函数,加载配置文件
*/
private function __construct() {
$configPath = __DIR__ . '/../config/config.php';
if (file_exists($configPath)) {
$this->config = include $configPath;
} else {
$this->config = $this->getDefaultConfig();
}
}
/**
* 获取默认配置
*/
private function getDefaultConfig() {
return [
'smtp' => [
'port' => 25,
'host' => '0.0.0.0',
'max_connections' => 100,
],
'pop3' => [
'port' => 110,
'host' => '0.0.0.0',
'max_connections' => 100,
],
'server' => [
'domain' => 'test.com',
'admin_password' => 'admin123',
'max_email_size' => 10 * 1024 * 1024,
],
'log' => [
'path' => '../logs/',
'level' => 'info',
'max_file_size' => 10 * 1024 * 1024,
],
'mailbox' => [
'max_size' => 100 * 1024 * 1024,
],
];
}
/**
* 获取配置值
* @param string $key 配置键,支持点号分隔,如 'smtp.port'
* @param mixed $default 默认值
* @return mixed 配置值
*/
public function get($key, $default = null) {
$keys = explode('.', $key);
$value = $this->config;
foreach ($keys as $k) {
if (isset($value[$k])) {
$value = $value[$k];
} else {
return $default;
}
}
return $value;
}
/**
* 设置配置值
* @param string $key 配置键
* @param mixed $value 配置值
*/
public function set($key, $value) {
$keys = explode('.', $key);
$config = &$this->config;
foreach ($keys as $i => $k) {
if ($i === count($keys) - 1) {
$config[$k] = $value;
} else {
if (!isset($config[$k]) || !is_array($config[$k])) {
$config[$k] = [];
}
$config = &$config[$k];
}
}
}
/**
* 保存配置到文件
* @return bool 是否保存成功
*/
public function save() {
$configPath = __DIR__ . '/../config/config.php';
$content = "<?php\n/**\n * 邮件服务器配置文件\n */\n\nreturn " . var_export($this->config, true) . ";\n";
return file_put_contents($configPath, $content) !== false;
}
/**
* 获取所有配置
* @return array 所有配置
*/
public function getAll() {
return $this->config;
}
}

@ -0,0 +1,330 @@
<?php
/**
* 数据库连接类 - 实现简单的连接池
*/
class Database {
private static $instance = null;
private $config;
private $connections = []; // 连接池
private $inUse = []; // 正在使用的连接
private $maxConnections = 10; // 最大连接数
private $minConnections = 3; // 最小连接数
/**
* 构造函数
*/
private function __construct() {
$this->config = Config::getInstance();
// 从配置获取连接池大小
$this->maxConnections = $this->config->get('database.max_connections', 10);
$this->minConnections = $this->config->get('database.min_connections', 3);
// 初始化连接池
$this->initConnectionPool();
}
/**
* 获取数据库连接实例
* @return Database 数据库连接实例
*/
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* 初始化连接池
*/
private function initConnectionPool() {
// 初始化最小数量的连接
for ($i = 0; $i < $this->minConnections; $i++) {
$this->connections[] = $this->createConnection();
}
}
/**
* 创建新的数据库连接
* @return PDO 数据库连接
* @throws Exception 连接失败时抛出异常
*/
private function createConnection() {
$dbConfig = $this->config->get('database');
$host = $dbConfig['host'];
$port = $dbConfig['port'];
$database = $dbConfig['database'];
$username = $dbConfig['username'];
$password = $dbConfig['password'];
$charset = $dbConfig['charset'];
try {
// 先连接到服务器,不指定数据库
$dsn = "mysql:host={$host};port={$port};charset={$charset}";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_PERSISTENT => true, // 使用持久连接
];
$pdo = new PDO($dsn, $username, $password, $options);
// 尝试创建数据库(如果不存在)
$pdo->exec("CREATE DATABASE IF NOT EXISTS $database CHARACTER SET $charset COLLATE {$dbConfig['collation']}");
// 选择数据库
$pdo->exec("USE $database");
return $pdo;
} catch (PDOException $e) {
throw new Exception("Database connection failed: " . $e->getMessage());
}
}
/**
* 从连接池获取可用连接
* @return PDO 可用的数据库连接
*/
private function getConnection() {
// 检查是否有可用连接
if (count($this->connections) > 0) {
$pdo = array_pop($this->connections);
// 验证连接是否有效
if (!$this->isValidConnection($pdo)) {
$pdo = $this->createConnection();
}
$this->inUse[] = $pdo;
return $pdo;
}
// 如果没有可用连接,检查是否可以创建新连接
if (count($this->inUse) < $this->maxConnections) {
$pdo = $this->createConnection();
$this->inUse[] = $pdo;
return $pdo;
}
// 连接池已满,等待可用连接(简单实现,实际应该使用更高效的机制)
usleep(1000); // 等待1毫秒
return $this->getConnection();
}
/**
* 将连接归还到连接池
* @param PDO $pdo 数据库连接
*/
private function releaseConnection($pdo) {
// 从正在使用的列表中移除
$key = array_search($pdo, $this->inUse);
if ($key !== false) {
unset($this->inUse[$key]);
// 如果连接有效,归还到连接池
if ($this->isValidConnection($pdo)) {
$this->connections[] = $pdo;
}
}
}
/**
* 验证连接是否有效
* @param PDO $pdo 数据库连接
* @return bool 是否有效
*/
private function isValidConnection($pdo) {
try {
$pdo->query("SELECT 1");
return true;
} catch (PDOException $e) {
return false;
}
}
/**
* 执行查询并返回所有结果
* @param string $sql SQL查询语句
* @param array $params 查询参数
* @return array 查询结果
*/
public function fetchAll($sql, $params = []) {
$pdo = $this->getConnection();
try {
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
} finally {
$this->releaseConnection($pdo);
}
}
/**
* 执行查询并返回第一条结果
* @param string $sql SQL查询语句
* @param array $params 查询参数
* @return array 查询结果
*/
public function fetchOne($sql, $params = []) {
$pdo = $this->getConnection();
try {
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetch();
} finally {
$this->releaseConnection($pdo);
}
}
/**
* 执行查询
* @param string $sql SQL查询语句
* @param array $params 查询参数
* @return PDOStatement PDO语句对象
*/
public function query($sql, $params = []) {
$pdo = $this->getConnection();
try {
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return $stmt;
} finally {
$this->releaseConnection($pdo);
}
}
/**
* 执行插入操作
* @param string $sql SQL插入语句
* @param array $params 查询参数
* @return int 插入的ID
*/
public function insert($sql, $params = []) {
$pdo = $this->getConnection();
try {
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return $pdo->lastInsertId();
} finally {
$this->releaseConnection($pdo);
}
}
/**
* 执行更新操作
* @param string $sql SQL更新语句
* @param array $params 查询参数
* @return int 影响的行数
*/
public function update($sql, $params = []) {
$pdo = $this->getConnection();
try {
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return $stmt->rowCount();
} finally {
$this->releaseConnection($pdo);
}
}
/**
* 执行删除操作
* @param string $sql SQL删除语句
* @param array $params 查询参数
* @return int 影响的行数
*/
public function delete($sql, $params = []) {
$pdo = $this->getConnection();
try {
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return $stmt->rowCount();
} finally {
$this->releaseConnection($pdo);
}
}
/**
* 执行SQL语句
* @param string $sql SQL语句
* @param array $params 查询参数
* @return int 影响的行数
*/
public function execute($sql, $params = []) {
$pdo = $this->getConnection();
try {
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return $stmt->rowCount();
} finally {
$this->releaseConnection($pdo);
}
}
/**
* 开始事务
* @return PDO 用于事务的连接
*/
public function beginTransaction() {
$pdo = $this->getConnection();
$pdo->beginTransaction();
// 事务期间连接不归还由commit或rollback处理
return $pdo;
}
/**
* 提交事务
* @param PDO $pdo 事务连接
*/
public function commit($pdo = null) {
if ($pdo) {
$pdo->commit();
$this->releaseConnection($pdo);
}
}
/**
* 回滚事务
* @param PDO $pdo 事务连接
*/
public function rollback($pdo = null) {
if ($pdo) {
$pdo->rollBack();
$this->releaseConnection($pdo);
}
}
/**
* 验证数据库连接
* @return bool 是否连接成功
*/
public function isConnected() {
$pdo = $this->getConnection();
try {
$pdo->query("SELECT 1");
return true;
} catch (PDOException $e) {
return false;
} finally {
$this->releaseConnection($pdo);
}
}
/**
* 获取连接池状态
* @return array 连接池状态
*/
public function getPoolStatus() {
return [
'total_connections' => count($this->connections) + count($this->inUse),
'available_connections' => count($this->connections),
'in_use_connections' => count($this->inUse),
'max_connections' => $this->maxConnections
];
}
}

@ -0,0 +1,142 @@
<?php
/**
* 辅助工具类
*/
class Helper {
/**
* 生成唯一ID
* @return string 唯一ID
*/
public static function generateId() {
return uniqid('', true);
}
/**
* 格式化日期时间
* @param mixed $timestamp 时间戳或日期时间字符串
* @param string $format 格式化字符串
* @return string 格式化后的日期时间
*/
public static function formatDateTime($timestamp, $format = 'Y-m-d H:i:s') {
if (is_numeric($timestamp)) {
return date($format, $timestamp);
} elseif (is_string($timestamp)) {
return date($format, strtotime($timestamp));
}
return date($format);
}
/**
* 验证邮箱格式
* @param string $email 邮箱地址
* @return bool 是否为有效的邮箱格式
*/
public static function validateEmail($email) {
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
/**
* 验证IP地址格式
* @param string $ip IP地址
* @return bool 是否为有效的IP地址格式
*/
public static function validateIp($ip) {
return filter_var($ip, FILTER_VALIDATE_IP) !== false;
}
/**
* 获取客户端IP地址
* @return string 客户端IP地址
*/
public static function getClientIp() {
$ip = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$forwardedIps = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
$ip = trim($forwardedIps[0]);
} elseif (isset($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
}
return $ip;
}
/**
* 加密密码
* @param string $password 原始密码
* @return string 加密后的密码
*/
public static function encryptPassword($password) {
return password_hash($password, PASSWORD_DEFAULT);
}
/**
* 验证密码
* @param string $password 原始密码
* @param string $hash 加密后的密码
* @return bool 密码是否匹配
*/
public static function verifyPassword($password, $hash) {
return password_verify($password, $hash);
}
/**
* 截断字符串
* @param string $string 原始字符串
* @param int $length 截断长度
* @param string $suffix 后缀
* @return string 截断后的字符串
*/
public static function truncate($string, $length = 100, $suffix = '...') {
if (strlen($string) <= $length) {
return $string;
}
return substr($string, 0, $length - strlen($suffix)) . $suffix;
}
/**
* 生成随机字符串
* @param int $length 字符串长度
* @param string $characters 字符集
* @return string 随机字符串
*/
public static function generateRandomString($length = 16, $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') {
$randomString = '';
$charactersLength = strlen($characters);
for ($i = 0; $i < $length; $i++) {
$randomString .= $characters[rand(0, $charactersLength - 1)];
}
return $randomString;
}
/**
* 计算文件大小的人类可读格式
* @param int $bytes 文件大小(字节)
* @param int $precision 精度
* @return string 人类可读的文件大小
*/
public static function formatFileSize($bytes, $precision = 2) {
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, $precision) . ' ' . $units[$pow];
}
/**
* 检查端口是否可用
* @param string $host 主机地址
* @param int $port 端口号
* @param int $timeout 超时时间(秒)
* @return bool 端口是否可用
*/
public static function isPortAvailable($host, $port, $timeout = 1) {
$socket = @fsockopen($host, $port, $errno, $errstr, $timeout);
if ($socket) {
fclose($socket);
return false; // 端口已被占用
}
return true; // 端口可用
}
}

@ -0,0 +1,152 @@
<?php
/**
* 日志管理类
*/
class Logger {
private $logPath;
private $logLevel;
private $maxFileSize;
// 日志级别
const LEVEL_DEBUG = 'debug';
const LEVEL_INFO = 'info';
const LEVEL_WARNING = 'warning';
const LEVEL_ERROR = 'error';
// 日志级别优先级
private $levelPriority = [
self::LEVEL_DEBUG => 0,
self::LEVEL_INFO => 1,
self::LEVEL_WARNING => 2,
self::LEVEL_ERROR => 3,
];
/**
* 构造函数
* @param string $logPath 日志文件路径
* @param string $logLevel 日志级别
* @param int $maxFileSize 最大日志文件大小(字节)
*/
public function __construct($logPath, $logLevel = self::LEVEL_INFO, $maxFileSize = 10 * 1024 * 1024) {
$this->logPath = rtrim($logPath, '/') . '/';
$this->logLevel = $logLevel;
$this->maxFileSize = $maxFileSize;
// 确保日志目录存在
if (!is_dir($this->logPath)) {
mkdir($this->logPath, 0755, true);
}
}
/**
* 记录debug级别的日志
* @param string $message 日志消息
* @param array $context 上下文信息
*/
public function debug($message, $context = []) {
$this->log(self::LEVEL_DEBUG, $message, $context);
}
/**
* 记录info级别的日志
* @param string $message 日志消息
* @param array $context 上下文信息
*/
public function info($message, $context = []) {
$this->log(self::LEVEL_INFO, $message, $context);
}
/**
* 记录warning级别的日志
* @param string $message 日志消息
* @param array $context 上下文信息
*/
public function warning($message, $context = []) {
$this->log(self::LEVEL_WARNING, $message, $context);
}
/**
* 记录error级别的日志
* @param string $message 日志消息
* @param array $context 上下文信息
*/
public function error($message, $context = []) {
$this->log(self::LEVEL_ERROR, $message, $context);
}
/**
* 记录日志
* @param string $level 日志级别
* @param string $message 日志消息
* @param array $context 上下文信息
*/
private function log($level, $message, $context = []) {
// 检查日志级别是否需要记录
if ($this->levelPriority[$level] < $this->levelPriority[$this->logLevel]) {
return;
}
// 格式化日志消息
$formattedMessage = $this->formatMessage($level, $message, $context);
// 确定日志文件名
$logFile = $this->logPath . date('Y-m-d') . '.log';
// 检查日志文件大小,如果超过最大值则滚动
$this->rotateLog($logFile);
// 写入日志
file_put_contents($logFile, $formattedMessage, FILE_APPEND | LOCK_EX);
}
/**
* 格式化日志消息
* @param string $level 日志级别
* @param string $message 日志消息
* @param array $context 上下文信息
* @return string 格式化后的日志消息
*/
private function formatMessage($level, $message, $context = []) {
$timestamp = date('Y-m-d H:i:s');
$pid = getmypid();
$ip = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
// 替换上下文中的占位符
if (!empty($context)) {
foreach ($context as $key => $value) {
$message = str_replace('{' . $key . '}', (string)$value, $message);
}
}
return sprintf("[%s] [%s] [PID: %d] [IP: %s] %s\n", $timestamp, strtoupper($level), $pid, $ip, $message);
}
/**
* 滚动日志文件
* @param string $logFile 日志文件名
*/
private function rotateLog($logFile) {
if (file_exists($logFile) && filesize($logFile) > $this->maxFileSize) {
$backupFile = $logFile . '.' . date('His');
rename($logFile, $backupFile);
}
}
/**
* 设置日志级别
* @param string $level 日志级别
*/
public function setLevel($level) {
if (isset($this->levelPriority[$level])) {
$this->logLevel = $level;
}
}
/**
* 获取当前日志级别
* @return string 当前日志级别
*/
public function getLevel() {
return $this->logLevel;
}
}

@ -0,0 +1,228 @@
# Windows电脑上的邮件服务器部署方案
## 1. 环境准备
### 1.1 系统要求
- Windows 10/11 64位操作系统
- 至少4GB内存
- 至少20GB可用磁盘空间
### 1.2 软件安装
1. **PHP环境**
- 下载PHP 8.0+ Windows版非线程安全版本
- 解压到指定目录,如 `E:\php`
- 配置环境变量将PHP目录添加到系统PATH中
2. **MySQL数据库**(可选,项目已配置远程数据库):
- 若使用本地MySQL下载并安装MySQL Community Server
- 配置MySQL服务启动
- 创建数据库和用户
## 2. 项目部署
### 2.1 项目文件准备
1. 将项目文件复制到Windows电脑建议放在非系统盘`D:\project`
2. PHP配置文件调整
- 复制 `php.ini-development``php.ini`
- 修改 `php.ini` 配置:
```ini
extension_dir = "ext"
extension=pdo_mysql
extension=mbstring
extension=openssl
extension=sockets
```
### 2.2 数据库配置
1. 编辑 `backend\config\config.php`,确保数据库连接信息正确:
```php
return [
'database' => [
'driver' => 'mysql',
'host' => 'dbconn.sealoshzh.site',
'port' => 40678,
'database' => 'smtp',
'username' => 'root',
'password' => 'nv7cr6db',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
],
// 其他配置...
];
```
## 3. 部署方案
### 3.1 方案一:只允许本地访问
**适用场景**:仅开发测试使用,不对外提供服务
#### 3.1.1 启动服务器
```bash
# 使用PHP内置服务器仅本地访问
php -S localhost:8000 -c php.ini -t backend\admin\
```
#### 3.1.2 访问方式
- 本地访问:`http://localhost:8000`
- API基础URL`http://localhost:8000/api/`
#### 3.1.3 特点
- 简单快捷,适合开发测试
- 安全性高,仅本地可访问
- 无需配置防火墙
### 3.2 方案二:允许其他电脑访问
**适用场景**:需要对外提供服务,或多端协作开发
#### 3.2.1 启动服务器
```bash
# 使用PHP内置服务器监听所有网卡
php -S 0.0.0.0:8000 -c php.ini -t backend\admin\
```
#### 3.2.2 防火墙配置
1. 打开"Windows Defender 防火墙"
2. 点击"高级设置"
3. 选择"入站规则" > "新建规则"
4. 选择"端口" > "TCP" > "特定本地端口",输入"8000"
5. 允许连接
6. 选择应用场景(域、专用、公用,建议至少勾选"专用"
7. 命名规则(如"PHP Server Port 8000"
#### 3.2.3 获取服务器IP地址
在命令提示符中输入:
```bash
ipconfig
```
查找"IPv4 地址",如:`192.168.1.100`
#### 3.2.4 外部访问方式
- 其他电脑访问地址:`http://192.168.1.100:8000`
- API基础URL`http://192.168.1.100:8000/api/`
#### 3.2.5 跨域配置(可选)
若前端部署在不同域名需要在API中添加CORS头
1. 编辑API文件在响应头中添加
```php
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
```
## 4. 测试验证
### 4.1 本地访问测试
1. 启动服务器后,打开浏览器访问 `http://localhost:8000`
2. 使用API测试工具如Postman测试API
```
GET http://localhost:8000/api/users.php?page=1&perPage=10
```
### 4.2 外部访问测试
1. 在另一台电脑上使用浏览器访问服务器IP`http://192.168.1.100:8000`
2. 使用API测试工具测试API
```
GET http://192.168.1.100:8000/api/users.php?page=1&perPage=10
```
## 5. 常见问题处理
### 5.1 端口被占用
- 更换其他端口如8080
```bash
php -S 0.0.0.0:8080 -c php.ini -t backend\admin\
```
### 5.2 PHP扩展缺失
- 检查 `php.ini` 中是否已启用所需扩展
- 确认扩展文件存在于 `ext` 目录中
### 5.3 数据库连接失败
- 检查数据库连接配置是否正确
- 确认数据库服务正在运行
- 验证网络连接是否正常
## 6. 维护建议
### 6.1 日志管理
- 日志文件位于 `backend\logs\`
- 定期清理旧日志文件,避免占用过多磁盘空间
### 6.2 安全措施
- 生产环境建议使用Nginx/Apache替代PHP内置服务器
- 配置HTTPS保护数据传输安全
- 定期更新PHP和MySQL版本
- 限制数据库用户权限避免使用root用户直接连接
### 6.3 性能优化
- 启用PHP OPcache加速
- 配置合适的内存限制
- 优化数据库查询语句
- 考虑使用缓存机制
## 7. 自动化脚本
### 7.1 启动脚本start_server.bat
```batch
@echo off
echo 正在启动邮件服务器...
php -S 0.0.0.0:8000 -c php.ini -t admin\
pause
```
### 7.2 停止脚本stop_server.bat
```batch
@echo off
echo 正在停止邮件服务器...
taskkill /F /IM php.exe /T
pause
```
## 8. 部署流程图
```
┌─────────────────────────────────────────────────┐
│ 环境准备安装PHP、配置环境变量 │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ 项目文件准备复制项目、配置php.ini │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ 数据库配置修改config.php中的数据库连接信息 │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ 选择部署模式: │
│ 1. 本地访问php -S localhost:8000 -c php.ini │
│ 2. 外部访问php -S 0.0.0.0:8000 -c php.ini │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ 外部访问配置配置防火墙规则允许8000端口 │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ 测试验证:本地和外部访问测试 │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ 部署完成,开始使用邮件服务器 │
└─────────────────────────────────────────────────┘
```
## 9. 总结
本部署方案提供了两种部署模式,可根据实际需求选择:
- 本地访问模式:适合开发测试,简单快捷
- 外部访问模式:适合多端协作,需要配置防火墙
部署过程中需注意:
- 正确配置PHP环境和扩展
- 确保数据库连接信息正确
- 外部访问时注意防火墙配置
- 生产环境建议使用更稳定的Web服务器
按照本方案部署后邮件服务器即可在Windows电脑上正常运行支持本地或外部访问。

@ -0,0 +1,73 @@
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }
require_once __DIR__ . '/../../utils/Database.php';
require_once __DIR__ . '/../../utils/Helper.php';
$db = Database::getInstance();
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
$username = isset($_GET['username']) ? trim($_GET['username']) : '';
$id = isset($_GET['id']) ? trim($_GET['id']) : '';
if ($id !== '') {
$email = $db->fetchOne("SELECT * FROM email WHERE id = ? AND is_deleted = 0", [$id]);
echo json_encode(['success' => true, 'data' => ['email' => $email]]);
exit;
}
if ($username === '') { http_response_code(400); echo json_encode(['success' => false, 'message' => 'invalid']); exit; }
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$perPage = isset($_GET['perPage']) ? min(100, max(1, (int)$_GET['perPage'])) : 10;
$offset = ($page - 1) * $perPage;
$totalRow = $db->fetchOne("SELECT COUNT(*) AS cnt FROM email WHERE rcpt_to = ? AND is_deleted = 0", [$username]);
$emails = $db->fetchAll("SELECT id, `from`, `to`, subject, `date`, folder, is_read, is_deleted, created_at FROM email WHERE rcpt_to = ? AND is_deleted = 0 ORDER BY `date` DESC LIMIT $perPage OFFSET $offset", [$username]);
echo json_encode(['success' => true, 'data' => ['emails' => $emails, 'total' => (int)$totalRow['cnt'], 'page' => $page, 'perPage' => $perPage, 'totalPages' => (int)ceil(((int)$totalRow['cnt']) / $perPage)]]);
exit;
}
if ($method === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
$username = trim($input['username'] ?? '');
$to = trim($input['to'] ?? '');
$subject = trim($input['subject'] ?? '');
$content = $input['content'] ?? '';
$isDraft = isset($input['isDraft']) ? (bool)$input['isDraft'] : false;
if ($username === '' || $to === '' || !Helper::validateEmail($to)) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'invalid']); exit; }
$user = $db->fetchOne("SELECT email FROM user WHERE username = ? AND is_deleted = 0", [$username]);
$fromEmail = $user ? $user['email'] : ($username . '@test.com');
$id = Helper::generateId();
$folder = $isDraft ? 'draft' : 'sent';
$dateNow = date('Y-m-d H:i:s');
$raw = "From: {$fromEmail}\r\nTo: {$to}\r\nSubject: {$subject}\r\nDate: {$dateNow}\r\n\r\n{$content}\r\n";
$db->insert("INSERT INTO email (id, ip, helo, mail_from, rcpt_to, `from`, `to`, subject, `date`, `data`, datagram, `length`, `state`, is_read, folder, created_at, is_deleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), 0)", [$id, Helper::getClientIp(), 'frontend', $fromEmail, $username, $fromEmail, $to, $subject, $dateNow, $content, $raw, strlen($raw), 1, $isDraft ? 0 : 1, $folder]);
echo json_encode(['success' => true, 'message' => ($isDraft ? 'draft saved' : 'sent'), 'data' => ['emailId' => $id]]);
exit;
}
if ($method === 'PUT') {
$input = json_decode(file_get_contents('php://input'), true);
$id = trim($input['id'] ?? '');
$username = trim($input['username'] ?? '');
$isRead = isset($input['isRead']) ? (int)$input['isRead'] : null;
$folder = isset($input['folder']) ? trim($input['folder']) : null;
if ($id === '' || $username === '') { http_response_code(400); echo json_encode(['success' => false, 'message' => 'invalid']); exit; }
$fields = [];
$params = [];
if ($isRead !== null) { $fields[] = "is_read = ?"; $params[] = $isRead; }
if ($folder !== null) { $fields[] = "folder = ?"; $params[] = $folder; }
if (empty($fields)) { echo json_encode(['success' => true, 'message' => 'no changes']); exit; }
$params[] = $id;
$params[] = $username;
$db->update("UPDATE email SET " . implode(', ', $fields) . " WHERE id = ? AND rcpt_to = ?", $params);
echo json_encode(['success' => true, 'message' => 'updated']);
exit;
}
if ($method === 'DELETE') {
$id = isset($_GET['id']) ? trim($_GET['id']) : '';
$username = isset($_GET['username']) ? trim($_GET['username']) : '';
if ($id === '' || $username === '') { http_response_code(400); echo json_encode(['success' => false, 'message' => 'invalid']); exit; }
$db->update("UPDATE email SET is_deleted = 1, folder = 'trash' WHERE id = ? AND rcpt_to = ?", [$id, $username]);
echo json_encode(['success' => true, 'message' => 'deleted']);
exit;
}
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'method not allowed']);

@ -0,0 +1,19 @@
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }
require_once __DIR__ . '/../../utils/Database.php';
require_once __DIR__ . '/../../utils/Helper.php';
$db = Database::getInstance();
$username = isset($_GET['username']) ? trim($_GET['username']) : '';
if ($username === '') { http_response_code(400); echo json_encode(['success' => false, 'message' => 'invalid']); exit; }
$emailRow = $db->fetchOne("SELECT email FROM user WHERE username = ? AND is_deleted = 0", [$username]);
$emailAddr = $emailRow ? $emailRow['email'] : ($username . '@test.com');
$inbox = $db->fetchOne("SELECT COUNT(*) AS cnt FROM email WHERE rcpt_to = ? AND folder = 'inbox' AND is_deleted = 0", [$username]);
$sent = $db->fetchOne("SELECT COUNT(*) AS cnt FROM email WHERE rcpt_to = ? AND folder = 'sent' AND is_deleted = 0", [$username]);
$draft = $db->fetchOne("SELECT COUNT(*) AS cnt FROM email WHERE rcpt_to = ? AND folder = 'draft' AND is_deleted = 0", [$username]);
$trash = $db->fetchOne("SELECT COUNT(*) AS cnt FROM email WHERE rcpt_to = ? AND folder = 'trash' AND is_deleted = 0", [$username]);
$unread = $db->fetchOne("SELECT COUNT(*) AS cnt FROM email WHERE rcpt_to = ? AND is_read = 0 AND is_deleted = 0", [$username]);
echo json_encode(['success' => true, 'data' => ['email' => $emailAddr, 'stats' => ['inbox' => (int)$inbox['cnt'], 'sent' => (int)$sent['cnt'], 'draft' => (int)$draft['cnt'], 'trash' => (int)$trash['cnt'], 'unread' => (int)$unread['cnt']], 'customFolders' => []]]);

@ -0,0 +1,25 @@
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }
require_once __DIR__ . '/../../utils/Config.php';
$config = Config::getInstance();
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
echo json_encode(['success' => true, 'data' => $config->getAll()]);
exit;
}
if ($method === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
if (isset($input['smtp'])) { $config->set('smtp', $input['smtp']); }
if (isset($input['pop3'])) { $config->set('pop3', $input['pop3']); }
if (isset($input['server'])) { $config->set('server', $input['server']); }
if (isset($input['log'])) { $config->set('log', $input['log']); }
$ok = $config->save();
echo json_encode(['success' => $ok ? true : false, 'message' => $ok ? 'saved' : 'failed']);
exit;
}
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'method not allowed']);

@ -0,0 +1,82 @@
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }
require_once __DIR__ . '/../../utils/Database.php';
require_once __DIR__ . '/../../utils/Helper.php';
$db = Database::getInstance();
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
$perPage = isset($_GET['perPage']) ? min(100, max(1, (int)$_GET['perPage'])) : 10;
$offset = ($page - 1) * $perPage;
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
$params = [];
$where = "WHERE is_deleted = 0";
if ($search !== '') {
$where .= " AND (username LIKE ? OR email LIKE ?)";
$params[] = '%' . $search . '%';
$params[] = '%' . $search . '%';
}
$totalRow = $db->fetchOne("SELECT COUNT(*) AS cnt FROM user $where", $params);
$users = $db->fetchAll("SELECT username,email,phone,level,avatar,is_admin,is_enabled,create_time,updated_at FROM user $where ORDER BY create_time DESC LIMIT $perPage OFFSET $offset", $params);
echo json_encode(['success' => true, 'data' => ['users' => $users, 'total' => (int)$totalRow['cnt'], 'page' => $page, 'perPage' => $perPage, 'totalPages' => (int)ceil(((int)$totalRow['cnt']) / $perPage)]]);
exit;
}
if ($method === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
if (isset($input['login']) && $input['login'] === true) {
$username = trim($input['username'] ?? '');
$password = $input['password'] ?? '';
if ($username === '' || $password === '') { http_response_code(400); echo json_encode(['success' => false, 'message' => 'invalid']); exit; }
$row = $db->fetchOne("SELECT username,password,email,is_admin,is_enabled,is_deleted FROM user WHERE username = ? LIMIT 1", [$username]);
if (!$row || (int)$row['is_deleted'] === 1 || (int)$row['is_enabled'] === 0) { http_response_code(401); echo json_encode(['success' => false, 'message' => 'unauthorized']); exit; }
if (!Helper::verifyPassword($password, $row['password'])) { http_response_code(401); echo json_encode(['success' => false, 'message' => 'unauthorized']); exit; }
$token = base64_encode(hash('sha256', $username . '|' . microtime(true) . '|' . Helper::generateRandomString(16), true));
echo json_encode(['success' => true, 'data' => ['token' => $token, 'username' => $row['username'], 'email' => $row['email'], 'is_admin' => (int)$row['is_admin']]]);
exit;
}
$username = trim($input['username'] ?? '');
$password = $input['password'] ?? '';
$email = trim($input['email'] ?? '');
if ($username === '' || $password === '' || $email === '' || !Helper::validateEmail($email)) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'invalid']); exit; }
$hash = Helper::encryptPassword($password);
$db->insert("INSERT INTO user (username,password,email,level,is_admin,is_enabled,is_deleted,create_time,updated_at) VALUES (?, ?, ?, 1, 0, 1, 0, NOW(), NOW())", [$username, $hash, $email]);
echo json_encode(['success' => true, 'message' => 'created']);
exit;
}
if ($method === 'PUT') {
$input = json_decode(file_get_contents('php://input'), true);
$username = trim($input['username'] ?? '');
if ($username === '') { http_response_code(400); echo json_encode(['success' => false, 'message' => 'invalid']); exit; }
$email = isset($input['email']) ? trim($input['email']) : null;
$phone = isset($input['phone']) ? trim($input['phone']) : null;
$level = isset($input['level']) ? (int)$input['level'] : null;
$is_admin = isset($input['is_admin']) ? (int)$input['is_admin'] : null;
$is_enabled = isset($input['is_enabled']) ? (int)$input['is_enabled'] : null;
$fields = [];
$params = [];
if ($email !== null) { $fields[] = "email = ?"; $params[] = $email; }
if ($phone !== null) { $fields[] = "phone = ?"; $params[] = $phone; }
if ($level !== null) { $fields[] = "level = ?"; $params[] = $level; }
if ($is_admin !== null) { $fields[] = "is_admin = ?"; $params[] = $is_admin; }
if ($is_enabled !== null) { $fields[] = "is_enabled = ?"; $params[] = $is_enabled; }
if (isset($input['password']) && $input['password'] !== '') { $fields[] = "password = ?"; $params[] = Helper::encryptPassword($input['password']); }
if (empty($fields)) { echo json_encode(['success' => true, 'message' => 'no changes']); exit; }
$fields[] = "updated_at = NOW()";
$params[] = $username;
$db->update("UPDATE user SET " . implode(', ', $fields) . " WHERE username = ? AND is_deleted = 0", $params);
echo json_encode(['success' => true, 'message' => 'updated']);
exit;
}
if ($method === 'DELETE') {
$username = isset($_GET['username']) ? trim($_GET['username']) : '';
if ($username === '') { http_response_code(400); echo json_encode(['success' => false, 'message' => 'invalid']); exit; }
$db->update("UPDATE user SET is_deleted = 1, is_enabled = 0, updated_at = NOW() WHERE username = ? AND is_deleted = 0", [$username]);
echo json_encode(['success' => true, 'message' => 'deleted']);
exit;
}
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'method not allowed']);

@ -0,0 +1,40 @@
<?php
return [
'database' => [
'driver' => 'mysql',
'host' => 'dbconn.sealoshzh.site',
'port' => 33979,
'database' => 'smtp',
'username' => 'root',
'password' => 'nv7cr6db',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => true,
'max_connections' => 10,
'min_connections' => 3
],
'smtp' => [
'port' => 25,
'host' => '0.0.0.0',
'max_connections' => 100
],
'pop3' => [
'port' => 110,
'host' => '0.0.0.0',
'max_connections' => 100
],
'server' => [
'domain' => 'test.com',
'admin_password' => 'admin123',
'max_email_size' => 10485760
],
'log' => [
'path' => '../logs/',
'level' => 'info',
'max_file_size' => 10485760
],
'mailbox' => [
'max_size' => 104857600
]
];

@ -0,0 +1,170 @@
<?php
require_once __DIR__ . '/../utils/Config.php';
class Pop3Handler {
private $socket;
private $clientIp;
private $logger;
private $config;
private $state = 'auth';
private $username = '';
private $password = '';
private $authenticated = false;
private $messages = [];
private $deletedMessages = [];
private $messageCount = 0;
private $mailboxSize = 0;
public function __construct($socket, $clientIp, $logger) {
$this->socket = $socket;
$this->clientIp = $clientIp;
$this->logger = $logger;
$this->config = Config::getInstance();
}
public function handle($data) {
$lines = explode("\r\n", $data);
foreach ($lines as $line) {
$line = trim($line);
if ($line === '') { continue; }
$this->processCommand($line);
}
}
private function processCommand($line) {
$parts = preg_split('/\\s+/', $line, 2);
$cmd = strtoupper($parts[0]);
$arg = $parts[1] ?? '';
switch ($cmd) {
case 'USER':
$this->username = $arg;
$this->send("+OK");
break;
case 'PASS':
$this->password = $arg;
if ($this->authenticate()) {
$this->authenticated = true;
$this->state = 'transaction';
$this->initMessages($this->username);
$this->send("+OK {$this->messageCount} {$this->mailboxSize}");
} else {
$this->send("-ERR");
}
break;
case 'STAT':
if (!$this->authenticated) { $this->send("-ERR"); break; }
$this->send("+OK {$this->messageCount} {$this->mailboxSize}");
break;
case 'LIST':
if (!$this->authenticated) { $this->send("-ERR"); break; }
if (!empty($arg)) {
$id = (int)$arg;
if (isset($this->messages[$id])) { $this->send("+OK {$id} {$this->messages[$id]['size']}"); }
else { $this->send("-ERR"); }
} else {
$this->send("+OK {$this->messageCount} messages");
foreach ($this->messages as $id => $msg) { $this->send("{$id} {$msg['size']}"); }
$this->send(".");
}
break;
case 'UIDL':
if (!$this->authenticated) { $this->send("-ERR"); break; }
if (!empty($arg)) {
$id = (int)$arg;
if (isset($this->messages[$id])) { $this->send("+OK {$id} {$this->messages[$id]['uid']}"); }
else { $this->send("-ERR"); }
} else {
$this->send("+OK");
foreach ($this->messages as $id => $msg) { $this->send("{$id} {$msg['uid']}"); }
$this->send(".");
}
break;
case 'RETR':
if (!$this->authenticated) { $this->send("-ERR"); break; }
$id = (int)$arg;
if (isset($this->messages[$id])) {
$this->send("+OK {$this->messages[$id]['size']} octets");
$this->sendRaw($this->messages[$id]['content']);
$this->send(".");
} else { $this->send("-ERR"); }
break;
case 'DELE':
if (!$this->authenticated) { $this->send("-ERR"); break; }
$id = (int)$arg;
if (isset($this->messages[$id])) { $this->deletedMessages[$id] = true; $this->send("+OK"); }
else { $this->send("-ERR"); }
break;
case 'NOOP':
$this->send("+OK");
break;
case 'RSET':
$this->deletedMessages = [];
$this->send("+OK");
break;
case 'QUIT':
if ($this->authenticated) { $this->applyDeletes(); }
$this->send("+OK bye");
break;
default:
$this->send("-ERR");
break;
}
}
private function authenticate() {
try {
require_once __DIR__ . '/../utils/Database.php';
$db = Database::getInstance();
$row = $db->fetchOne("SELECT password FROM user WHERE username = ? AND is_deleted = 0", [$this->username]);
if (!$row) { return false; }
return password_verify($this->password, $row['password']);
} catch (Exception $e) {
return false;
}
}
private function initMessages($username) {
try {
require_once __DIR__ . '/../utils/Database.php';
$db = Database::getInstance();
$emails = $db->fetchAll("SELECT * FROM email WHERE rcpt_to = ? AND folder = 'inbox' AND is_deleted = 0 ORDER BY `date` ASC", [$username]);
$this->messages = [];
$this->messageCount = 0;
$this->mailboxSize = 0;
$this->deletedMessages = [];
if ($emails) {
$id = 1;
foreach ($emails as $email) {
$content = "From: {$email['from']}\r\n" .
"To: {$email['to']}\r\n" .
"Subject: {$email['subject']}\r\n" .
"Date: {$email['date']}\r\n\r\n" .
"{$email['data']}\r\n";
$size = strlen($content);
$this->messages[$id] = [
'uid' => $email['id'],
'size' => $size,
'content' => $content,
'email_id' => $email['id']
];
$this->mailboxSize += $size;
$id++;
}
}
$this->messageCount = count($this->messages);
} catch (Exception $e) {
$this->messages = [];
$this->messageCount = 0;
$this->mailboxSize = 0;
$this->deletedMessages = [];
}
}
private function applyDeletes() {
if (empty($this->deletedMessages)) { return; }
try {
require_once __DIR__ . '/../utils/Database.php';
$db = Database::getInstance();
foreach ($this->deletedMessages as $id => $_) {
if (isset($this->messages[$id])) {
$db->update("UPDATE email SET is_deleted = 1, folder = 'trash' WHERE id = ?", [$this->messages[$id]['email_id']]);
}
}
} catch (Exception $e) {}
}
private function send($line) { $out = $line . "\r\n"; socket_write($this->socket, $out, strlen($out)); }
private function sendRaw($raw) { socket_write($this->socket, $raw, strlen($raw)); }
}

@ -0,0 +1,108 @@
<?php
require_once __DIR__ . '/../utils/Config.php';
require_once __DIR__ . '/../utils/Logger.php';
require_once __DIR__ . '/Pop3Handler.php';
class Pop3Server {
private $host;
private $port;
private $maxConnections;
private $socket;
private $connections = [];
private $logger;
private $config;
public function __construct($host = '0.0.0.0', $port = 110, $maxConnections = 100) {
$this->config = Config::getInstance();
$this->host = $host;
$this->port = $port;
$this->maxConnections = $maxConnections;
$logPath = $this->config->get('log.path', '../logs/');
$logLevel = $this->config->get('log.level', 'info');
$this->logger = new Logger($logPath, $logLevel);
}
public function start() {
$this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($this->socket === false) { $this->logger->error("Failed to create socket: " . socket_strerror(socket_last_error())); return false; }
socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, 1);
if (!socket_bind($this->socket, $this->host, $this->port)) { $this->logger->error("Failed to bind socket: " . socket_strerror(socket_last_error($this->socket))); socket_close($this->socket); return false; }
$backlog = $this->config->get('pop3.backlog', 50);
if (!socket_listen($this->socket, $backlog)) { $this->logger->error("Failed to listen on socket: " . socket_strerror(socket_last_error($this->socket))); socket_close($this->socket); return false; }
$this->logger->info("POP3 server started on {host}:{port}", ['host' => $this->host, 'port' => $this->port]);
while (true) { $this->handleConnections(); }
return true;
}
public function stop() {
foreach ($this->connections as $connection) { socket_close($connection['socket']); }
if ($this->socket) { socket_close($this->socket); }
$this->logger->info("POP3 server stopped");
}
private function handleConnections() {
$readSockets = array_column($this->connections, 'socket');
$readSockets[] = $this->socket;
$writeSockets = null;
$exceptSockets = null;
$activity = socket_select($readSockets, $writeSockets, $exceptSockets, 1);
if ($activity === false) { $this->logger->error("Socket select error: " . socket_strerror(socket_last_error())); return; }
foreach ($readSockets as $socket) {
if ($socket === $this->socket) { $this->acceptNewConnection(); } else { $this->handleClientRequest($socket); }
}
$this->cleanupTimeoutConnections();
}
private function acceptNewConnection() {
if (count($this->connections) >= $this->maxConnections) { $this->logger->warning("Maximum connections reached: {max}", ['max' => $this->maxConnections]); return; }
$clientSocket = socket_accept($this->socket);
if ($clientSocket === false) { $this->logger->error("Failed to accept connection: " . socket_strerror(socket_last_error($this->socket))); return; }
socket_getpeername($clientSocket, $clientIp, $clientPort);
$this->logger->info("New connection from {ip}:{port}", ['ip' => $clientIp, 'port' => $clientPort]);
$connectionId = uniqid();
$socketKey = (int)$clientSocket;
$this->connections[$connectionId] = [
'socket' => $clientSocket,
'ip' => $clientIp,
'port' => $clientPort,
'handler' => new Pop3Handler($clientSocket, $clientIp, $this->logger),
'lastActivity' => time()
];
static $socketToConnection = [];
$socketToConnection[$socketKey] = $connectionId;
$welcomeMsg = "+OK POP3 Service Ready\r\n";
socket_write($clientSocket, $welcomeMsg, strlen($welcomeMsg));
}
private function handleClientRequest($socket) {
$socketKey = (int)$socket;
static $socketToConnection = [];
if (empty($socketToConnection) && !empty($this->connections)) {
foreach ($this->connections as $id => $connection) { $socketToConnection[(int)$connection['socket']] = $id; }
}
if (!isset($socketToConnection[$socketKey])) {
$socketToConnection = [];
foreach ($this->connections as $id => $connection) { $socketToConnection[(int)$connection['socket']] = $id; }
if (!isset($socketToConnection[$socketKey])) { return; }
}
$connectionId = $socketToConnection[$socketKey];
$data = socket_read($socket, 1024);
if ($data === false) { $this->logger->error("Failed to read from socket: " . socket_strerror(socket_last_error($socket))); $this->closeConnection($connectionId); return; }
if (empty($data)) { $this->logger->info("Connection closed by client: {ip}:{port}", ['ip' => $this->connections[$connectionId]['ip'], 'port' => $this->connections[$connectionId]['port']]); $this->closeConnection($connectionId); return; }
$this->connections[$connectionId]['lastActivity'] = time();
$this->connections[$connectionId]['handler']->handle($data);
}
private function closeConnection($connectionId) {
if (isset($this->connections[$connectionId])) {
$socket = $this->connections[$connectionId]['socket'];
$socketKey = (int)$socket;
static $socketToConnection = [];
if (isset($socketToConnection[$socketKey])) { unset($socketToConnection[$socketKey]); }
socket_close($socket);
unset($this->connections[$connectionId]);
}
}
private function cleanupTimeoutConnections() {
$timeout = 300;
$now = time();
foreach ($this->connections as $id => $connection) {
if ($now - $connection['lastActivity'] > $timeout) {
$this->logger->info("Connection timed out: {ip}:{port}", ['ip' => $connection['ip'], 'port' => $connection['port']]);
$this->closeConnection($id);
}
}
}
}

@ -0,0 +1,14 @@
<?php
require_once __DIR__ . '/Pop3Server.php';
require_once __DIR__ . '/../utils/Config.php';
require_once __DIR__ . '/../utils/Logger.php';
$options = getopt("h:p:m:", ["host:", "port:", "max-connections:", "help"]);
if (isset($options['help'])) { echo "POP3 Server Usage\n"; exit(0); }
$config = Config::getInstance();
$host = $options['h'] ?? $options['host'] ?? $config->get('pop3.host', '0.0.0.0');
$port = (int)($options['p'] ?? $options['port'] ?? $config->get('pop3.port', 110));
$maxConnections = (int)($options['m'] ?? $options['max-connections'] ?? $config->get('pop3.max_connections', 100));
if ($port < 1 || $port > 65535) { echo "Invalid port\n"; exit(1); }
if ($maxConnections < 1 || $maxConnections > 1000) { echo "Invalid max connections\n"; exit(1); }
$server = new Pop3Server($host, $port, $maxConnections);
$server->start();

@ -0,0 +1,171 @@
<?php
require_once __DIR__ . '/../utils/Config.php';
class SmtpHandler {
private $socket;
private $clientIp;
private $logger;
private $config;
private $state = 'init';
private $heloHost = '';
private $fromAddress = '';
private $toAddresses = [];
private $dataBuffer = '';
private $dataSize = 0;
private $maxDataSize = 10485760;
public function __construct($socket, $clientIp, $logger) {
$this->socket = $socket;
$this->clientIp = $clientIp;
$this->logger = $logger;
$this->config = Config::getInstance();
$this->maxDataSize = $this->config->get('server.max_email_size', 10485760);
}
public function handle($data) {
if ($this->state === 'data') { $this->handleDataContent($data); }
else {
$lines = explode("\r\n", $data);
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) { continue; }
$this->processCommand($line);
}
}
}
private function processCommand($command) {
$parts = preg_split('/\\s+/', $command, 2);
$cmd = strtoupper($parts[0]);
$params = isset($parts[1]) ? $parts[1] : '';
switch ($cmd) {
case 'HELO':
case 'EHLO':
$this->heloHost = $params;
$this->state = 'helo';
$this->sendResponse(250, "250 {domain} Hello {host} [{ip}]");
break;
case 'MAIL':
if ($this->state !== 'helo') { $this->sendResponse(503, "503 Bad sequence of commands"); return; }
preg_match('/^FROM:\\s*<([^>]*)>/i', $params, $matches);
if (empty($matches)) { $this->sendResponse(501, "501 Syntax error in parameters or arguments"); return; }
$sender = $matches[1];
if (!filter_var($sender, FILTER_VALIDATE_EMAIL)) { $this->sendResponse(553, "553 5.1.8 Invalid sender email address"); return; }
$this->fromAddress = $sender;
$this->state = 'mail';
$this->toAddresses = [];
$this->sendResponse(250, "250 2.1.0 Sender OK");
break;
case 'RCPT':
if ($this->state !== 'mail') { $this->sendResponse(503, "503 Bad sequence of commands"); return; }
preg_match('/^TO:\\s*<([^>]*)>/i', $params, $matches);
if (empty($matches)) { $this->sendResponse(501, "501 Syntax error in parameters or arguments"); return; }
$toAddress = $matches[1];
if (!filter_var($toAddress, FILTER_VALIDATE_EMAIL)) { $this->sendResponse(553, "553 5.1.8 Invalid recipient email address"); return; }
if (count($this->toAddresses) >= 100) { $this->sendResponse(452, "452 4.5.3 Too many recipients"); return; }
$this->toAddresses[] = $toAddress;
$this->sendResponse(250, "250 2.1.5 Recipient OK");
break;
case 'DATA':
if ($this->state !== 'mail' || empty($this->toAddresses)) { $this->sendResponse(503, "503 Bad sequence of commands"); return; }
$this->state = 'data';
$this->dataBuffer = '';
$this->dataSize = 0;
$this->sendResponse(354, "354 Start mail input; end with <CRLF>.<CRLF>");
break;
case 'RSET':
$this->state = 'helo';
$this->fromAddress = '';
$this->toAddresses = [];
$this->dataBuffer = '';
$this->dataSize = 0;
$this->sendResponse(250, "250 2.0.0 OK");
break;
case 'NOOP':
$this->sendResponse(250, "250 2.0.0 OK");
break;
case 'QUIT':
$this->sendResponse(221, "221 2.0.0 Bye");
break;
default:
$this->sendResponse(500, "500 Syntax error, command unrecognized");
break;
}
}
private function handleDataContent($data) {
$this->dataBuffer .= $data;
$this->dataSize += strlen($data);
if ($this->dataSize > $this->maxDataSize) {
$this->sendResponse(552, "552 5.2.3 Message exceeds maximum size");
$this->state = 'helo';
$this->dataBuffer = '';
$this->dataSize = 0;
return;
}
if (substr($this->dataBuffer, -5) === "\r\n.\r\n") {
$emailContent = substr($this->dataBuffer, 0, -5);
$this->processEmail($emailContent);
$this->state = 'helo';
$this->dataBuffer = '';
$this->dataSize = 0;
}
}
private function processEmail($emailContent) {
$headers = $this->parseHeaders($emailContent);
$subject = $headers['Subject'] ?? '';
$body = $this->extractBody($emailContent);
$this->saveEmail($headers, $subject, $body, $emailContent);
$this->sendResponse(250, "250 2.0.0 OK: queued as {message_id}");
}
private function parseHeaders($emailContent) {
$headers = [];
$lines = explode("\r\n", $emailContent);
foreach ($lines as $line) {
if (empty($line)) { break; }
if (preg_match('/^\\s+/', $line)) {
$last = array_key_last($headers);
$headers[$last] = isset($headers[$last]) ? $headers[$last] . ' ' . trim($line) : trim($line);
} else {
$parts = explode(':', $line, 2);
if (count($parts) === 2) {
$name = trim($parts[0]);
$value = trim($parts[1]);
$headers[$name] = $value;
}
}
}
return $headers;
}
private function extractBody($emailContent) {
$parts = explode("\r\n\r\n", $emailContent, 2);
return isset($parts[1]) ? $parts[1] : '';
}
private function saveEmail($headers, $subject, $body, $raw) {
try {
require_once __DIR__ . '/../utils/Database.php';
$db = Database::getInstance();
foreach ($this->toAddresses as $toAddress) {
$username = null;
$user = $db->fetchOne("SELECT username FROM user WHERE email = ? AND is_deleted = 0", [$toAddress]);
if ($user && isset($user['username'])) { $username = $user['username']; }
if (!$username) {
$parts = explode('@', $toAddress);
$username = $parts[0] ?? $toAddress;
}
$id = uniqid('', true);
$dateHeader = $headers['Date'] ?? date('Y-m-d H:i:s');
$toHeader = $headers['To'] ?? implode(', ', $this->toAddresses);
$fromHeader = $headers['From'] ?? $this->fromAddress;
$length = strlen($raw);
$sql = "INSERT INTO email (id, ip, helo, mail_from, rcpt_to, `from`, `to`, subject, `date`, `data`, datagram, `length`, `state`, is_read, folder, created_at, is_deleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), 0)";
$db->insert($sql, [$id, $this->clientIp, $this->heloHost, $this->fromAddress, $username, $fromHeader, $toHeader, $subject, $dateHeader, $body, $raw, $length, 1, 0, 'inbox']);
}
} catch (Exception $e) {
$this->logger->error("Failed to save email: {error}", ['error' => $e->getMessage()]);
}
}
private function sendResponse($code, $message) {
$domain = $this->config->get('server.domain', 'test.com');
$message = str_replace('{domain}', $domain, $message);
$message = str_replace('{ip}', $this->clientIp, $message);
$message = str_replace('{host}', $this->heloHost, $message);
$response = $message . "\r\n";
socket_write($this->socket, $response, strlen($response));
}
}

@ -0,0 +1,108 @@
<?php
require_once __DIR__ . '/../utils/Config.php';
require_once __DIR__ . '/../utils/Logger.php';
require_once __DIR__ . '/SmtpHandler.php';
class SmtpServer {
private $host;
private $port;
private $maxConnections;
private $socket;
private $connections = [];
private $logger;
private $config;
public function __construct($host = '0.0.0.0', $port = 25, $maxConnections = 100) {
$this->config = Config::getInstance();
$this->host = $host;
$this->port = $port;
$this->maxConnections = $maxConnections;
$logPath = $this->config->get('log.path', '../logs/');
$logLevel = $this->config->get('log.level', 'info');
$this->logger = new Logger($logPath, $logLevel);
}
public function start() {
$this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($this->socket === false) { $this->logger->error("Failed to create socket: " . socket_strerror(socket_last_error())); return false; }
socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, 1);
if (!socket_bind($this->socket, $this->host, $this->port)) { $this->logger->error("Failed to bind socket: " . socket_strerror(socket_last_error($this->socket))); socket_close($this->socket); return false; }
$backlog = $this->config->get('smtp.backlog', 50);
if (!socket_listen($this->socket, $backlog)) { $this->logger->error("Failed to listen on socket: " . socket_strerror(socket_last_error($this->socket))); socket_close($this->socket); return false; }
$this->logger->info("SMTP server started on {host}:{port}", ['host' => $this->host, 'port' => $this->port]);
while (true) { $this->handleConnections(); }
return true;
}
public function stop() {
foreach ($this->connections as $connection) { socket_close($connection['socket']); }
if ($this->socket) { socket_close($this->socket); }
$this->logger->info("SMTP server stopped");
}
private function handleConnections() {
$readSockets = array_column($this->connections, 'socket');
$readSockets[] = $this->socket;
$writeSockets = null;
$exceptSockets = null;
$activity = socket_select($readSockets, $writeSockets, $exceptSockets, 1);
if ($activity === false) { $this->logger->error("Socket select error: " . socket_strerror(socket_last_error())); return; }
foreach ($readSockets as $socket) {
if ($socket === $this->socket) { $this->acceptNewConnection(); } else { $this->handleClientRequest($socket); }
}
$this->cleanupTimeoutConnections();
}
private function acceptNewConnection() {
if (count($this->connections) >= $this->maxConnections) { $this->logger->warning("Maximum connections reached: {max}", ['max' => $this->maxConnections]); return; }
$clientSocket = socket_accept($this->socket);
if ($clientSocket === false) { $this->logger->error("Failed to accept connection: " . socket_strerror(socket_last_error($this->socket))); return; }
socket_getpeername($clientSocket, $clientIp, $clientPort);
$this->logger->info("New connection from {ip}:{port}", ['ip' => $clientIp, 'port' => $clientPort]);
$connectionId = uniqid();
$socketKey = (int)$clientSocket;
$this->connections[$connectionId] = [
'socket' => $clientSocket,
'ip' => $clientIp,
'port' => $clientPort,
'handler' => new SmtpHandler($clientSocket, $clientIp, $this->logger),
'lastActivity' => time()
];
static $socketToConnection = [];
$socketToConnection[$socketKey] = $connectionId;
$welcomeMsg = "220 {domain} ESMTP Service Ready\r\n";
socket_write($clientSocket, $welcomeMsg, strlen($welcomeMsg));
}
private function handleClientRequest($socket) {
$socketKey = (int)$socket;
static $socketToConnection = [];
if (empty($socketToConnection) && !empty($this->connections)) {
foreach ($this->connections as $id => $connection) { $socketToConnection[(int)$connection['socket']] = $id; }
}
if (!isset($socketToConnection[$socketKey])) {
$socketToConnection = [];
foreach ($this->connections as $id => $connection) { $socketToConnection[(int)$connection['socket']] = $id; }
if (!isset($socketToConnection[$socketKey])) { return; }
}
$connectionId = $socketToConnection[$socketKey];
$data = socket_read($socket, 1024);
if ($data === false) { $this->logger->error("Failed to read from socket: " . socket_strerror(socket_last_error($socket))); $this->closeConnection($connectionId); return; }
if (empty($data)) { $this->logger->info("Connection closed by client: {ip}:{port}", ['ip' => $this->connections[$connectionId]['ip'], 'port' => $this->connections[$connectionId]['port']]); $this->closeConnection($connectionId); return; }
$this->connections[$connectionId]['lastActivity'] = time();
$this->connections[$connectionId]['handler']->handle($data);
}
private function closeConnection($connectionId) {
if (isset($this->connections[$connectionId])) {
$socket = $this->connections[$connectionId]['socket'];
$socketKey = (int)$socket;
static $socketToConnection = [];
if (isset($socketToConnection[$socketKey])) { unset($socketToConnection[$socketKey]); }
socket_close($socket);
unset($this->connections[$connectionId]);
}
}
private function cleanupTimeoutConnections() {
$timeout = 300;
$now = time();
foreach ($this->connections as $id => $connection) {
if ($now - $connection['lastActivity'] > $timeout) {
$this->logger->info("Connection timed out: {ip}:{port}", ['ip' => $connection['ip'], 'port' => $connection['port']]);
$this->closeConnection($id);
}
}
}
}

@ -0,0 +1,14 @@
<?php
require_once __DIR__ . '/SmtpServer.php';
require_once __DIR__ . '/../utils/Config.php';
require_once __DIR__ . '/../utils/Logger.php';
$options = getopt("h:p:m:", ["host:", "port:", "max-connections:", "help"]);
if (isset($options['help'])) { echo "SMTP Server Usage\n"; exit(0); }
$config = Config::getInstance();
$host = $options['h'] ?? $options['host'] ?? $config->get('smtp.host', '0.0.0.0');
$port = (int)($options['p'] ?? $options['port'] ?? $config->get('smtp.port', 25));
$maxConnections = (int)($options['m'] ?? $options['max-connections'] ?? $config->get('smtp.max_connections', 100));
if ($port < 1 || $port > 65535) { echo "Invalid port\n"; exit(1); }
if ($maxConnections < 1 || $maxConnections > 1000) { echo "Invalid max connections\n"; exit(1); }
$server = new SmtpServer($host, $port, $maxConnections);
$server->start();

@ -0,0 +1,59 @@
<?php
class Config {
private static $instance = null;
private $config = [];
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
$configPath = __DIR__ . '/../config/config.php';
if (file_exists($configPath)) {
$this->config = include $configPath;
} else {
$this->config = [
'smtp' => ['port' => 25, 'host' => '0.0.0.0', 'max_connections' => 100],
'pop3' => ['port' => 110, 'host' => '0.0.0.0', 'max_connections' => 100],
'server' => ['domain' => 'test.com', 'admin_password' => 'admin123', 'max_email_size' => 10485760],
'log' => ['path' => '../logs/', 'level' => 'info', 'max_file_size' => 10485760],
'mailbox' => ['max_size' => 104857600]
];
}
}
public function get($key, $default = null) {
$keys = explode('.', $key);
$value = $this->config;
foreach ($keys as $k) {
if (isset($value[$k])) {
$value = $value[$k];
} else {
return $default;
}
}
return $value;
}
public function set($key, $value) {
$keys = explode('.', $key);
$config = &$this->config;
foreach ($keys as $i => $k) {
if ($i === count($keys) - 1) {
$config[$k] = $value;
} else {
if (!isset($config[$k]) || !is_array($config[$k])) {
$config[$k] = [];
}
$config = &$config[$k];
}
}
}
public function save() {
$configPath = __DIR__ . '/../config/config.php';
$content = "<?php\nreturn " . var_export($this->config, true) . ";\n";
return file_put_contents($configPath, $content) !== false;
}
public function getAll() {
return $this->config;
}
}

@ -0,0 +1,115 @@
<?php
require_once __DIR__ . '/Config.php';
class Database {
private static $instance = null;
private $config;
private $connections = [];
private $inUse = [];
private $maxConnections = 10;
private $minConnections = 3;
private function __construct() {
$this->config = Config::getInstance();
$this->maxConnections = $this->config->get('database.max_connections', 10);
$this->minConnections = $this->config->get('database.min_connections', 3);
$this->initPool();
}
public static function getInstance() {
if (self::$instance === null) { self::$instance = new self(); }
return self::$instance;
}
private function initPool() {
for ($i = 0; $i < $this->minConnections; $i++) { $this->connections[] = $this->createConnection(); }
}
private function createConnection() {
$dbConfig = $this->config->get('database');
$host = $dbConfig['host'];
$port = $dbConfig['port'];
$database = $dbConfig['database'];
$username = $dbConfig['username'];
$password = $dbConfig['password'];
$charset = $dbConfig['charset'];
$dsn = "mysql:host={$host};port={$port};charset={$charset}";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_PERSISTENT => true
];
$pdo = new PDO($dsn, $username, $password, $options);
$pdo->exec("CREATE DATABASE IF NOT EXISTS `$database` CHARACTER SET $charset COLLATE {$dbConfig['collation']}");
$pdo->exec("USE `$database`");
return $pdo;
}
private function getConnection() {
if (count($this->connections) > 0) {
$pdo = array_pop($this->connections);
if (!$this->isValid($pdo)) { $pdo = $this->createConnection(); }
$this->inUse[] = $pdo;
return $pdo;
}
if (count($this->inUse) < $this->maxConnections) {
$pdo = $this->createConnection();
$this->inUse[] = $pdo;
return $pdo;
}
usleep(1000);
return $this->getConnection();
}
private function release($pdo) {
$key = array_search($pdo, $this->inUse);
if ($key !== false) {
unset($this->inUse[$key]);
if ($this->isValid($pdo)) { $this->connections[] = $pdo; }
}
}
private function isValid($pdo) {
try { $pdo->query("SELECT 1"); return true; } catch (PDOException $e) { return false; }
}
public function fetchAll($sql, $params = []) {
$pdo = $this->getConnection();
try { $stmt = $pdo->prepare($sql); $stmt->execute($params); return $stmt->fetchAll(); }
finally { $this->release($pdo); }
}
public function fetchOne($sql, $params = []) {
$pdo = $this->getConnection();
try { $stmt = $pdo->prepare($sql); $stmt->execute($params); return $stmt->fetch(); }
finally { $this->release($pdo); }
}
public function query($sql, $params = []) {
$pdo = $this->getConnection();
try { $stmt = $pdo->prepare($sql); $stmt->execute($params); return $stmt; }
finally { $this->release($pdo); }
}
public function insert($sql, $params = []) {
$pdo = $this->getConnection();
try { $stmt = $pdo->prepare($sql); $stmt->execute($params); return $pdo->lastInsertId(); }
finally { $this->release($pdo); }
}
public function update($sql, $params = []) {
$pdo = $this->getConnection();
try { $stmt = $pdo->prepare($sql); $stmt->execute($params); return $stmt->rowCount(); }
finally { $this->release($pdo); }
}
public function delete($sql, $params = []) {
$pdo = $this->getConnection();
try { $stmt = $pdo->prepare($sql); $stmt->execute($params); return $stmt->rowCount(); }
finally { $this->release($pdo); }
}
public function beginTransaction() { $pdo = $this->getConnection(); $pdo->beginTransaction(); return $pdo; }
public function commit($pdo = null) { if ($pdo) { $pdo->commit(); $this->release($pdo); } }
public function rollback($pdo = null) { if ($pdo) { $pdo->rollBack(); $this->release($pdo); } }
public function isConnected() {
$pdo = $this->getConnection();
try { $pdo->query("SELECT 1"); return true; }
catch (PDOException $e) { return false; }
finally { $this->release($pdo); }
}
public function getPoolStatus() {
return [
'total_connections' => count($this->connections) + count($this->inUse),
'available_connections' => count($this->connections),
'in_use_connections' => count($this->inUse),
'max_connections' => $this->maxConnections
];
}
}

@ -0,0 +1,46 @@
<?php
class Helper {
public static function generateId() { return uniqid('', true); }
public static function formatDateTime($timestamp, $format = 'Y-m-d H:i:s') {
if (is_numeric($timestamp)) { return date($format, $timestamp); }
if (is_string($timestamp)) { return date($format, strtotime($timestamp)); }
return date($format);
}
public static function validateEmail($email) { return filter_var($email, FILTER_VALIDATE_EMAIL) !== false; }
public static function validateIp($ip) { return filter_var($ip, FILTER_VALIDATE_IP) !== false; }
public static function getClientIp() {
$ip = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$forwardedIps = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
$ip = trim($forwardedIps[0]);
} elseif (isset($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
}
return $ip;
}
public static function encryptPassword($password) { return password_hash($password, PASSWORD_DEFAULT); }
public static function verifyPassword($password, $hash) { return password_verify($password, $hash); }
public static function truncate($string, $length = 100, $suffix = '...') {
if (strlen($string) <= $length) { return $string; }
return substr($string, 0, $length - strlen($suffix)) . $suffix;
}
public static function generateRandomString($length = 16, $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') {
$randomString = '';
$charactersLength = strlen($characters);
for ($i = 0; $i < $length; $i++) { $randomString .= $characters[rand(0, $charactersLength - 1)]; }
return $randomString;
}
public static function formatFileSize($bytes, $precision = 2) {
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, $precision) . ' ' . $units[$pow];
}
public static function isPortAvailable($host, $port, $timeout = 1) {
$socket = @fsockopen($host, $port, $errno, $errstr, $timeout);
if ($socket) { fclose($socket); return false; }
return true;
}
}

@ -0,0 +1,51 @@
<?php
class Logger {
private $logPath;
private $logLevel;
private $maxFileSize;
const LEVEL_DEBUG = 'debug';
const LEVEL_INFO = 'info';
const LEVEL_WARNING = 'warning';
const LEVEL_ERROR = 'error';
private $levelPriority = [
self::LEVEL_DEBUG => 0,
self::LEVEL_INFO => 1,
self::LEVEL_WARNING => 2,
self::LEVEL_ERROR => 3
];
public function __construct($logPath, $logLevel = self::LEVEL_INFO, $maxFileSize = 10485760) {
$this->logPath = rtrim($logPath, '/') . '/';
$this->logLevel = $logLevel;
$this->maxFileSize = $maxFileSize;
if (!is_dir($this->logPath)) {
mkdir($this->logPath, 0755, true);
}
}
public function debug($message, $context = []) { $this->log(self::LEVEL_DEBUG, $message, $context); }
public function info($message, $context = []) { $this->log(self::LEVEL_INFO, $message, $context); }
public function warning($message, $context = []) { $this->log(self::LEVEL_WARNING, $message, $context); }
public function error($message, $context = []) { $this->log(self::LEVEL_ERROR, $message, $context); }
private function log($level, $message, $context = []) {
if ($this->levelPriority[$level] < $this->levelPriority[$this->logLevel]) { return; }
if (!empty($context)) {
foreach ($context as $key => $value) {
$message = str_replace('{' . $key . '}', (string)$value, $message);
}
}
$timestamp = date('Y-m-d H:i:s');
$pid = getmypid();
$ip = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
$formatted = sprintf("[%s] [%s] [PID: %d] [IP: %s] %s\n", $timestamp, strtoupper($level), $pid, $ip, $message);
$logFile = $this->logPath . date('Y-m-d') . '.log';
$this->rotate($logFile);
file_put_contents($logFile, $formatted, FILE_APPEND | LOCK_EX);
}
private function rotate($logFile) {
if (file_exists($logFile) && filesize($logFile) > $this->maxFileSize) {
$backupFile = $logFile . '.' . date('His');
rename($logFile, $backupFile);
}
}
public function setLevel($level) { if (isset($this->levelPriority[$level])) { $this->logLevel = $level; } }
public function getLevel() { return $this->logLevel; }
}

@ -0,0 +1,15 @@
# 前端说明
- 访问 `index.html`,在顶部设置后端地址(默认 `http://localhost:8000`
- 支持按照后端文档进行常见操作:用户、邮件、设置、日志、通讯录、统计、邮箱
- 提供 API 调试台,可直接输入路径与方法并发送
## 本地预览
- 使用任意静态服务器在 `frontend` 目录运行,例如:
- `python3 -m http.server 5173` 然后访问 `http://localhost:5173/`
- 或直接在 IDE 预览
## 跨域
- 若前端与后端不同源,请在后端增加 CORS 响应头

@ -0,0 +1,24 @@
*{box-sizing:border-box}body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Helvetica,Arial,"Apple Color Emoji","Segoe UI Emoji";color:#1f2937;background:#f8fafc}
.topbar{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:#0ea5e9;color:#fff}
.topbar .brand{font-weight:600}
.topbar .controls{display:flex;gap:8px;align-items:center}
.topbar input{height:32px;padding:4px 8px;border:1px solid #93c5fd;border-radius:6px}
.topbar button{height:32px;padding:4px 10px;border:0;border-radius:6px;background:#1e3a8a;color:#fff}
.status{margin-left:8px;font-size:12px}
.tabs{display:flex;gap:8px;padding:8px;background:#eff6ff;border-bottom:1px solid #e5e7eb}
.tabs button{padding:8px 12px;border:1px solid #c7d2fe;background:#fff;border-radius:8px;color:#1f2937}
.tabs button.active{background:#1e40af;color:#fff;border-color:#1e40af}
main{padding:16px}
.section.hidden{display:none}
.card{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:12px;margin-bottom:12px}
.grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.row{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:8px}
.form{display:grid;grid-template-columns:repeat(2,1fr);gap:8px}
textarea{width:100%;min-height:120px;padding:8px;border:1px solid #e5e7eb;border-radius:8px}
table{width:100%;border-collapse:collapse;border:1px solid #e5e7eb}
th,td{border-bottom:1px solid #e5e7eb;padding:8px;text-align:left}
button{padding:6px 10px;border-radius:8px;border:1px solid #c7d2fe;background:#eef2ff}
button.danger{background:#fee2e2;border-color:#fecaca;color:#991b1b}
.label{font-size:12px;color:#64748b;margin-bottom:4px}
pre{background:#0b1220;color:#e5e7eb;border-radius:8px;padding:8px;overflow:auto;max-height:300px}
.note{font-size:12px;color:#64748b}

@ -0,0 +1,9 @@
{
"appId": "net.educoder.mailbox",
"appName": "Mailbox",
"webDir": "dist",
"server": {
"androidScheme": "http",
"cleartext": true
}
}

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>邮件服务器管理前端</title>
<link rel="stylesheet" href="./assets/styles.css">
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

@ -0,0 +1,963 @@
{
"name": "mail-admin-frontend",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mail-admin-frontend",
"version": "0.1.0",
"devDependencies": {
"vite": "^5.4.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz",
"integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz",
"integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz",
"integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz",
"integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz",
"integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz",
"integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz",
"integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz",
"integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz",
"integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz",
"integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz",
"integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz",
"integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz",
"integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz",
"integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz",
"integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz",
"integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz",
"integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz",
"integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz",
"integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz",
"integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz",
"integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz",
"integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.0.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.2.tgz",
"integrity": "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
},
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.21.5",
"@esbuild/android-arm": "0.21.5",
"@esbuild/android-arm64": "0.21.5",
"@esbuild/android-x64": "0.21.5",
"@esbuild/darwin-arm64": "0.21.5",
"@esbuild/darwin-x64": "0.21.5",
"@esbuild/freebsd-arm64": "0.21.5",
"@esbuild/freebsd-x64": "0.21.5",
"@esbuild/linux-arm": "0.21.5",
"@esbuild/linux-arm64": "0.21.5",
"@esbuild/linux-ia32": "0.21.5",
"@esbuild/linux-loong64": "0.21.5",
"@esbuild/linux-mips64el": "0.21.5",
"@esbuild/linux-ppc64": "0.21.5",
"@esbuild/linux-riscv64": "0.21.5",
"@esbuild/linux-s390x": "0.21.5",
"@esbuild/linux-x64": "0.21.5",
"@esbuild/netbsd-x64": "0.21.5",
"@esbuild/openbsd-x64": "0.21.5",
"@esbuild/sunos-x64": "0.21.5",
"@esbuild/win32-arm64": "0.21.5",
"@esbuild/win32-ia32": "0.21.5",
"@esbuild/win32-x64": "0.21.5"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/rollup": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
"integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.8"
},
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.53.3",
"@rollup/rollup-android-arm64": "4.53.3",
"@rollup/rollup-darwin-arm64": "4.53.3",
"@rollup/rollup-darwin-x64": "4.53.3",
"@rollup/rollup-freebsd-arm64": "4.53.3",
"@rollup/rollup-freebsd-x64": "4.53.3",
"@rollup/rollup-linux-arm-gnueabihf": "4.53.3",
"@rollup/rollup-linux-arm-musleabihf": "4.53.3",
"@rollup/rollup-linux-arm64-gnu": "4.53.3",
"@rollup/rollup-linux-arm64-musl": "4.53.3",
"@rollup/rollup-linux-loong64-gnu": "4.53.3",
"@rollup/rollup-linux-ppc64-gnu": "4.53.3",
"@rollup/rollup-linux-riscv64-gnu": "4.53.3",
"@rollup/rollup-linux-riscv64-musl": "4.53.3",
"@rollup/rollup-linux-s390x-gnu": "4.53.3",
"@rollup/rollup-linux-x64-gnu": "4.53.3",
"@rollup/rollup-linux-x64-musl": "4.53.3",
"@rollup/rollup-openharmony-arm64": "4.53.3",
"@rollup/rollup-win32-arm64-msvc": "4.53.3",
"@rollup/rollup-win32-ia32-msvc": "4.53.3",
"@rollup/rollup-win32-x64-gnu": "4.53.3",
"@rollup/rollup-win32-x64-msvc": "4.53.3",
"fsevents": "~2.3.2"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/vite": {
"version": "5.4.21",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
"rollup": "^4.20.0"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^18.0.0 || >=20.0.0",
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"less": {
"optional": true
},
"lightningcss": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
}
}
}
}
}

@ -0,0 +1,18 @@
{
"name": "mail-admin-frontend",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --port 5174 --strictPort --host",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^5.4.0",
"@vitejs/plugin-vue": "^5.0.5"
},
"dependencies": {
"vue": "^3.5.10"
}
}

@ -0,0 +1,63 @@
<template>
<Topbar />
<Tabs v-if="authed" v-model="tab" />
<main>
<LoginView v-if="!authed" />
<DashboardView v-else-if="tab==='dashboard'" />
<UsersView v-else-if="tab==='users'" />
<EmailsView v-else-if="tab==='emails'" />
<SettingsView v-else-if="tab==='settings'" />
<LogsView v-else-if="tab==='logs'" />
<ContactsView v-else-if="tab==='contacts'" />
<StatsView v-else-if="tab==='stats'" />
<MailboxView v-else-if="tab==='mailbox'" />
<ConsoleView v-else-if="tab==='console'" />
</main>
</template>
<script setup>
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
import Topbar from './components/Topbar.vue'
import Tabs from './components/Tabs.vue'
import DashboardView from './views/DashboardView.vue'
import LoginView from './views/LoginView.vue'
import UsersView from './views/UsersView.vue'
import EmailsView from './views/EmailsView.vue'
import SettingsView from './views/SettingsView.vue'
import LogsView from './views/LogsView.vue'
import ContactsView from './views/ContactsView.vue'
import StatsView from './views/StatsView.vue'
import MailboxView from './views/MailboxView.vue'
import ConsoleView from './views/ConsoleView.vue'
import { isAuthed } from './composables/auth'
const tab = ref('dashboard')
const authed = isAuthed
const allowed = ['dashboard','users','emails','settings','logs','contacts','stats','mailbox','console','login']
const readHash = () => {
const slug = (window.location.hash || '').replace(/^#\/?/, '') || 'dashboard'
return allowed.includes(slug) ? slug : 'dashboard'
}
const writeHash = (slug) => {
const h = `#/${slug}`
if (window.location.hash !== h) history.replaceState(null, '', h)
}
watch(tab, (v) => writeHash(v))
const onHashChange = () => { const slug = readHash(); if (tab.value !== slug) tab.value = slug }
onMounted(() => {
const slug = readHash()
tab.value = authed.value ? (slug === 'login' ? 'dashboard' : slug) : 'login'
window.addEventListener('hashchange', onHashChange)
})
watch(authed, (v) => {
if (v) {
if (readHash() === 'login') writeHash('dashboard')
} else {
writeHash('login')
}
})
onBeforeUnmount(() => { window.removeEventListener('hashchange', onHashChange) })
</script>
<style scoped>
</style>

@ -0,0 +1,37 @@
export default {
name: 'ComposeDrawer',
props: ['initial'],
emits: ['send','close'],
data() {
return { to: this.initial?.to || '', subject: this.initial?.subject || '', body: this.initial?.body || '' }
},
template: `
<div class="drawer" @click.self="$emit('close')">
<div class="panel">
<h3>写邮件</h3>
<div class="field">
<label>收件人</label>
<input v-model="to" placeholder="example@domain.com">
</div>
<div class="field">
<label>主题</label>
<input v-model="subject" placeholder="主题">
</div>
<div class="field">
<label>正文</label>
<textarea v-model="body" placeholder="请输入正文"></textarea>
</div>
<div class="row">
<button class="btn" @click="onSend">发送</button>
<button class="btn danger" @click="$emit('close')">取消</button>
</div>
</div>
</div>
`,
methods: {
onSend() {
if (!this.to || !this.subject) { alert('请填写收件人与主题'); return }
this.$emit('send', { to:this.to, subject:this.subject, body:this.body })
}
}
}

@ -0,0 +1,16 @@
<template>
<div v-if="message" class="error">{{ message }}</div>
</template>
<script setup>
defineProps({ message: { type: String, default: '' } })
</script>
<style scoped>
.error {
padding: 8px 12px;
color: #dc2626;
font-size: 14px;
}
</style>

@ -0,0 +1,16 @@
<template>
<div v-if="show" class="loader">...</div>
</template>
<script setup>
defineProps({ show: { type: Boolean, default: false } })
</script>
<style scoped>
.loader {
padding: 8px 12px;
color: #2563eb;
font-size: 14px;
}
</style>

@ -0,0 +1,37 @@
export default {
name: 'MailDetail',
props: ['mail'],
emits: ['reply','close'],
template: `
<div class="drawer" @click.self="$emit('close')">
<div class="panel">
<h3>{{ mail.subject }}</h3>
<div class="meta" style="margin-bottom:8px;">
<div>发件人{{ mail.from }}</div>
<div>时间{{ formatTime(mail.time) }}</div>
</div>
<div style="line-height:1.6; color:#111827;">{{ mail.body }}</div>
<div style="margin-top:12px;" v-if="mail.attachments && mail.attachments.length">
<div class="row" style="margin-bottom:8px;">
<div style="font-weight:600;">附件</div>
<div class="badge small"> {{ mail.attachments.length }}</div>
</div>
<div style="display:flex; gap:8px; flex-wrap:wrap;">
<a v-for="(a,i) in mail.attachments" :key="i" class="btn" :href="a.url" target="_blank">{{ a.name }}</a>
</div>
</div>
<div class="row">
<button class="btn" @click="$emit('reply', mail)">回复</button>
<button class="btn danger" @click="$emit('close')">关闭</button>
</div>
</div>
</div>
`,
methods: {
formatTime(ts) {
const d = new Date(ts)
const p = n=>String(n).padStart(2,'0')
return d.getFullYear()+'-'+p(d.getMonth()+1)+'-'+p(d.getDate())+' '+p(d.getHours())+':'+p(d.getMinutes())
}
}
}

@ -0,0 +1,40 @@
export default {
name: 'MailList',
props: ['mails'],
emits: ['open','toggle-star','mark-read','remove'],
template: `
<div class="list">
<div class="card" v-for="m in mails" :key="m.id" @click="$emit('open', m)">
<div class="avatar">{{ m.from.slice(0,1).toUpperCase() }}</div>
<div class="content">
<div class="row">
<div class="subject">{{ m.subject }}</div>
<div class="badge small">{{ formatTime(m.time) }}</div>
</div>
<div class="meta">
<div>{{ m.from }}</div>
<div v-if="!m.read">未读</div>
<div v-if="m.starred">星标</div>
</div>
<div class="preview">{{ m.preview }}</div>
<div class="row" style="margin-top:8px;">
<div class="actions">
<button class="btn" :class="{starred:m.starred}" @click.stop="$emit('toggle-star', m)">{{ m.starred ? '取消星标' : '设为星标' }}</button>
<button class="btn read" v-if="!m.read" @click.stop="$emit('mark-read', m)">标记已读</button>
<button class="btn danger" @click.stop="$emit('remove', m)">删除</button>
</div>
<div class="badge small" v-if="m.attachments && m.attachments.length">附件 {{ m.attachments.length }}</div>
</div>
</div>
</div>
<div v-if="mails.length===0" style="text-align:center; color:var(--muted); padding:24px;">无匹配邮件</div>
</div>
`,
methods: {
formatTime(ts) {
const d = new Date(ts)
const p = n=>String(n).padStart(2,'0')
return d.getFullYear()+'-'+p(d.getMonth()+1)+'-'+p(d.getDate())+' '+p(d.getHours())+':'+p(d.getMinutes())
}
}
}

@ -0,0 +1,22 @@
<template>
<nav class="tabs">
<button :class="{active: modelValue==='dashboard'}" @click="$emit('update:modelValue','dashboard')"></button>
<button :class="{active: modelValue==='users'}" @click="$emit('update:modelValue','users')"></button>
<button :class="{active: modelValue==='emails'}" @click="$emit('update:modelValue','emails')"></button>
<button :class="{active: modelValue==='settings'}" @click="$emit('update:modelValue','settings')"></button>
<button :class="{active: modelValue==='logs'}" @click="$emit('update:modelValue','logs')"></button>
<button :class="{active: modelValue==='contacts'}" @click="$emit('update:modelValue','contacts')"></button>
<button :class="{active: modelValue==='stats'}" @click="$emit('update:modelValue','stats')"></button>
<button :class="{active: modelValue==='mailbox'}" @click="$emit('update:modelValue','mailbox')"></button>
<button :class="{active: modelValue==='console'}" @click="$emit('update:modelValue','console')">API</button>
</nav>
</template>
<script setup>
defineProps({ modelValue: { type: String, required: true } })
defineEmits(['update:modelValue'])
</script>
<style scoped>
</style>

@ -0,0 +1,27 @@
<template>
<header class="topbar">
<div class="brand">邮件服务器管理</div>
<div class="controls">
<label>后端地址</label>
<input v-model="localBase" placeholder="http://localhost:8000" />
<button @click="save"></button>
<span :style="{color: backendOk ? '#16a34a' : '#dc2626'}" class="status">{{ backendOk ? '后端可用' : '后端不可访问' }}</span>
<button v-if="authed" @click="doLogout">退</button>
</div>
</header>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { baseURL, backendOk, saveBaseURL, ping } from '../composables/api'
import { isAuthed, logout } from '../composables/auth'
const localBase = ref('')
const save = () => { saveBaseURL(localBase.value.trim()); ping() }
onMounted(() => { localBase.value = baseURL.value; ping() })
const authed = isAuthed
const doLogout = () => { logout() }
</script>
<style scoped>
</style>

@ -0,0 +1,31 @@
import { ref } from 'vue'
import { token } from './auth'
export const baseURL = ref(localStorage.getItem('baseURL') || 'http://localhost:8000')
export const backendOk = ref(false)
const qs = o => Object.entries(o).filter(([_, v]) => v !== '' && v !== undefined && v !== null).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&')
const path = p => p.startsWith('/') ? p : `/${p}`
export const api = async (p, opt = {}) => {
const url = new URL(path(p), baseURL.value)
const m = (opt.method || 'GET').toUpperCase()
const h = { 'Content-Type': 'application/json' }
if (token.value) h['Authorization'] = `Bearer ${token.value}`
const o = { method: m, headers: h }
if (opt.query && Object.keys(opt.query).length) url.search = qs(opt.query)
if (m === 'POST' || m === 'PUT') o.body = JSON.stringify(opt.body || {})
try {
const r = await fetch(url.toString(), o)
const data = await r.json().catch(() => ({}))
return { ok: r.ok, status: r.status, data }
} catch (e) {
return { ok: false, status: 0, data: { success: false, message: String(e) } }
}
}
export const pretty = v => typeof v === 'string' ? v : JSON.stringify(v, null, 2)
export const saveBaseURL = v => { baseURL.value = v; localStorage.setItem('baseURL', v) }
export const ping = async () => { const r = await api('/api/settings.php'); backendOk.value = r.ok; return r }

@ -0,0 +1,24 @@
import { ref } from 'vue'
export const isAuthed = ref(localStorage.getItem('auth') === 'true')
export const token = ref(localStorage.getItem('token') || '')
export const username = ref(localStorage.getItem('username') || '')
export const setAuth = ({ token: t = '', username: u = '' } = {}) => {
isAuthed.value = true
token.value = t
username.value = u
localStorage.setItem('auth', 'true')
localStorage.setItem('token', t)
localStorage.setItem('username', u)
}
export const logout = () => {
isAuthed.value = false
token.value = ''
username.value = ''
localStorage.removeItem('auth')
localStorage.removeItem('token')
localStorage.removeItem('username')
}

@ -0,0 +1,22 @@
import { ref } from 'vue'
export const useRequest = () => {
const loading = ref(false)
const error = ref('')
const run = async fn => {
error.value = ''
loading.value = true
try {
const r = await fn()
if (!r?.ok) error.value = r?.data?.message || '请求失败'
return r
} catch (e) {
error.value = String(e)
return { ok: false, data: {} }
} finally {
loading.value = false
}
}
return { loading, error, run }
}

@ -0,0 +1,5 @@
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

@ -0,0 +1,35 @@
export const initialMails = [
{
id: 1,
from: "Alice",
subject: "会议安排",
preview: "本周五下午两点的项目会议,请准时参加。",
time: Date.now() - 3600_000,
read: false,
starred: false,
attachments: [{ name: "议程.pdf", url: "#" }],
body: "会议讨论需求、进度与风险评估。",
},
{
id: 2,
from: "Bob",
subject: "代码评审",
preview: "请查看最新的PR并给出建议。",
time: Date.now() - 7200_000,
read: true,
starred: true,
attachments: [],
body: "主要关注接口设计与错误处理。",
},
{
id: 3,
from: "Carol",
subject: "出差报销",
preview: "附上发票与行程单,请查收。",
time: Date.now() - 1800_000,
read: false,
starred: false,
attachments: [{ name: "invoice.jpg", url: "#" }],
body: "如有问题请及时回复。",
},
];

@ -0,0 +1,47 @@
<template>
<section class="section">
<div class="card">
<h2>API调试台</h2>
<Loader :show="loading" />
<ErrorMessage :message="error" />
<div class="row">
<input v-model="consolePath" placeholder="/api/users.php" />
<select v-model="consoleMethod">
<option>GET</option>
<option>POST</option>
<option>PUT</option>
<option>DELETE</option>
</select>
<button @click="sendConsole"></button>
</div>
<textarea v-model="consoleBody" placeholder="请求体JSONGET/DELETE忽略"></textarea>
<pre>{{ pretty(consoleResult) }}</pre>
</div>
<div class="note">如跨域报错请在后端开启CORS响应头</div>
</section>
</template>
<script setup>
import { ref } from 'vue'
import { api, pretty } from '../composables/api'
import { useRequest } from '../composables/request'
import Loader from '../components/Loader.vue'
import ErrorMessage from '../components/ErrorMessage.vue'
const consolePath = ref('/api/users.php')
const consoleMethod = ref('GET')
const consoleBody = ref('')
const consoleResult = ref({})
const { loading, error, run } = useRequest()
const sendConsole = async () => {
let body
if (consoleMethod.value === 'POST' || consoleMethod.value === 'PUT') {
try { body = JSON.parse(consoleBody.value || '{}') } catch (e) { consoleResult.value = 'JSON格式错误'; return }
}
const r = await run(() => api(consolePath.value, { method: consoleMethod.value, body }))
if (r?.ok) consoleResult.value = r.data
}
</script>
<style scoped>
</style>

@ -0,0 +1,86 @@
<template>
<section class="section">
<div class="card">
<h2>联系人列表</h2>
<Loader :show="loading" />
<ErrorMessage :message="error" />
<div class="row">
<input v-model="contactsQuery.username" placeholder="用户名" />
<input v-model.number="contactsQuery.page" type="number" />
<input v-model.number="contactsQuery.perPage" type="number" />
<input v-model="contactsQuery.search" placeholder="关键词" />
<button @click="loadContacts"></button>
</div>
<table>
<thead>
<tr><th>ID</th><th>姓名</th><th>邮箱</th><th>电话</th><th>公司</th><th>部门</th><th>职位</th><th>操作</th></tr>
</thead>
<tbody>
<tr v-for="c in contacts" :key="c.id">
<td>{{ c.id }}</td>
<td>{{ c.name || '' }}</td>
<td>{{ c.email || '' }}</td>
<td>{{ c.phone || '' }}</td>
<td>{{ c.company || '' }}</td>
<td>{{ c.department || '' }}</td>
<td>{{ c.position || '' }}</td>
<td><button @click="contactDetailId = String(c.id); contactDetail()">详情</button></td>
</tr>
</tbody>
</table>
</div>
<div class="card">
<h2>联系人详情</h2>
<div class="row">
<input v-model="contactDetailUsername" placeholder="用户名" />
<input v-model="contactDetailId" placeholder="联系人ID" />
<button @click="contactDetail"></button>
</div>
<pre>{{ pretty(contactDetailResult) }}</pre>
</div>
<div class="card">
<h2>新增/编辑/删除联系人</h2>
<div class="form">
<input v-model="contactForm.id" placeholder="ID" />
<input v-model="contactForm.username" placeholder="用户名" />
<input v-model="contactForm.name" placeholder="姓名" />
<input v-model="contactForm.email" placeholder="邮箱" />
<input v-model="contactForm.phone" placeholder="电话" />
<input v-model="contactForm.company" placeholder="公司" />
<input v-model="contactForm.department" placeholder="部门" />
<input v-model="contactForm.position" placeholder="职位" />
<div class="row">
<button @click="addContact"></button>
<button @click="editContact"></button>
<button class="danger" @click="deleteContact"></button>
</div>
</div>
<pre>{{ pretty(contactMutationResult) }}</pre>
</div>
</section>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { api, pretty } from '../composables/api'
import { useRequest } from '../composables/request'
import Loader from '../components/Loader.vue'
import ErrorMessage from '../components/ErrorMessage.vue'
const contactsQuery = reactive({ username: 'admin', page: 1, perPage: 20, search: '' })
const contacts = ref([])
const { loading, error, run } = useRequest()
const loadContacts = async () => { const r = await run(() => api('/api/contacts.php', { query: contactsQuery })); if (r?.ok) contacts.value = r.data?.data?.contacts || [] }
const contactDetailUsername = ref('admin')
const contactDetailId = ref('')
const contactDetailResult = ref({})
const contactDetail = async () => { const r = await run(() => api('/api/contacts.php', { query: { username: contactDetailUsername.value.trim(), id: Number(contactDetailId.value) } })); if (r?.ok) contactDetailResult.value = r.data }
const contactForm = reactive({ id: '', username: 'admin', name: '', email: '', phone: '', company: '', department: '', position: '' })
const contactMutationResult = ref({})
const addContact = async () => { const r = await run(() => api('/api/contacts.php', { method: 'POST', body: contactForm })); if (r?.ok) contactMutationResult.value = r.data }
const editContact = async () => { const r = await run(() => api('/api/contacts.php', { method: 'PUT', body: { ...contactForm, id: Number(contactForm.id || 0) } })); if (r?.ok) contactMutationResult.value = r.data }
const deleteContact = async () => { const r = await run(() => api('/api/contacts.php', { method: 'DELETE', query: { username: contactForm.username.trim(), id: Number(contactForm.id || 0) } })); if (r?.ok) contactMutationResult.value = r.data }
</script>
<style scoped>
</style>

@ -0,0 +1,38 @@
<template>
<section class="section">
<div class="card">
<h2>快速检查</h2>
<Loader :show="loading" />
<ErrorMessage :message="error" />
<div class="grid">
<div>
<div class="label">设置</div>
<button @click="checkSettings"></button>
<pre>{{ pretty(checkSettingsResult) }}</pre>
</div>
<div>
<div class="label">用户列表</div>
<button @click="checkUsers"></button>
<pre>{{ pretty(checkUsersResult) }}</pre>
</div>
</div>
</div>
</section>
</template>
<script setup>
import { ref } from 'vue'
import { api, pretty } from '../composables/api'
import { useRequest } from '../composables/request'
import Loader from '../components/Loader.vue'
import ErrorMessage from '../components/ErrorMessage.vue'
const checkSettingsResult = ref({})
const checkUsersResult = ref({})
const { loading, error, run } = useRequest()
const checkSettings = async () => { const r = await run(() => api('/api/settings.php')); if (r?.ok) checkSettingsResult.value = r.data }
const checkUsers = async () => { const r = await run(() => api('/api/users.php', { query: { page: 1, perPage: 10 } })); if (r?.ok) checkUsersResult.value = r.data }
</script>
<style scoped>
</style>

@ -0,0 +1,123 @@
<template>
<section class="section">
<div class="card">
<h2>邮件列表</h2>
<Loader :show="loading" />
<ErrorMessage :message="error" />
<div class="row">
<input v-model="emailsQuery.username" placeholder="用户名" />
<select v-model="emailsQuery.folder">
<option value="inbox">inbox</option>
<option value="sent">sent</option>
<option value="draft">draft</option>
<option value="trash">trash</option>
</select>
<input v-model.number="emailsQuery.page" type="number" />
<input v-model.number="emailsQuery.perPage" type="number" />
<input v-model="emailsQuery.search" placeholder="搜索关键词" />
<button @click="loadEmails"></button>
</div>
<table>
<thead>
<tr><th>ID</th><th>主题</th><th>发件人</th><th>日期</th><th>文件夹</th><th>操作</th></tr>
</thead>
<tbody>
<tr v-for="e in emails" :key="e.id">
<td>{{ e.id }}</td>
<td>{{ e.subject || '' }}</td>
<td>{{ e.from || '' }}</td>
<td>{{ e.date || '' }}</td>
<td>{{ e.folder || '' }}</td>
<td><button @click="emailDetailId = String(e.id); emailDetail()">详情</button></td>
</tr>
</tbody>
</table>
</div>
<div class="card">
<h2>邮件详情</h2>
<div class="row">
<input v-model="emailDetailUsername" placeholder="用户名" />
<input v-model="emailDetailId" placeholder="邮件ID" />
<button @click="emailDetail"></button>
</div>
<pre>{{ pretty(emailDetailResult) }}</pre>
</div>
<div class="card">
<h2>发送邮件/草稿</h2>
<div class="form">
<input v-model="sendMail.username" placeholder="用户名" />
<input v-model="sendMail.to" placeholder="收件人邮箱" />
<input v-model="sendMail.subject" placeholder="主题" />
<textarea v-model="sendMail.content" placeholder="内容"></textarea>
<label><input v-model="sendMail.isDraft" type="checkbox" />保存为草稿</label>
<button @click="sendMailSubmit"></button>
</div>
<pre>{{ pretty(sendMailResult) }}</pre>
</div>
<div class="card">
<h2>更新与删除</h2>
<div class="row">
<input v-model="updateMail.id" placeholder="邮件ID" />
<input v-model="updateMail.username" placeholder="用户名" />
<label>已读
<select v-model="updateMail.isRead">
<option value="">不变</option>
<option value="1">1</option>
<option value="0">0</option>
</select>
</label>
<label>文件夹
<select v-model="updateMail.folder">
<option value="">不变</option>
<option value="inbox">inbox</option>
<option value="sent">sent</option>
<option value="draft">draft</option>
<option value="trash">trash</option>
</select>
</label>
<button @click="updateEmail"></button>
<button class="danger" @click="deleteEmail"></button>
</div>
<pre>{{ pretty(updateDeleteResult) }}</pre>
</div>
</section>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { api, pretty } from '../composables/api'
import { useRequest } from '../composables/request'
import Loader from '../components/Loader.vue'
import ErrorMessage from '../components/ErrorMessage.vue'
const emailsQuery = reactive({ username: 'admin', folder: 'inbox', page: 1, perPage: 10, search: '' })
const emails = ref([])
const { loading, error, run } = useRequest()
const loadEmails = async () => { const r = await run(() => api('/api/emails.php', { query: emailsQuery })); if (r?.ok) emails.value = r.data?.data?.emails || [] }
const emailDetailUsername = ref('admin')
const emailDetailId = ref('')
const emailDetailResult = ref({})
const emailDetail = async () => { const r = await run(() => api('/api/emails.php', { query: { username: emailDetailUsername.value.trim(), id: emailDetailId.value.trim() } })); if (r?.ok) emailDetailResult.value = r.data }
const sendMail = reactive({ username: 'admin', to: '', subject: '', content: '', isDraft: false })
const sendMailResult = ref({})
const sendMailSubmit = async () => { const r = await run(() => api('/api/emails.php', { method: 'POST', body: sendMail })); if (r?.ok) sendMailResult.value = r.data }
const updateMail = reactive({ id: '', username: 'admin', isRead: '', folder: '' })
const updateDeleteResult = ref({})
const updateEmail = async () => {
const body = { id: updateMail.id.trim(), username: updateMail.username.trim() }
if (updateMail.isRead !== '') body.isRead = Number(updateMail.isRead)
if (updateMail.folder !== '') body.folder = updateMail.folder
const r = await run(() => api('/api/emails.php', { method: 'PUT', body }))
if (r?.ok) updateDeleteResult.value = r.data
}
const deleteEmail = async () => {
const r = await run(() => api('/api/emails.php', { method: 'DELETE', query: { id: updateMail.id.trim(), username: updateMail.username.trim() } }))
if (r?.ok) updateDeleteResult.value = r.data
}
</script>
<style scoped>
</style>

@ -0,0 +1,39 @@
<template>
<section class="section">
<div class="card">
<h2>登录</h2>
<div class="row">
<input v-model="form.username" placeholder="用户名" />
<input v-model="form.password" placeholder="密码" type="password" />
<button @click="login"></button>
</div>
<Loader :show="loading" />
<ErrorMessage :message="error" />
<pre>{{ pretty(result) }}</pre>
</div>
</section>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { api, pretty } from '../composables/api'
import { useRequest } from '../composables/request'
import { setAuth } from '../composables/auth'
import Loader from '../components/Loader.vue'
import ErrorMessage from '../components/ErrorMessage.vue'
const form = reactive({ username: 'admin', password: '' })
const result = ref({})
const { loading, error, run } = useRequest()
const login = async () => {
const r = await run(() => api('/api/users.php', { method: 'POST', body: { login: true, username: form.username.trim(), password: form.password } }))
result.value = r.data
if (r?.ok && r.data?.success) {
const t = r.data?.data?.token || ''
setAuth({ token: t, username: form.username.trim() })
}
}
</script>
<style scoped>
</style>

@ -0,0 +1,62 @@
<template>
<section class="section">
<div class="card">
<h2>日志列表</h2>
<Loader :show="loading" />
<ErrorMessage :message="error" />
<div class="row">
<input v-model.number="logsQuery.page" type="number" />
<input v-model.number="logsQuery.perPage" type="number" />
<input v-model="logsQuery.search" placeholder="关键词" />
<select v-model="logsQuery.type"><option value="">全部</option><option value="info">info</option><option value="warning">warning</option><option value="error">error</option></select>
<input v-model="logsQuery.startDate" type="date" />
<input v-model="logsQuery.endDate" type="date" />
<button @click="loadLogs"></button>
</div>
<table>
<thead>
<tr><th>ID</th><th>类型</th><th>消息</th><th>IP</th><th>用户</th><th>时间</th><th>操作</th></tr>
</thead>
<tbody>
<tr v-for="l in logs" :key="l.id">
<td>{{ l.id }}</td>
<td>{{ l.type || '' }}</td>
<td>{{ l.message || '' }}</td>
<td>{{ l.ip || '' }}</td>
<td>{{ l.user_id || '' }}</td>
<td>{{ l.created_at || '' }}</td>
<td></td>
</tr>
</tbody>
</table>
</div>
<div class="card">
<h2>删除日志</h2>
<div class="row">
<input v-model="deleteLogs.id" placeholder="日志ID" />
<label><input v-model="deleteLogs.all" type="checkbox" />删除所有</label>
<button class="danger" @click="deleteLogsSubmit"></button>
</div>
<pre>{{ pretty(deleteLogsResult) }}</pre>
</div>
</section>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { api, pretty } from '../composables/api'
import { useRequest } from '../composables/request'
import Loader from '../components/Loader.vue'
import ErrorMessage from '../components/ErrorMessage.vue'
const logsQuery = reactive({ page: 1, perPage: 10, search: '', type: '', startDate: '', endDate: '' })
const logs = ref([])
const deleteLogs = reactive({ id: '', all: false })
const deleteLogsResult = ref({})
const { loading, error, run } = useRequest()
const loadLogs = async () => { const r = await run(() => api('/api/logs.php', { query: logsQuery })); if (r?.ok) logs.value = r.data?.data?.logs || [] }
const deleteLogsSubmit = async () => { const r = await run(() => api('/api/logs.php', { method: 'DELETE', query: { id: deleteLogs.id.trim(), all: deleteLogs.all || undefined } })); if (r?.ok) deleteLogsResult.value = r.data }
</script>
<style scoped>
</style>

@ -0,0 +1,32 @@
<template>
<section class="section">
<div class="card">
<h2>邮箱信息</h2>
<Loader :show="loading" />
<ErrorMessage :message="error" />
<div class="row">
<input v-model="mailboxUsername" placeholder="用户名" />
<button @click="mailboxInfo"></button>
<button @click="mailboxQuota"></button>
</div>
<pre>{{ pretty(mailboxResult) }}</pre>
</div>
</section>
</template>
<script setup>
import { ref } from 'vue'
import { api, pretty } from '../composables/api'
import { useRequest } from '../composables/request'
import Loader from '../components/Loader.vue'
import ErrorMessage from '../components/ErrorMessage.vue'
const mailboxUsername = ref('admin')
const mailboxResult = ref({})
const { loading, error, run } = useRequest()
const mailboxInfo = async () => { const r = await run(() => api('/api/mailbox.php', { query: { username: mailboxUsername.value.trim() } })); if (r?.ok) mailboxResult.value = r.data }
const mailboxQuota = async () => { const r = await run(() => api('/api/mailbox.php', { query: { username: mailboxUsername.value.trim(), action: 'quota' } })); if (r?.ok) mailboxResult.value = r.data }
</script>
<style scoped>
</style>

@ -0,0 +1,40 @@
<template>
<section class="section">
<div class="card">
<h2>获取服务器设置</h2>
<Loader :show="loading" />
<ErrorMessage :message="error" />
<button @click="getSettings"></button>
<pre>{{ pretty(getSettingsResult) }}</pre>
</div>
<div class="card">
<h2>保存服务器设置</h2>
<textarea v-model="settingsPayload" placeholder='{"smtp":{"port":25,"host":"0.0.0.0","max_connections":100},"pop3":{"port":110,"host":"0.0.0.0","max_connections":100},"server":{"domain":"test.com","max_email_size":10485760}}'></textarea>
<button @click="saveSettings"></button>
<pre>{{ pretty(saveSettingsResult) }}</pre>
</div>
</section>
</template>
<script setup>
import { ref } from 'vue'
import { api, pretty } from '../composables/api'
import { useRequest } from '../composables/request'
import Loader from '../components/Loader.vue'
import ErrorMessage from '../components/ErrorMessage.vue'
const getSettingsResult = ref({})
const settingsPayload = ref('')
const saveSettingsResult = ref({})
const { loading, error, run } = useRequest()
const getSettings = async () => { const r = await run(() => api('/api/settings.php')); if (r?.ok) getSettingsResult.value = r.data }
const saveSettings = async () => {
let body = {}
try { body = JSON.parse(settingsPayload.value || '{}') } catch (e) { saveSettingsResult.value = 'JSON格式错误'; return }
const r = await run(() => api('/api/settings.php', { method: 'POST', body }))
if (r?.ok) saveSettingsResult.value = r.data
}
</script>
<style scoped>
</style>

@ -0,0 +1,33 @@
<template>
<section class="section">
<div class="card">
<h2>统计</h2>
<Loader :show="loading" />
<ErrorMessage :message="error" />
<div class="row">
<input v-model="statsQuery.username" placeholder="用户名" />
<input v-model="statsQuery.startDate" type="date" />
<input v-model="statsQuery.endDate" type="date" />
<select v-model="statsQuery.type"><option value="mail">mail</option><option value="traffic">traffic</option><option value="send">send</option></select>
<button @click="loadStats"></button>
</div>
<pre>{{ pretty(statsResult) }}</pre>
</div>
</section>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { api, pretty } from '../composables/api'
import { useRequest } from '../composables/request'
import Loader from '../components/Loader.vue'
import ErrorMessage from '../components/ErrorMessage.vue'
const statsQuery = reactive({ username: 'admin', startDate: '', endDate: '', type: 'mail' })
const statsResult = ref({})
const { loading, error, run } = useRequest()
const loadStats = async () => { const r = await run(() => api('/api/stats.php', { query: statsQuery })); if (r?.ok) statsResult.value = r.data }
</script>
<style scoped>
</style>

@ -0,0 +1,93 @@
<template>
<section class="section">
<div class="card">
<h2>用户列表</h2>
<Loader :show="loading" />
<ErrorMessage :message="error" />
<div class="row">
<input v-model.number="usersQuery.page" type="number" />
<input v-model.number="usersQuery.perPage" type="number" />
<input v-model="usersQuery.search" placeholder="搜索关键词" />
<button @click="loadUsers"></button>
</div>
<table id="usersTable">
<thead>
<tr>
<th>用户名</th>
<th>邮箱</th>
<th>管理员</th>
<th>启用</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="u in users" :key="u.username">
<td>{{ u.username || "" }}</td>
<td>{{ u.email || "" }}</td>
<td>{{ u.is_admin ? "是" : "否" }}</td>
<td>{{ u.is_enabled ? "是" : "否" }}</td>
<td><button @click="deleteUser(u.username)"></button></td>
</tr>
</tbody>
</table>
</div>
<div class="card">
<h2>添加用户</h2>
<div class="form">
<input v-model="addUser.username" placeholder="用户名" />
<input v-model="addUser.email" placeholder="邮箱" />
<input v-model="addUser.password" placeholder="密码" type="password" />
<label
><input v-model="addUser.is_admin" type="checkbox" />管理员</label
>
<label
><input v-model="addUser.is_enabled" type="checkbox" />启用</label
>
<button @click="createUser"></button>
</div>
<pre>{{ pretty(addUserResult) }}</pre>
</div>
</section>
</template>
<script setup>
import { ref, reactive } from "vue";
import { api, pretty } from "../composables/api";
import { useRequest } from "../composables/request";
import Loader from "../components/Loader.vue";
import ErrorMessage from "../components/ErrorMessage.vue";
const usersQuery = reactive({ page: 1, perPage: 10, search: "" });
const users = ref([]);
const { loading, error, run } = useRequest();
const loadUsers = async () => {
const r = await run(() => api("/api/users.php", { query: usersQuery }));
if (r?.ok) users.value = r.data?.data?.users || [];
};
const deleteUser = async (username) => {
const r = await run(() =>
api("/api/users.php", { method: "DELETE", query: { username } })
);
if (r?.ok) {
alert(pretty(r.data));
loadUsers();
}
};
const addUser = reactive({
username: "",
email: "",
password: "",
is_admin: false,
is_enabled: true,
});
const addUserResult = ref({});
const createUser = async () => {
const r = await run(() =>
api("/api/users.php", { method: "POST", body: addUser })
);
if (r?.ok) addUserResult.value = r.data;
};
</script>
<style scoped></style>

@ -0,0 +1,247 @@
:root {
--primary: #3b82f6;
--bg: #f6f7f9;
--text: #111827;
--muted: #6b7280;
--card: #ffffff;
--danger: #ef4444;
--success: #10b981;
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
height: 100%;
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica,
Arial, PingFang SC, Microsoft YaHei, sans-serif;
}
.app {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.topbar {
height: 56px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
background: var(--card);
border-bottom: 1px solid #e5e7eb;
position: sticky;
top: 0;
z-index: 10;
}
.topbar .title {
font-size: 18px;
font-weight: 600;
}
.topbar .badge {
background: var(--primary);
color: #fff;
border-radius: 999px;
padding: 4px 10px;
font-size: 12px;
}
.search {
padding: 10px 16px;
background: var(--card);
border-bottom: 1px solid #e5e7eb;
display: flex;
gap: 8px;
}
.search input {
flex: 1;
height: 36px;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 0 12px;
font-size: 14px;
}
.search button {
height: 36px;
padding: 0 12px;
border: none;
border-radius: 8px;
background: var(--primary);
color: #fff;
font-size: 14px;
}
.list {
flex: 1;
overflow: auto;
padding: 12px;
}
.card {
background: var(--card);
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 12px;
margin-bottom: 10px;
display: flex;
gap: 12px;
align-items: flex-start;
}
.avatar {
width: 36px;
height: 36px;
border-radius: 999px;
background: #dbeafe;
color: #1e40af;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
}
.content {
flex: 1;
min-width: 0;
}
.row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.subject {
font-size: 15px;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.meta {
font-size: 12px;
color: var(--muted);
display: flex;
gap: 8px;
}
.preview {
font-size: 13px;
color: #374151;
margin-top: 6px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.actions {
display: flex;
align-items: center;
gap: 8px;
}
.btn {
border: none;
background: #eef2ff;
color: #3730a3;
padding: 6px 10px;
border-radius: 8px;
font-size: 12px;
}
.btn.danger {
background: #fee2e2;
color: #b91c1c;
}
.btn.starred {
background: #fff7ed;
color: #c2410c;
}
.btn.read {
background: #ecfeff;
color: #0e7490;
}
.fab {
position: fixed;
right: 16px;
bottom: 84px;
width: 56px;
height: 56px;
border-radius: 999px;
background: var(--primary);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
border: none;
}
.tabs {
position: sticky;
bottom: 0;
background: var(--card);
border-top: 1px solid #e5e7eb;
height: 64px;
display: flex;
}
.tab {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 12px;
color: var(--muted);
}
.tab.active {
color: var(--primary);
}
.drawer {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.12);
display: flex;
justify-content: center;
align-items: flex-end;
padding: 16px;
}
.panel {
width: 100%;
max-width: 640px;
background: var(--card);
border-radius: 16px;
border: 1px solid #e5e7eb;
padding: 16px;
max-height: 80vh;
overflow: auto;
}
.panel h3 {
margin: 0 0 8px;
font-size: 16px;
}
.panel .field {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 12px;
}
.panel input,
.panel textarea {
width: 100%;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 10px 12px;
font-size: 14px;
}
.panel textarea {
min-height: 120px;
resize: vertical;
}
.panel .row {
margin-top: 8px;
}
.panel .row .btn {
font-size: 14px;
padding: 10px 14px;
}
.badge.small {
font-size: 11px;
padding: 2px 8px;
border-radius: 999px;
background: #f3f4f6;
color: #374151;
}

@ -0,0 +1,16 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
port: 5174,
open: false,
host: '0.0.0.0',
allowedHosts: ['gmaeskhqybqt.sealoshzh.site']
},
preview: {
port: 5174,
host: '0.0.0.0'
}
})

@ -0,0 +1,9 @@
import { defineConfig } from 'vite'
export default defineConfig({
server: {
port: 5174,
strictPort: true,
host: true
}
})

@ -0,0 +1,2 @@
#Mon Dec 15 23:55:24 CST 2025
gradle.version=9.0-milestone-1

@ -0,0 +1,2 @@
#Tue Dec 16 00:09:46 CST 2025
java.home=C\:\\Users\\32428\\Desktop\\android-studio\\jbr

@ -0,0 +1,2 @@
#Mon Dec 15 23:49:41 CST 2025
gradle.version=9.0-milestone-1

@ -0,0 +1,2 @@
#Mon Dec 15 23:48:43 CST 2025
java.home=C\:\\Users\\32428\\Desktop\\android-studio\\jbr

@ -0,0 +1,53 @@
plugins {
id 'com.android.application'
}
android {
namespace 'com.example.mailclient'
compileSdk 33
defaultConfig {
applicationId "com.example.mailclient"
minSdk 21
targetSdk 33
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.recyclerview:recyclerview:1.3.1'
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// OkHttp
implementation 'com.squareup.okhttp3:okhttp:4.10.0' // 使Retrofit 2.9.0
implementation 'com.squareup.okhttp3:logging-interceptor:4.10.0'
// Gson
implementation 'com.google.code.gson:gson:2.10.1'
//
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save