Compare commits
19 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
2d5e8d4716 | 4 weeks ago |
|
|
0ef2c10687 | 4 weeks ago |
|
|
0b8d7b002b | 4 weeks ago |
|
|
18abe265ca | 4 weeks ago |
|
|
a8b1bb64fe | 4 weeks ago |
|
|
c501baad67 | 4 weeks ago |
|
|
00c6009b55 | 4 weeks ago |
|
|
802ef1ee2b | 4 weeks ago |
|
|
da321c9829 | 4 weeks ago |
|
|
f34f525aa8 | 4 weeks ago |
|
|
2e522414aa | 4 weeks ago |
|
|
72aebacbec | 4 weeks ago |
|
|
9649c5c1a2 | 4 weeks ago |
|
|
23c779a10e | 4 weeks ago |
|
|
8224a21df0 | 4 weeks ago |
|
|
dbb96a6781 | 4 weeks ago |
|
|
ec9ee4e18e | 4 weeks ago |
|
|
ddb811bac7 | 4 weeks ago |
|
|
fb296f7b3e | 4 weeks 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