Repository navigation
Expand file tree
/
Copy pathdeploy.sh
More file actions
executable file
·1935 lines (1676 loc) · 65.6 KB
/
Copy pathdeploy.sh
File metadata and controls
executable file
·1935 lines (1676 loc) · 65.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env bash
# ============================================================================
# Magicmail 一键部署脚本
# Copyright (C) 2026 magiccode (魔法代码)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# Project: https://github.com/magiccode1412/magicmail
# License: AGPL-3.0 (https://github.com/magiccode1412/magicmail/blob/main/LICENSE)
#
# 用法: ./deploy.sh [命令] [选项]
#
# 命令:
# install 安装并启动服务(默认)
# start 启动服务
# stop 停止服务
# restart 重启服务
# status 查看运行状态
# update 更新到最新版本
# uninstall 卸载(保留数据,可选删除)
# logs 查看日志
# version 显示版本信息
# doctor 环境自检
# help 显示帮助信息
#
# 选项:
# --yes, -y 跳过确认提示(非交互模式)
# --version V 指定安装版本(默认 latest)
# --port P 指定监听端口(默认 8080)
#
# 安装后可通过 magicmail 命令管理:
# magicmail status / start / stop / restart / update / uninstall / logs / version / doctor
# ============================================================================
set -euo pipefail
# ─── 颜色定义 ──────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m'
# ─── 配置 ──────────────────────────────────────────
REPO="magiccode1412/magicmail"
REPO_URL="https://github.com/${REPO}"
API_URL="https://api.github.com/repos/${REPO}"
RELEASE_URL="${API_URL}/releases/latest"
INSTALL_DIR="/opt/magicmail"
BIN_NAME="magicmail"
DATA_DIR="/var/lib/magicmail"
LOG_FILE="/var/log/magicmail.log"
CONFIG_DIR="${INSTALL_DIR}/config"
SERVICE_NAME="magicmail"
CLI_BIN="/usr/local/bin/${BIN_NAME}"
DEFAULT_PORT=8080
# 全局变量
TARGET_OS=""
TARGET_ARCH=""
TARGET_VERSION=""
TARGET_PORT="${DEFAULT_PORT}"
INTERACTIVE=true
IN_DOCKER=false
# ─── 工具函数 ──────────────────────────────────────
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
success() { echo -e "${GREEN}[OK]${NC} $*"; }
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
die() { error "$*"; exit 1; }
print_banner() {
local ver="$(get_installed_version 2>/dev/null || echo '?.?.?')"
echo ""
# ─── 主 Logo(青色粗体,figlet slant 字体)───────
printf '\033[1;36m'
cat << 'BANNER'
__ ___ _ _ __
/ |/ /___ _____ _(_)________ ___ ____ _(_) /
/ /|_/ / __ `/ __ `/ / ___/ __ `__ \/ __ `/ / /
/ / / / /_/ / /_/ / / /__/ / / / / / /_/ / / /
/_/ /_/\__,_/\__, /_/\___/_/ /_/ /_/\__,_/_/_/
/____/
BANNER
printf '\033[0m'
# ─── 版本号 + 副标题框─────────────────────────────
# 中文字符为双宽度(×2),内容区=8空格+6中文(12列)+8空格=28列
printf "\033[0;36m v%s\n" "${ver}"
printf '\033[1;37m'
printf ' ┌────────────────────────────┐\n'
printf ' │ 一键部署工具 │\n'
printf ' └────────────────────────────┘\n'
printf '\033[0m\n'
}
# ═══════════════════════════════════════════════════════
# 第一部分:平台检测模块
# ═══════════════════════════════════════════════════════
# 检测操作系统类型 (linux/darwin/windows)
detect_os() {
local os_name="$(uname -s)"
case "${os_name,,}" in
linux*) echo "linux" ;;
darwin*) echo "darwin" ;;
msys*|mingw*|cygwin*) echo "windows" ;;
*) die "不支持的操作系统: ${os_name}" ;;
esac
}
# 检测 CPU 架构 (amd64/arm64)
detect_arch() {
local arch_name="$(uname -m)"
case "${arch_name,,}" in
x86_64|amd64) echo "amd64" ;;
aarch64|arm64) echo "arm64" ;;
armv7l|armhf) die "不支持 ARMv7,请使用 ARM64 (aarch64) 设备" ;;
i386|i686) die "不支持 32 位系统,请使用 64 位系统" ;;
*) die "不支持的 CPU 架构: ${arch_name}" ;;
esac
}
# 检测是否运行在 Docker 容器内
is_docker() {
if [ -f /.dockerenv ] 2>/dev/null; then
return 0
fi
if [ -f /proc/1/cgroup ] 2>/dev/null && grep -qE 'docker|lxc|containerd' /proc/1/cgroup 2>/dev/null; then
return 0
fi
return 1
}
# 检测 Linux 发行版名称
detect_distro() {
if [[ "$(uname -s)" != "Linux"* ]]; then
echo "macOS"
return
fi
# 优先使用 /etc/os-release (最可靠)
if [ -f /etc/os-release ]; then
# shellcheck source=/dev/null
. /etc/os-release
case "${ID,,}" in
debian|ubuntu|linuxmint|pop|elementary)
echo "debian" ;;
centos|rhel|rocky|alma|fedora|ol)
echo "rhel" ;;
alpine)
echo "alpine" ;;
arch|manjaro|endeavouros)
echo "arch" ;;
opensuse*|suse|sles)
echo "opensuse" ;;
*)
echo "${ID:-unknown}" ;;
esac
return
fi
# 兜底探测
if [ -f /etc/debian_version ]; then
echo "debian"
elif [ -f /etc/redhat-release ]; then
echo "rhel"
elif [ -f /etc/alpine-release ]; then
echo "alpine"
elif [ -f /etc/arch-release ]; then
echo "arch"
elif [ -f /etc/SuSE-release ] || [ -f /etc/SUSE-brand ]; then
echo "opensuse"
else
echo "unknown-linux"
fi
}
# 获取可用的包管理器名称
get_package_manager() {
local distro
distro="$(detect_distro)"
case "${distro}" in
debian)
if command -v apt-get &>/dev/null; then echo "apt"; return; fi
;;
rhel)
if command -v dnf &>/dev/null; then echo "dnf"; return; fi
if command -v yum &>/dev/null; then echo "yum"; return; fi
;;
alpine)
if command -v apk &>/dev/null; then echo "apk"; return; fi
;;
arch)
if command -v pacman &>/dev/null; then echo "pacman"; return; fi
;;
opensuse)
if command -v zypper &>/dev/null; then echo "zypper"; return; fi
;;
macOS)
if command -v brew &>/dev/null; then echo "brew"; return; fi
;;
esac
# 全局兜底扫描
for pm in apt dnf yum apk pacman zypper brew; do
if command -v "${pm}" &>/dev/null; then
echo "${pm}"
return
fi
done
echo "unknown"
}
is_linux() { [[ "$(uname -s)" == Linux* ]]; }
is_macos() { [[ "$(uname -s)" == Darwin* ]]; }
# ═══════════════════════════════════════════════════════
# 第一点五:GitHub 镜像代理(国内加速)
# ═══════════════════════════════════════════════════════
# 镜像代理列表(按优先级排序,依次尝试)
# 格式: "前缀" - 将 GitHub URL 前面加上此前缀即可使用镜像
MIRROR_PREFIXES=(
"" # 原始地址(优先直连)
"https://magiccode.dpdns.org/" # magiccode(优先使用)
"https://gh-proxy.com/" # gh-proxy 镜像
"https://ghfast.top/" # ghfast 镜像
)
# jsDelivr CDN 专用格式: https://cdn.jsdelivr.net/gh/{user}/{repo}@{branch}/{path}
# 仅适用于 raw.githubusercontent.com 文件下载,不适用于 releases 和 API
JSDELVR_BASE="https://cdn.jsdelivr.net/gh/${REPO}"
# 判断 URL 是否为 raw 文件(可用 jsDelivr 加速)
_is_raw_url() {
[[ "$1" == *"raw.githubusercontent.com"* ]]
}
# 判断 URL 是否为 GitHub API / Release(只能用通用代理)
_is_github_api_or_release() {
[[ "$1" == *".github.com/"* ]] && ! _is_raw_url "$1"
}
# 将 raw.githubusercontent.com URL 转为 jsDelivr CDN URL
_to_jsdelivr_url() {
local url="$1"
# https://raw.githubusercontent.com/magiccode1412/magicmail/main/deploy.sh
# → https://cdn.jsdelivr.net/gh/magiccode1412/magicmail@main/deploy.sh
echo "${url}" | sed -E 's|https?://raw\.githubusercontent\.com/[^/]+/[^/]+/|'"${JSDELVR_BASE}"'@|'
}
# 获取所有可尝试的 URL 列表(原始 + jsDelivr(仅raw) + 各镜像代理)
_get_mirror_urls() {
local url="$1"
# 1. 始终先尝试直连(短超时快速判断)
printf '%s\n' "${url}"
# 2. raw 文件额外提供 jsDelivr CDN 加速
if _is_raw_url "${url}"; then
printf '%s\n' "$(_to_jsdelivr_url "${url}")"
fi
# 3. 通用镜像代理链(适用于 API/Release/raw 全类型)
local prefix
for prefix in "${MIRROR_PREFIXES[@]}"; do
[ -z "${prefix}" ] && continue # 跳过空元素(已用原始 URL)
printf '%s\n' "${prefix}${url}"
done
}
# 单次下载尝试(内部用),成功返回 0,失败返回 1
_download_attempt() {
local url="$1"
local dest="$2"
local timeout="${3:-30}" # 默认超时 30 秒
if command -v curl &>/dev/null; then
curl -fSL --connect-timeout "${timeout}" --max-time 300 \
--progress-bar -o "${dest}" "${url}" 2>/dev/null && return 0
elif command -v wget &>/dev/null; then
wget --timeout="${timeout}" --progress=bar:force:noscroll \
-O "${dest}" "${url}" 2>/dev/null && return 0
fi
return 1
}
# ═══════════════════════════════════════════════════════
# 第二部分:依赖检查与安装模块
# ═══════════════════════════════════════════════════════
# 通过包管理器安装包列表
install_packages() {
local packages=("$@")
if [ ${#packages[@]} -eq 0 ]; then return; fi
local pm
pm="$(get_package_manager)"
case "${pm}" in
apt)
apt-get update -qq && apt-get install -y -qq "${packages[@]}" ;;
dnf)
dnf install -y -q "${packages[@]}" ;;
yum)
yum install -y -q "${packages[@]}" ;;
pacman)
pacman -S --noconfirm "${packages[@]}" ;;
apk)
apk add "${packages[@]}" ;;
zypper)
zypper --non-interactive install "${packages[@]}" ;;
brew)
brew install "${packages[@]}" ;;
*)
die "无法自动安装依赖 (${packages[*]})。未知的包管理器,请手动安装后再试" ;;
esac
}
# 确保下载工具可用 (curl/wget)
ensure_download_tool() {
if ! command -v curl &>/dev/null && ! command -v wget &>/dev/null; then
info "安装下载工具 (curl)..."
install_packages curl || die "无法自动安装 curl/wget,请手动安装后再试"
success "curl 已安装"
fi
}
# 确保必要工具存在
ensure_required_tools() {
local missing=()
for tool in tar gzip; do
if ! command -v "${tool}" &>/dev/null; then
missing+=("${tool}")
fi
done
if [ ${#missing[@]} -gt 0 ]; then
info "安装缺失的工具: ${missing[*]}"
install_packages "${missing[@]}"
success "工具已安装: ${missing[*]}"
fi
}
# 系统资源预检 (磁盘空间 >= 200MB, 内存 >= 256MB)
check_system_resources() {
info "检查系统资源..."
# 磁盘空间检查 (目标安装路径所在分区)
local check_dir="${INSTALL_DIR}"
if [ ! -d "${check_dir}" ]; then
check_dir="/"
fi
local disk_free_kb
disk_free_kb=$(df -k "${check_dir}" 2>/dev/null | awk 'NR==2 {print $4}' || echo "0")
local disk_free_mb=$((disk_free_kb / 1024))
if [ "${disk_free_mb}" -lt 200 ]; then
warn "磁盘剩余空间不足: ${disk_free_mb}MB < 200MB (建议至少 500MB)"
warn "安装可能失败或影响程序正常运行"
else
success "磁盘空间充足: ${disk_free_mb}MB 可用"
fi
# 内存检查
if is_linux && [ -r /proc/meminfo ]; then
local mem_total_kb
mem_total_kb=$(awk '/MemTotal/ {print $2}' /proc/meminfo 2>/dev/null || echo "0")
local mem_total_mb=$((mem_total_kb / 1024))
if [ "${mem_total_mb}" -lt 256 ]; then
warn "内存较小: ${mem_total_mb}MB < 256MB,可能影响性能"
else
success "内存充足: ${mem_total_mb}MB 总计"
fi
fi
# Docker 容器提示
if is_docker; then
IN_DOCKER=true
warn "检测到 Docker 容器环境 — systemd 服务不可用,将使用前台/后台模式运行"
fi
}
# 端口冲突检测
check_port_conflict() {
local port="${1:-${TARGET_PORT}}"
info "检查端口 ${port} 占用情况..."
local occupied=false
local occupier=""
if command -v ss &>/dev/null; then
occupier=$(ss -tlnp 2>/dev/null | grep ":${port}" || true)
elif command -v netstat &>/dev/null; then
occupier=$(netstat -tlnp 2>/dev/null | grep ":${port}" || true)
elif command -v lsof &>/dev/null; then
occupier=$(lsof -i ":${port}" 2>/dev/null || true)
fi
if [ -n "${occupier}" ]; then
occupied=true
warn "端口 ${port} 已被占用:"
echo " ${occupier}" | head -3 | sed 's/^/ /'
warn "如果端口冲突,Magicmail 可能无法正常启动。可通过 --port 参数指定其他端口"
else
success "端口 ${port} 可用"
fi
}
# 防火墙状态检测与提示
check_firewall() {
local port="${1:-${TARGET_PORT}}"
if is_linux; then
if command -v ufw &>/dev/null && ufw status 2>/dev/null | grep -q "active"; then
warn "UFW 防火墙已启用,如需外部访问请执行:"
echo -e " ${DIM}sudo ufw allow ${port}/tcp${NC}"
fi
if command -v firewall-cmd &>/dev/null && firewall-cmd --state &>/dev/null; then
warn "firewalld 防火墙已启用,如需外部访问请执行:"
echo -e " ${DIM}sudo firewall-cmd --add-port=${port}/tcp --permanent && sudo firewall-cmd --reload${NC}"
fi
if command -v iptables &>/dev/null && iptables -L INPUT -n 2>/dev/null | grep -q "DROP\|REJECT"; then
warn "iptables 规则可能限制端口访问,请确认 ${port} 端口已放行"
fi
fi
}
# ═══════════════════════════════════════════════════════
# 第三部分:下载与二进制安装模块
# ═══════════════════════════════════════════════════════
# 获取下载文件名后缀
get_binary_suffix() {
local os="$1"
local arch="$2"
case "${os}-${arch}" in
linux-amd64) echo "" ;;
linux-arm64) echo "-arm64" ;;
darwin-amd64) echo "-macos-x86_64" ;;
darwin-arm64) echo "-macos-arm64" ;;
windows-amd64) echo ".exe" ;;
*) die "无法确定二进制文件名: ${os}/${arch}" ;;
esac
}
# 获取最新版本号(支持镜像自动切换)
get_latest_version() {
# 优先使用本地 git tag(源码部署场景)
if command -v git &>/dev/null && [ -d ".git" ] 2>/dev/null; then
local tag
tag="$(git describe --tags --abbrev=0 2>/dev/null || true)"
if [ -n "${tag}" ]; then
echo "${tag}"
return
fi
fi
# 从 GitHub API 获取,带镜像自动切换
local api_urls
api_urls="$(_get_mirror_urls "${RELEASE_URL}")"
local version=""
local http_code=""
local api_url
while IFS= read -r api_url; do
[ -z "${api_url}" ] && continue
if command -v curl &>/dev/null; then
# 同时提取 HTTP 状态码和响应体(用于区分网络问题 vs 无 Release)
http_code="$(curl -fsSL -o /tmp/magicmail_api_resp.json \
-w '%{http_code}' --connect-timeout 10 --max-time 30 \
"${api_url}" 2>/dev/null || true)"
version="$(grep '"tag_name"' /tmp/magicmail_api_resp.json 2>/dev/null | head -1 \
| sed -E 's/.*"tag_name":\s*"([^"]+)".*/\1/' || true)"
# 404 = 该仓库暂未发布 Release,无需继续尝试镜像
if [ "${http_code}" = "404" ]; then
rm -f /tmp/magicmail_api_resp.json
die "该仓库暂未发布任何 Release(HTTP 404)。
请先在 GitHub 上创建一个 Release: ${REPO_URL}/releases/new
或使用 --version V.x.y.z 手动指定版本"
fi
elif command -v wget &>/dev/null; then
wget --timeout=30 -qO- "${api_url}" > /tmp/magicmail_api_resp.json 2>/dev/null || true
version="$(grep '"tag_name"' /tmp/magicmail_api_resp.json 2>/dev/null | head -1 \
| sed -E 's/.*"tag_name":\s*"([^"]+)".*/\1/' || true)"
fi
if [ -n "${version}" ]; then
rm -f /tmp/magicmail_api_resp.json
echo "${version}"
return
fi
done <<< "${api_urls}"
rm -f /tmp/magicmail_api_resp.json
die "无法获取最新版本号,请检查网络连接或手动指定版本 (--version V.x.y.z)
提示: 国内用户如遇 GitHub 访问问题,脚本会自动切换代理;如全部失败请检查服务器是否能访问外网
Release 页面: ${REPO_URL}/releases"
}
# 通用文件下载(自动切换镜像)
download_file() {
local url="$1"
local dest="$2"
info "正在下载: $(echo "${url}" | sed -E 's|https?://[^/]+/||')"
# 尝试原始 URL(短超时,快速判断直连是否可用)
info "正在使用直连下载..."
if _download_attempt "${url}" "${dest}" 10; then
success "下载成功 (直连)"
return
fi
warn "直连失败,自动切换镜像加速..."
local mirror_urls
mirror_urls="$(_get_mirror_urls "${url}")"
local mirror_url
while IFS= read -r mirror_url; do
# 跳过空行和原始 URL(已试过)
[ -z "${mirror_url}" ] && continue
[ "${mirror_url}" = "${url}" ] && continue
# 提取镜像名称用于日志显示
local mirror_name="${mirror_url%%://*}"
case "${mirror_name}" in
*magiccode.dpdns.org*) mirror_name="magiccode" ;;
*gh-proxy.com*) mirror_name="gh-proxy 镜像" ;;
*ghfast.top*) mirror_name="ghfast 镜像" ;;
*.jsdelivr.net*) mirror_name="jsDelivr CDN" ;;
*) mirror_name="未知源" ;;
esac
info "正在使用 ${mirror_name} 下载..."
if _download_attempt "${mirror_url}" "${dest}" 30; then
success "下载成功 (${mirror_name})"
return
fi
done <<< "${mirror_urls}"
die "所有镜像均下载失败,请检查网络或手动下载后放置到 ${dest}"
}
# 下载并安装二进制文件
install_binary() {
local target_os="$1"
local target_arch="$2"
local version="$3"
local suffix
suffix="$(get_binary_suffix "${target_os}" "${target_arch}")"
local binary_url="${REPO_URL}/releases/download/${version}/${BIN_NAME}${suffix}"
local binary_path="${INSTALL_DIR}/${BIN_NAME}${suffix}"
info "正在下载 Magicmail ${version} (${target_os}/${target_arch})..."
info "下载地址: ${binary_url}"
download_file "${binary_url}" "${binary_path}"
chmod +x "${binary_path}"
# 备份旧版本
local final_bin="${INSTALL_DIR}/${BIN_NAME}"
if [ -f "${final_bin}" ] && [ "${final_bin}" != "${binary_path}" ]; then
mv "${final_bin}" "${final_bin}.bak.$(date +%Y%m%d%H%M%S)" 2>/dev/null || true
fi
# 统一入口处理
if [ "${suffix}" != "" ]; then
ln -sf "$(basename "${binary_path}")" "${final_bin}"
fi
success "二进制已安装: ${final_bin}"
echo "${final_bin}"
}
# 获取已安装的版本号 (从二进制或记录文件)
get_installed_version() {
local bin="${INSTALL_DIR}/${BIN_NAME}"
if [ -x "${bin}" ]; then
# 尝试从文件名/元数据获取(Go 二进制无 --version 时用备用方案)
local ver_file="${INSTALL_DIR}/.version"
if [ -f "${ver_file}" ]; then
cat "${ver_file}"
else
echo "installed"
fi
else
echo ""
fi
}
# ═══════════════════════════════════════════════════════
# 第四部分:目录与权限初始化
# ═══════════════════════════════════════════════════════
init_directories() {
mkdir -p "${INSTALL_DIR}"
mkdir -p "${DATA_DIR}/data"
mkdir -p "$(dirname "${LOG_FILE}")"
# 复制部署脚本到安装目录(供 CLI wrapper 使用)
cp -f "$0" "${INSTALL_DIR}/deploy.sh" 2>/dev/null || true
chmod +x "${INSTALL_DIR}/deploy.sh" 2>/dev/null || true
success "目录已创建: ${INSTALL_DIR}, ${DATA_DIR}"
}
# ─── 权限检查 ─────────────────────────────────────
check_root() {
if [[ $EUID -ne 0 ]]; then
error "此操作需要 root 权限,请使用 sudo 运行:"
echo -e " ${DIM}sudo $0 $*${NC}"
exit 1
fi
}
# ═══════════════════════════════════════════════════════
# 第五部分:服务管理模块
# ═══════════════════════════════════════════════════════
# 创建 Linux systemd 服务文件
create_systemd_service() {
local service_file="/etc/systemd/system/${SERVICE_NAME}.service"
cat > "${service_file}" << SERVICEEOF
[Unit]
Description=Magicmail Mail Management Service
Documentation=https://github.com/magiccode1412/magicmail
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=${INSTALL_DIR}/${BIN_NAME}
WorkingDirectory=${DATA_DIR}
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=60
StartLimitBurst=5
# 环境变量配置
Environment=MAGICMAIL_ENV=production
Environment=MAGICMAIL_PORT=${TARGET_PORT}
Environment=MAGICMAIL_HOST=0.0.0.0
Environment=MAGICMAIL_DSN=${DATA_DIR}/data/magicmail.db
# 日志输出
StandardOutput=append:${LOG_FILE}
StandardError=append:${LOG_FILE}
# 安全加固
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=${DATA_DIR}
PrivateTmp=true
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
SERVICEEOF
systemctl daemon-reload
systemctl enable "${SERVICE_NAME}"
success "systemd 服务已创建: ${service_file}"
}
# 创建 macOS LaunchDaemon 服务
create_launchd_service() {
local plist_file="/Library/LaunchDaemons/com.magicmail.service.plist"
cat > "${plist_file}" << PLISTEOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.magicmail.service</string>
<key>ProgramArguments</key>
<array>
<string>${INSTALL_DIR}/${BIN_NAME}</string>
</array>
<key>WorkingDirectory</key>
<string>${DATA_DIR}</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>${LOG_FILE}</string>
<key>StandardErrorPath</key>
<string>${LOG_FILE}</string>
<key>EnvironmentVariables</key>
<dict>
<key>MAGICMAIL_ENV</key>
<string>production</string>
<key>MAGICMAIL_PORT</key>
<string>${TARGET_PORT}</string>
<key>MAGICMAIL_HOST</key>
<string>0.0.0.0</string>
<key>MAGICMAIL_DSN</key>
<string>${DATA_DIR}/data/magicmail.db</string>
</dict>
</dict>
</plist>
PLISTEOF
launchctl load "${plist_file}" 2>/dev/null || true
success "LaunchDaemon 服务已创建: ${plist_file}"
}
# 启动服务
svc_start() {
check_root start
if is_docker; then
# Docker 内直接后台运行
if pgrep -f "${INSTALL_DIR}/${BIN_NAME}" &>/dev/null; then
warn "进程已在运行中"
else
cd "${DATA_DIR}" && MAGICMAIL_ENV=production MAGICMAIL_PORT="${TARGET_PORT}" \
nohup "${INSTALL_DIR}/${BIN_NAME}" >> "${LOG_FILE}" 2>&1 &
sleep 1
success "已在 Docker 中启动 (PID: $!)"
fi
svc_status
return
fi
if is_linux; then
systemctl start "${SERVICE_NAME}" || die "启动失败"
sleep 1
else
launchctl start "com.magicmail.service" 2>/dev/null \
|| (cd "${DATA_DIR}" && "${INSTALL_DIR}/${BIN_NAME}" &)
sleep 1
fi
svc_status
success "服务已启动"
}
# 停止服务
svc_stop() {
check_root stop
if is_docker; then
pkill -f "${INSTALL_DIR}/${BIN_NAME}" 2>/dev/null || true
success "Docker 进程已停止"
return
fi
if is_linux; then
systemctl stop "${SERVICE_NAME}" 2>/dev/null || warn "服务可能未在运行"
else
launchctl stop "com.magicmail.service" 2>/dev/null \
|| pkill -f "${INSTALL_DIR}/${BIN_NAME}" 2>/dev/null || true
fi
success "服务已停止"
}
# 重启服务
svc_restart() {
check_root restart
if is_docker; then
svc_stop
sleep 1
svc_start
return
fi
if is_linux; then
systemctl restart "${SERVICE_NAME}" || die "重启失败"
else
svc_stop
sleep 1
svc_start
fi
success "服务已重启"
}
# 服务状态查看
svc_status() {
local running=false
if is_linux && ! is_docker; then
if systemctl is-active "${SERVICE_NAME}" &>/dev/null; then
running=true
echo -e "${GREEN}● 服务状态: 运行中${NC}"
systemctl status "${SERVICE_NAME}" --no-pager -l 2>/dev/null | head -12
else
echo -e "${RED}○ 服务状态: 未运行${NC}"
# 显示失败原因
systemctl status "${SERVICE_NAME}" --no-pager -l 2>/dev/null | grep -E "Active:|Main PID:" | head -3 | sed 's/^/ /'
fi
elif is_docker; then
if pgrep -f "${INSTALL_DIR}/${BIN_NAME}" &>/dev/null; then
running=true
echo -e "${GREEN}● 服务状态: 运行中 (Docker模式)${NC}"
ps aux | grep "[${BIN_NAME:0:1}]${BIN_NAME:1}" | head -3 | sed 's/^/ /'
else
echo -e "${RED}○ 服务状态: 未运行${NC}"
fi
else
if pgrep -f "${INSTALL_DIR}/${BIN_NAME}" &>/dev/null; then
running=true
echo -e "${GREEN}● 服务状态: 运行中${NC}"
ps aux | grep "[${BIN_NAME:0:1}]${BIN_NAME:1}" | head -5 | sed 's/^/ /'
else
echo -e "${RED}○ 服务状态: 未运行${NC}"
fi
fi
echo ""
# 端口监听信息
local listen_port="${TARGET_PORT}"
local port_info=""
if command -v ss &>/dev/null; then
port_info="$(ss -tlnp 2>/dev/null | grep ":${listen_port}" || true)"
elif command -v netstat &>/dev/null; then
port_info="$(netstat -tlnp 2>/dev/null | grep ":${listen_port}" || true)"
elif command -v lsof &>/dev/null; then
if lsof -i ":${listen_port}" &>/dev/null; then
port_info="(listening)"
fi
fi
if [ -n "${port_info}" ] || ${running}; then
info "端口监听: ${listen_port}"
else
warn "端口 ${listen_port}: 未监听"
fi
# 关键路径信息
echo ""
info "访问地址: http://localhost:${listen_port}"
info "数据目录: ${DATA_DIR}"
info "日志文件: ${LOG_FILE}"
info "配置目录: ${CONFIG_DIR}"
# 版本信息
local installed_ver
installed_ver="$(get_installed_version 2>/dev/null || echo '?')"
if [ -n "${installed_ver}" ]; then
info "已装版本: ${installed_ver}"
fi
}
# 查看日志
svc_logs() {
local lines="${1:-100}"
if [ -f "${LOG_FILE}" ]; then
tail -"${lines}" "${LOG_FILE}" 2>/dev/null || warn "无法读取日志"
elif is_linux && ! is_docker; then
journalctl -u "${SERVICE_NAME}" --no-pager -n "${lines}" 2>/dev/null \
|| warn "无日志可读 (journalctl)"
else
warn "日志文件不存在: ${LOG_FILE}"
fi
}
# ═══════════════════════════════════════════════════════
# 第六部分:CLI Wrapper 生成模块
# ═══════════════════════════════════════════════════════
# 创建 /usr/local/bin/magicmail CLI 命令(自包含,不依赖 deploy.sh)
create_cli_wrapper() {
local wrapper="${CLI_BIN}"
cat > "${wrapper}" << 'WRAPPEREOF'
#!/usr/bin/env bash
# ============================================================================
# Magicmail CLI - 自包含命令行工具
# 用法: magicmail [命令] [选项]
# 注意: 此文件由 install 自动生成,基础操作不依赖 deploy.sh
# ============================================================================
set -euo pipefail
# ─── 配置 ──────────────────────────────────────────
MAGICMAIL_INSTALL_DIR="/opt/magicmail"
MAGICMAIL_DATA_DIR="/var/lib/magicmail"
MAGICMAIL_LOG_FILE="/var/log/magicmail.log"
MAGICMAIL_SERVICE="magicmail"
DEPLOY_SCRIPT="${MAGICMAIL_INSTALL_DIR}/deploy.sh"
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
# ─── 工具函数 ──────────────────────────────────────
_detect_os() {
case "$(uname -s)" in
Linux) echo "linux" ;;
Darwin) echo "macos" ;;
*) echo "other" ;;
esac
}
_is_docker() {
[ -f /.dockerenv ] 2>/dev/null || grep -qi 'docker\|lxc' /proc/1/cgroup 2>/dev/null
}
_is_linux() { [ "$(_detect_os)" = "linux" ]; }
_is_macos() { [ "$(_detect_os)" = "macos" ]; }
_ensure_root() {
if [ $EUID -ne 0 ]; then
exec sudo "$0" "$@"
fi
}
# 检查 deploy.sh 是否可用,不可用时给出友好提示
_check_deploy_script() {
if [ ! -x "${DEPLOY_SCRIPT}" ] && [ ! -f "${DEPLOY_SCRIPT}" ]; then
error "部署脚本不存在: ${DEPLOY_SCRIPT}"
echo ""
warn "此功能需要部署脚本支持,请重新下载:"
echo " curl -fsSL https://raw.githubusercontent.com/magiccode1412/magicmail/main/deploy.sh -o ${DEPLOY_SCRIPT}"
echo " chmod +x ${DEPLOY_SCRIPT}"
echo ""
echo "或者直接执行:"
echo " curl -fsSL https://raw.githubusercontent.com/magiccode1412/magicmail/main/deploy.sh | sudo bash -s -- $*"
exit 1
fi
}
show_help() {
echo ""
echo -e "${CYAN}${BOLD}Magicmail 魔法邮箱 - 命令行工具${NC}"
echo ""
echo "用法: magicmail <命令> [选项]"
echo ""
echo "命令:"
echo " status 查看服务运行状态"
echo " start 启动服务"
echo " stop 停止服务"
echo " restart 重启服务"
echo " update 更新到最新版本 (需要部署脚本)"
echo " logs [N] 查看最近 N 行日志 (默认100)"
echo " version 显示版本信息"
echo " doctor 环境健康自检 (需要部署脚本)"
echo " uninstall 卸载程序 (需要部署脚本)"
echo " update-script 更新部署脚本到最新版"
echo " help, -h 显示此帮助"
echo ""
echo "选项:"
echo " -y, --yes 跳过确认提示"
echo ""
echo "示例:"
echo " magicmail status # 查看状态"
echo " magicmail logs 50 # 查看50行日志"
echo " magicmail update -y # 无交互更新"
echo ""
}
# ─── 状态查询(自包含)──────────────────────────────
_cmd_status() {
local bin="${MAGICMAIL_INSTALL_DIR}/magicmail"
local running="否"