Compare commits
19 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
2d5e8d4716 | 1 month ago |
|
|
0ef2c10687 | 1 month ago |
|
|
0b8d7b002b | 1 month ago |
|
|
18abe265ca | 1 month ago |
|
|
a8b1bb64fe | 1 month ago |
|
|
c501baad67 | 1 month ago |
|
|
00c6009b55 | 1 month ago |
|
|
802ef1ee2b | 1 month ago |
|
|
da321c9829 | 1 month ago |
|
|
f34f525aa8 | 1 month ago |
|
|
2e522414aa | 1 month ago |
|
|
72aebacbec | 1 month ago |
|
|
9649c5c1a2 | 1 month ago |
|
|
23c779a10e | 1 month ago |
|
|
8224a21df0 | 1 month ago |
|
|
dbb96a6781 | 1 month ago |
|
|
ec9ee4e18e | 1 month ago |
|
|
ddb811bac7 | 1 month ago |
|
|
fb296f7b3e | 1 month ago |
@ -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,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,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,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,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,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,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,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,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,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
|
||||||
|
}
|
||||||
|
})
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,2 @@
|
|||||||
|
#Mon Dec 15 23:55:24 CST 2025
|
||||||
|
gradle.version=9.0-milestone-1
|
||||||
Binary file not shown.
@ -0,0 +1,2 @@
|
|||||||
|
#Tue Dec 16 00:09:46 CST 2025
|
||||||
|
java.home=C\:\\Users\\32428\\Desktop\\android-studio\\jbr
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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…
Reference in new issue