diff --git a/install.php b/install.php new file mode 100644 index 0000000..e2b6c77 --- /dev/null +++ b/install.php @@ -0,0 +1,630 @@ +已安装 + +
+

✅ 系统已安装

+

如需重新安装,请先删除 data/installed.lock 文件。

+ 进入登录页 +
'); +} + +$step = (int)($_GET['step'] ?? $_POST['step'] ?? 1); +$error = ''; + +if ($step > 1 && empty($_SESSION['install_step_done'])) { + $step = 1; +} + +/** + * 环境检测 + */ +function check_environment() { + $checks = []; + + $checks[] = [ + 'name' => 'PHP 版本 ≥ 7.4', + 'ok' => version_compare(PHP_VERSION, '7.4.0', '>='), + 'value' => PHP_VERSION, + 'tip' => '当前版本过低,请升级 PHP。', + ]; + + $checks[] = [ + 'name' => 'IMAP 扩展', + 'ok' => function_exists('imap_open'), + 'value' => function_exists('imap_open') ? '已开启' : '未开启', + 'tip' => '需要在 PHP 配置中启用 php_imap 扩展,或联系主机商开启。', + ]; + + $checks[] = [ + 'name' => 'PDO SQLite 驱动', + 'ok' => extension_loaded('pdo_sqlite'), + 'value' => extension_loaded('pdo_sqlite') ? '已开启' : '未开启', + 'tip' => '需要 pdo_sqlite 扩展来存储邮件。', + ]; + + $checks[] = [ + 'name' => 'mbstring 扩展', + 'ok' => function_exists('mb_convert_encoding'), + 'value' => function_exists('mb_convert_encoding') ? '已开启' : '未开启', + 'tip' => '用于处理邮件编码转换。', + ]; + + $checks[] = [ + 'name' => 'Session 支持', + 'ok' => function_exists('session_start'), + 'value' => function_exists('session_start') ? '已开启' : '未开启', + 'tip' => '用于登录状态保持。', + ]; + + $checks[] = [ + 'name' => 'cURL 扩展', + 'ok' => function_exists('curl_init'), + 'value' => function_exists('curl_init') ? '已开启' : '未开启', + 'tip' => '用于邮件转发功能。未开启不影响收信。', + 'warn' => true, + ]; + + $writable = is_dir(DATA_DIR) ? is_writable(DATA_DIR) : is_writable(__DIR__); + $checks[] = [ + 'name' => '目录可写', + 'ok' => $writable, + 'value' => $writable ? '可写' : '不可写', + 'tip' => '需要能创建 data/ 目录并写入文件。请设置目录权限为 755 或 777。', + ]; + + $https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') + || (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https'); + $checks[] = [ + 'name' => 'HTTPS(强烈建议)', + 'ok' => $https, + 'value' => $https ? '已开启' : '未开启', + 'tip' => '未启用 HTTPS 时,登录密码和邮件内容会以明文传输。', + 'warn' => true, + ]; + + return $checks; +} + +/** + * 创建数据库 + */ +function do_install_database() { + if (!is_dir(DATA_DIR)) { + if (!mkdir(DATA_DIR, 0755, true)) throw new Exception('无法创建 data 目录'); + } + + $htaccess = DATA_DIR . '/.htaccess'; + if (!file_exists($htaccess)) { + file_put_contents($htaccess, "Require all denied\nDeny from all\n"); + } + + $db = new PDO('sqlite:' . DB_FILE); + $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $db->exec("CREATE TABLE IF NOT EXISTS mails ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + account TEXT NOT NULL, + account_label TEXT, + uid TEXT NOT NULL, + from_addr TEXT, + subject TEXT, + mail_date TEXT, + mail_ts INTEGER DEFAULT 0, + body TEXT, + body_html TEXT, + attachments TEXT, + blocked INTEGER DEFAULT 0, + is_starred INTEGER DEFAULT 0, + is_important INTEGER DEFAULT 0, + is_read INTEGER DEFAULT 1, + created_at TEXT, + UNIQUE(account, uid) + )"); + $db->exec("CREATE INDEX IF NOT EXISTS idx_account ON mails(account)"); + $db->exec("CREATE INDEX IF NOT EXISTS idx_mail_ts ON mails(mail_ts)"); + $db->exec("CREATE INDEX IF NOT EXISTS idx_blocked ON mails(blocked)"); + $db->exec("CREATE INDEX IF NOT EXISTS idx_starred ON mails(is_starred)"); + $db->exec("CREATE INDEX IF NOT EXISTS idx_read ON mails(is_read)"); + + $db->exec("CREATE TABLE IF NOT EXISTS accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + key_name TEXT UNIQUE NOT NULL, + label TEXT NOT NULL, + host TEXT NOT NULL, + user TEXT NOT NULL, + password TEXT NOT NULL, + enabled INTEGER DEFAULT 1, + last_fetch TEXT, + created_at TEXT + )"); + + $db->exec("CREATE TABLE IF NOT EXISTS block_rules ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + keyword TEXT NOT NULL, + enabled INTEGER DEFAULT 1, + created_at TEXT + )"); + + $db->exec("CREATE TABLE IF NOT EXISTS forward_rules ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + keyword TEXT NOT NULL, + channel TEXT NOT NULL, + target TEXT NOT NULL, + enabled INTEGER DEFAULT 1, + created_at TEXT + )"); + + $db->exec("CREATE TABLE IF NOT EXISTS blocked_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + account TEXT NOT NULL, + uid TEXT NOT NULL, + from_addr TEXT, + subject TEXT, + mail_date TEXT, + matched TEXT, + created_at TEXT, + UNIQUE(account, uid) + )"); + + $db->exec("CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT + )"); + + // 默认拦截规则 + $count = $db->query("SELECT COUNT(*) FROM block_rules")->fetchColumn(); + if ($count == 0) { + $stmt = $db->prepare("INSERT INTO block_rules (keyword, enabled, created_at) VALUES (?, 1, ?)"); + foreach (['取消订阅', '退订', 'unsubscribe'] as $kw) { + $stmt->execute([$kw, date('Y-m-d H:i:s')]); + } + } + + // 默认设置 + $db->exec("INSERT OR IGNORE INTO settings (key, value) VALUES ('fetch_days', '7')"); + $db->exec("INSERT OR IGNORE INTO settings (key, value) VALUES ('auto_delete_days', '0')"); + + // 附件目录 + $attachDir = DATA_DIR . '/attachments'; + if (!is_dir($attachDir)) mkdir($attachDir, 0755, true); + + // 用户库 + $udb = new PDO('sqlite:' . USERS_DB); + $udb->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $udb->exec("CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + created_at TEXT + )"); + + return true; +} + +/** + * 创建管理员 + */ +function do_create_admin($username, $password) { + if (strlen($username) < 3) throw new Exception('用户名至少 3 个字符'); + if (strlen($password) < 8) throw new Exception('密码至少 8 位'); + + $udb = new PDO('sqlite:' . USERS_DB); + $udb->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $check = $udb->prepare("SELECT id FROM users WHERE username = ?"); + $check->execute([$username]); + if ($check->fetchColumn()) throw new Exception('用户名已存在,请换一个'); + + $stmt = $udb->prepare("INSERT INTO users (username, password_hash, created_at) VALUES (?, ?, ?)"); + $stmt->execute([$username, password_hash($password, PASSWORD_DEFAULT), date('Y-m-d H:i:s')]); + return true; +} + +/** + * 添加邮箱 + */ +function do_add_account($key, $label, $host, $user, $pass) { + $db = new PDO('sqlite:' . DB_FILE); + $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $stmt = $db->prepare("INSERT INTO accounts (key_name, label, host, user, password, enabled, created_at) + VALUES (?, ?, ?, ?, ?, 1, ?)"); + $stmt->execute([$key, $label, $host, $user, $pass, date('Y-m-d H:i:s')]); + return true; +} + +/** + * POST 处理 + */ +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $action = $_POST['action'] ?? ''; + try { + if ($action === 'step1') { + $checks = check_environment(); + $blocked = array_filter($checks, fn($c) => !$c['ok'] && empty($c['warn'])); + if ($blocked) { + $error = '请先解决所有标红的检测项'; + } else { + $_SESSION['install_step_done'] = 1; + header('Location: install.php?step=2'); + exit; + } + } + + if ($action === 'step2') { + do_install_database(); + $_SESSION['install_step_done'] = 2; + header('Location: install.php?step=3'); + exit; + } + + if ($action === 'step3') { + $username = trim($_POST['username'] ?? ''); + $password = $_POST['password'] ?? ''; + $confirm = $_POST['confirm'] ?? ''; + if ($password !== $confirm) throw new Exception('两次输入的密码不一致'); + do_create_admin($username, $password); + $_SESSION['admin_username'] = $username; + $_SESSION['install_step_done'] = 3; + header('Location: install.php?step=4'); + exit; + } + + if ($action === 'step4') { + if (isset($_POST['skip'])) { + $_SESSION['install_step_done'] = 4; + header('Location: install.php?step=5'); + exit; + } + $key = trim($_POST['key_name'] ?? ''); + $label = trim($_POST['label'] ?? ''); + $host = trim($_POST['host'] ?? ''); + $user = trim($_POST['user'] ?? ''); + $pass = $_POST['password'] ?? ''; + + if ($key === '' || $label === '' || $host === '' || $user === '' || $pass === '') { + throw new Exception('请填写完整,或点击跳过'); + } + if (!preg_match('/^[a-z0-9_]+$/', $key)) { + throw new Exception('标识只能包含小写字母、数字、下划线'); + } + + $mbox = @imap_open($host, $user, $pass, 0, 1); + if (!$mbox) throw new Exception('连接失败:' . imap_last_error()); + imap_close($mbox); + + do_add_account($key, $label, $host, $user, $pass); + $_SESSION['install_step_done'] = 4; + $_SESSION['added_account'] = $label; + header('Location: install.php?step=5'); + exit; + } + + if ($action === 'finish') { + file_put_contents(LOCK_FILE, json_encode([ + 'installed_at' => date('Y-m-d H:i:s'), + 'version' => '1.0.0', + ])); + unset($_SESSION['install_step_done']); + header('Location: install.php?step=done'); + exit; + } + } catch (Exception $e) { + $error = $e->getMessage(); + } +} + +/** + * 完成页 + */ +if (isset($_GET['step']) && $_GET['step'] === 'done') { + ?> + + + + + + 安装完成 - Zmail System + + + + +
+
+
🎉
+

安装完成

+

Zmail System 已就绪。

+ +
+
    +
  • 管理员账号:
  • + +
  • 已添加邮箱:
  • + +
+
+ +
+ ⚠️ 重要:请立即删除 install.php
+ 否则他人可以重新运行安装程序,覆盖你的数据库。 +
+ +

接下来建议:

+ + + 进入登录页 +
+
+ + + !$c['ok'] && empty($c['warn'])); +$can_proceed = empty($blocked); +?> + + + + + + +安装向导 - 第 <?= $step ?> 步 - Zmail System + + + + +
+
+

📬 Zmail System 安装向导

+ +
+ +
+ +
+ + +
+ + + +

第 1 步:环境检测

+ + +
✅ 所有必需项均已通过
+ +
❌ 有 项未通过,请先解决
+ + + + +
+ + + +
+ + +

第 2 步:创建数据库

+ +

系统将在 data/ 目录下创建:

+ + +
+ SQLite 是文件型数据库,不需要额外申请,适合虚拟主机。 +
+ +
+ + + +
+ + +

第 3 步:创建管理员账号

+ +
+ + + + + +
至少 3 个字符
+ + + +
建议包含大小写字母和数字
+ + + + + +
+ + +

第 4 步:添加第一个邮箱

+ +

这一步可以跳过,安装完成后再添加。

+ + [ + 'label' => 'Gmail', + 'host' => '{imap.gmail.com:993/imap/ssl}INBOX', + 'tip' => '需在 Google 账户生成「应用专用密码」。', + ], + 'qq' => [ + 'label' => 'QQ 邮箱', + 'host' => '{imap.qq.com:993/imap/ssl}INBOX', + 'tip' => '开启 IMAP 服务后获得 16 位授权码,用户名填 QQ 号。', + ], + 'outlook' => [ + 'label' => 'Outlook', + 'host' => '{outlook.office365.com:993/imap/ssl}INBOX', + 'tip' => '需使用应用密码;部分账户默认关闭 IMAP。', + ], + '163' => [ + 'label' => '163 邮箱', + 'host' => '{imap.163.com:993/imap/ssl}INBOX', + 'tip' => '开启 IMAP 后生成授权码。', + ], + 'custom' => [ + 'label' => '自定义', + 'host' => '', + 'tip' => '格式:{imap.服务器:993/imap/ssl}INBOX', + ], + ]; + ?> + +
+ + + + + +
+ +
+
+ + +
+
+ + +
+
+ + + + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+ + + + +

第 5 步:确认安装

+ +
✅ 所有配置已完成
+ + + +
+ 点击下方按钮后,系统会生成安装锁文件,install.php 将无法再次访问。 +
+ +
+ + +
+ + +
+
+ + \ No newline at end of file