统一 frontend、backend、test、test-log 和 out 目录,更新构建、部署及测试入口;纳入配置管理兼容与首次安全引导修复;排除本机配置、密钥和测试产物。
This commit is contained in:
parent
28cd9d3406
commit
f60d3b0880
77
.github/workflows/release.yml
vendored
77
.github/workflows/release.yml
vendored
@ -55,21 +55,21 @@ jobs:
|
||||
echo "SPAMBLOCKFILENAME=spam_block_${{ matrix.goos }}_${{ matrix.goarch }}.exe" >> ${GITHUB_ENV}
|
||||
echo "TELEGRAMFILENAME=pmail_telegram_push_${{ matrix.goos }}_${{ matrix.goarch }}.exe" >> ${GITHUB_ENV}
|
||||
- name: FE Build
|
||||
run: cd fe && yarn install --frozen-lockfile && yarn build
|
||||
run: cd frontend && yarn install --frozen-lockfile && yarn build
|
||||
- name: BE Build
|
||||
run: |
|
||||
cd server
|
||||
cp -rf ../fe/dist listen/http_server
|
||||
mkdir -p out/release/plugins
|
||||
cd backend
|
||||
BUILD_FLAGS="-s -w -X 'git.pcux.cn/xuans/Smail/internal/buildinfo.Version=${{ env.VERSION }}' -X 'git.pcux.cn/xuans/Smail/internal/buildinfo.GoVersion=$(go version)' -X 'git.pcux.cn/xuans/Smail/internal/buildinfo.GitHash=$(git show -s --format=%H)' -X 'git.pcux.cn/xuans/Smail/internal/buildinfo.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)'"
|
||||
go build -ldflags "$BUILD_FLAGS" -o ${{ env.FILENAME }} ./cmd/smail
|
||||
go build -ldflags "$BUILD_FLAGS" -o ${{ env.ADMINFILENAME }} ./cmd/smail-admin
|
||||
go build -ldflags "-s -w" -o ${{ env.WCFILENAME }} hooks/wechat_push/wechat_push.go
|
||||
go build -ldflags "-s -w" -o ${{ env.SPAMBLOCKFILENAME }} hooks/spam_block/spam_block.go
|
||||
go build -ldflags "$BUILD_FLAGS" -o ../out/release/${{ env.FILENAME }} ./cmd/smail
|
||||
go build -ldflags "$BUILD_FLAGS" -o ../out/release/${{ env.ADMINFILENAME }} ./cmd/smail-admin
|
||||
go build -ldflags "-s -w" -o ../out/release/plugins/${{ env.WCFILENAME }} hooks/wechat_push/wechat_push.go
|
||||
go build -ldflags "-s -w" -o ../out/release/plugins/${{ env.SPAMBLOCKFILENAME }} hooks/spam_block/spam_block.go
|
||||
cd ../plugins/pmail_telegram_push
|
||||
npm ci
|
||||
npm run build-only
|
||||
go build -ldflags "-s -w" -o ../../server/${{ env.TELEGRAMFILENAME }} .
|
||||
cd ../../server
|
||||
go build -ldflags "-s -w" -o ../../out/release/plugins/${{ env.TELEGRAMFILENAME }} .
|
||||
cd ../../out/release
|
||||
cat > release.json <<EOF
|
||||
{
|
||||
"version": "${{ env.VERSION }}",
|
||||
@ -78,27 +78,23 @@ jobs:
|
||||
"createdAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
}
|
||||
EOF
|
||||
cp ../README.md README.md
|
||||
cp ../../README.md README.md
|
||||
ls -alh
|
||||
- name: Zip
|
||||
run: |
|
||||
cd ./server
|
||||
mkdir plugins
|
||||
mv ${{ env.WCFILENAME }} plugins/
|
||||
mv ${{ env.SPAMBLOCKFILENAME }} plugins/
|
||||
mv ${{ env.TELEGRAMFILENAME }} plugins/
|
||||
cd ./out/release
|
||||
zip -r ${{ env.ZIPNAME }}.zip ${{ env.FILENAME }} ${{ env.ADMINFILENAME }} plugins release.json README.md
|
||||
ls
|
||||
- name: Upload files to Artifacts
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: ${{ env.ZIPNAME }}
|
||||
path: server/${{ env.ZIPNAME }}.zip
|
||||
path: out/release/${{ env.ZIPNAME }}.zip
|
||||
- name: Upload binaries to release
|
||||
uses: svenstaro/upload-release-action@29e53e917877a24fad85510ded594ab3c9ca12de
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: server/${{ env.ZIPNAME }}.zip
|
||||
file: out/release/${{ env.ZIPNAME }}.zip
|
||||
tag: ${{ github.ref }}
|
||||
file_glob: true
|
||||
|
||||
@ -113,32 +109,35 @@ jobs:
|
||||
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff
|
||||
with:
|
||||
go-version: '1.26.6'
|
||||
- name: Verify Go dependencies
|
||||
run: cd server && go mod verify
|
||||
- name: Run Go tests
|
||||
run: cd server && go test ./...
|
||||
- name: Run Go static analysis
|
||||
run: cd server && go vet ./...
|
||||
- name: Run Go race tests
|
||||
run: cd server && go test -race ./...
|
||||
- name: Run Go security analysis
|
||||
run: |
|
||||
go install github.com/securego/gosec/v2/cmd/gosec@v2.22.10
|
||||
cd server && gosec ./...
|
||||
- name: Scan Go vulnerabilities
|
||||
run: |
|
||||
go install golang.org/x/vuln/cmd/govulncheck@v1.7.0
|
||||
cd server && govulncheck ./...
|
||||
- name: Setup Node.js environment
|
||||
- name: Setup Node.js for test runner
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: 24
|
||||
- name: Install Yarn
|
||||
run: npm install --global yarn@1.22.22
|
||||
- name: Verify frontend dependencies and build
|
||||
- name: Prepare frontend and Go embedded assets
|
||||
run: |
|
||||
cd fe
|
||||
npm install --global yarn@1.22.22
|
||||
cd frontend
|
||||
yarn install --frozen-lockfile
|
||||
yarn test
|
||||
yarn build
|
||||
- name: Verify Go dependencies
|
||||
run: cd backend && go mod verify
|
||||
- name: Run Go tests
|
||||
run: node test/run.mjs backend
|
||||
- name: Run Go static analysis
|
||||
run: cd backend && go vet ./...
|
||||
- name: Run Go race tests
|
||||
run: node test/run.mjs backend -race ./...
|
||||
- name: Run Go security analysis
|
||||
run: |
|
||||
go install github.com/securego/gosec/v2/cmd/gosec@v2.22.10
|
||||
cd backend && gosec ./...
|
||||
- name: Scan Go vulnerabilities
|
||||
run: |
|
||||
go install golang.org/x/vuln/cmd/govulncheck@v1.7.0
|
||||
cd backend && govulncheck ./...
|
||||
- name: Verify frontend dependencies and lint
|
||||
run: |
|
||||
cd frontend
|
||||
yarn audit --groups dependencies
|
||||
yarn eslint . --ext .vue,.js,.jsx,.cjs,.mjs --ignore-path .gitignore
|
||||
yarn build
|
||||
|
||||
47
.github/workflows/security.yml
vendored
47
.github/workflows/security.yml
vendored
@ -26,32 +26,35 @@ jobs:
|
||||
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff
|
||||
with:
|
||||
go-version: '1.26.6'
|
||||
- name: Verify Go dependencies
|
||||
run: cd server && go mod verify
|
||||
- name: Run Go tests
|
||||
run: cd server && go test ./...
|
||||
- name: Run Go static analysis
|
||||
run: cd server && go vet ./...
|
||||
- name: Run Go race tests
|
||||
run: cd server && go test -race ./...
|
||||
- name: Run Go security analysis
|
||||
run: |
|
||||
go install github.com/securego/gosec/v2/cmd/gosec@v2.22.10
|
||||
cd server && gosec ./...
|
||||
- name: Scan Go vulnerabilities
|
||||
run: |
|
||||
go install golang.org/x/vuln/cmd/govulncheck@v1.7.0
|
||||
cd server && govulncheck ./...
|
||||
- name: Setup Node.js environment
|
||||
- name: Setup Node.js for test runner
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: 24
|
||||
- name: Install Yarn
|
||||
run: npm install --global yarn@1.22.22
|
||||
- name: Verify frontend dependencies and build
|
||||
- name: Prepare frontend and Go embedded assets
|
||||
run: |
|
||||
cd fe
|
||||
npm install --global yarn@1.22.22
|
||||
cd frontend
|
||||
yarn install --frozen-lockfile
|
||||
yarn test
|
||||
yarn build
|
||||
- name: Verify Go dependencies
|
||||
run: cd backend && go mod verify
|
||||
- name: Run Go tests
|
||||
run: node test/run.mjs backend
|
||||
- name: Run Go static analysis
|
||||
run: cd backend && go vet ./...
|
||||
- name: Run Go race tests
|
||||
run: node test/run.mjs backend -race ./...
|
||||
- name: Run Go security analysis
|
||||
run: |
|
||||
go install github.com/securego/gosec/v2/cmd/gosec@v2.22.10
|
||||
cd backend && gosec ./...
|
||||
- name: Scan Go vulnerabilities
|
||||
run: |
|
||||
go install golang.org/x/vuln/cmd/govulncheck@v1.7.0
|
||||
cd backend && govulncheck ./...
|
||||
- name: Verify frontend dependencies and lint
|
||||
run: |
|
||||
cd frontend
|
||||
yarn audit --groups dependencies
|
||||
yarn eslint . --ext .vue,.js,.jsx,.cjs,.mjs --ignore-path .gitignore
|
||||
yarn build
|
||||
|
||||
70
.gitignore
vendored
70
.gitignore
vendored
@ -3,42 +3,60 @@
|
||||
dist
|
||||
output
|
||||
smail.db
|
||||
server/plugins/*
|
||||
!server/plugins/
|
||||
!server/plugins/catalog/
|
||||
!server/plugins/catalog/*.go
|
||||
backend/plugins/*
|
||||
!backend/plugins/
|
||||
!backend/plugins/catalog/
|
||||
!backend/plugins/catalog/*.go
|
||||
config
|
||||
*_KEY
|
||||
AURORA_SECRET
|
||||
.codex-edit-config.py
|
||||
server/gosec-report
|
||||
server/gosec-g104-current.json
|
||||
server/spam_block.exe
|
||||
server/wechat_push.exe
|
||||
server/utils/validation/test.tmp
|
||||
server/.codex-smail-*
|
||||
server/.tmp-*
|
||||
backend/gosec-report
|
||||
backend/gosec-g104-current.json
|
||||
backend/spam_block.exe
|
||||
backend/wechat_push.exe
|
||||
backend/utils/validation/test.tmp
|
||||
backend/.codex-smail-*
|
||||
backend/.tmp-*
|
||||
|
||||
!server/config/
|
||||
!server/config/*.go
|
||||
!backend/config/
|
||||
backend/config/*
|
||||
!backend/config/*.go
|
||||
!backend/config/dkim/
|
||||
backend/config/dkim/*
|
||||
!backend/config/dkim/README.md
|
||||
!backend/config/ssl/
|
||||
backend/config/ssl/*
|
||||
!backend/config/ssl/README.md
|
||||
.codex-cg*.patch
|
||||
server/config/credential.key
|
||||
server/smail.exe
|
||||
server/smail-admin.exe
|
||||
backend/config/credential.key
|
||||
backend/smail.exe
|
||||
backend/smail-admin.exe
|
||||
.codex-smail-*
|
||||
|
||||
# 本地测试输出和临时文件
|
||||
/out/
|
||||
/test-log/
|
||||
/test/archive/
|
||||
artifacts/
|
||||
ssl/
|
||||
/ssl/
|
||||
.tmp-*
|
||||
*.log
|
||||
*.pid
|
||||
server/smail_*
|
||||
server/smail-admin_*
|
||||
server/*.exe
|
||||
server/config/smail_temp.db
|
||||
server/config/external-email-*.key
|
||||
server/config/ssl/*.crt
|
||||
server/config/ssl/*.key
|
||||
server/config/ssl/*.csr
|
||||
server/config/dkim/backup-*/
|
||||
backend/smail_*
|
||||
backend/smail-admin_*
|
||||
backend/*.exe
|
||||
backend/config/smail_temp.db
|
||||
backend/config/external-email-*.key
|
||||
backend/config/ssl/*.crt
|
||||
backend/config/ssl/*.key
|
||||
backend/config/ssl/*.csr
|
||||
backend/config/dkim/backup-*/
|
||||
|
||||
# 配置迁移生成的本地分区和原始备份,不进入版本库
|
||||
backend/config/config.json.legacy
|
||||
backend/config/smtp.json
|
||||
backend/config/email.json
|
||||
backend/config/ssl.json
|
||||
backend/config/plugins.json
|
||||
backend/config/migration-backups/
|
||||
|
||||
98
Makefile
98
Makefile
@ -9,77 +9,71 @@ BUILD_LDFLAGS = -s -w -X 'git.pcux.cn/xuans/Smail/internal/buildinfo.Version=$(S
|
||||
build: build_fe build_server spam_block wechat_push telegram_push package
|
||||
|
||||
clean:
|
||||
rm -rf output
|
||||
@echo "请先审查 test-log 中的清理清单,不自动递归删除文件。"
|
||||
|
||||
clean_codex_test_artifacts:
|
||||
cd server && git clean -f -x -- .codex-smail-*
|
||||
@echo "测试产物保留在 test-log 中,清理前请先审查。"
|
||||
|
||||
build_fe:
|
||||
cd fe && yarn && yarn build
|
||||
rm -rf server/listen/http_server/dist
|
||||
cd server && cp -rf ../fe/dist listen/http_server
|
||||
cd frontend && yarn && yarn build
|
||||
|
||||
build_server:
|
||||
cd server && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(BUILD_LDFLAGS)" -o smail_linux_amd64 ./cmd/smail
|
||||
cd server && CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "$(BUILD_LDFLAGS)" -o smail_linux_arm64 ./cmd/smail
|
||||
cd server && CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "$(BUILD_LDFLAGS)" -o smail_windows_amd64.exe ./cmd/smail
|
||||
cd server && CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags "$(BUILD_LDFLAGS)" -o smail_windows_arm64.exe ./cmd/smail
|
||||
cd server && CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "$(BUILD_LDFLAGS)" -o smail_mac_amd64 ./cmd/smail
|
||||
cd server && CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "$(BUILD_LDFLAGS)" -o smail_mac_arm64 ./cmd/smail
|
||||
cd server && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(BUILD_LDFLAGS)" -o smail-admin_linux_amd64 ./cmd/smail-admin
|
||||
cd server && CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "$(BUILD_LDFLAGS)" -o smail-admin_linux_arm64 ./cmd/smail-admin
|
||||
cd server && CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "$(BUILD_LDFLAGS)" -o smail-admin_windows_amd64.exe ./cmd/smail-admin
|
||||
cd server && CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags "$(BUILD_LDFLAGS)" -o smail-admin_windows_arm64.exe ./cmd/smail-admin
|
||||
cd server && CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "$(BUILD_LDFLAGS)" -o smail-admin_mac_amd64 ./cmd/smail-admin
|
||||
cd server && CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "$(BUILD_LDFLAGS)" -o smail-admin_mac_arm64 ./cmd/smail-admin
|
||||
mkdir -p out/backend
|
||||
cd backend && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(BUILD_LDFLAGS)" -o ../out/backend/smail_linux_amd64 ./cmd/smail
|
||||
cd backend && CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "$(BUILD_LDFLAGS)" -o ../out/backend/smail_linux_arm64 ./cmd/smail
|
||||
cd backend && CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "$(BUILD_LDFLAGS)" -o ../out/backend/smail_windows_amd64.exe ./cmd/smail
|
||||
cd backend && CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags "$(BUILD_LDFLAGS)" -o ../out/backend/smail_windows_arm64.exe ./cmd/smail
|
||||
cd backend && CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "$(BUILD_LDFLAGS)" -o ../out/backend/smail_mac_amd64 ./cmd/smail
|
||||
cd backend && CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "$(BUILD_LDFLAGS)" -o ../out/backend/smail_mac_arm64 ./cmd/smail
|
||||
cd backend && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(BUILD_LDFLAGS)" -o ../out/backend/smail-admin_linux_amd64 ./cmd/smail-admin
|
||||
cd backend && CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "$(BUILD_LDFLAGS)" -o ../out/backend/smail-admin_linux_arm64 ./cmd/smail-admin
|
||||
cd backend && CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "$(BUILD_LDFLAGS)" -o ../out/backend/smail-admin_windows_amd64.exe ./cmd/smail-admin
|
||||
cd backend && CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags "$(BUILD_LDFLAGS)" -o ../out/backend/smail-admin_windows_arm64.exe ./cmd/smail-admin
|
||||
cd backend && CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "$(BUILD_LDFLAGS)" -o ../out/backend/smail-admin_mac_amd64 ./cmd/smail-admin
|
||||
cd backend && CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "$(BUILD_LDFLAGS)" -o ../out/backend/smail-admin_mac_arm64 ./cmd/smail-admin
|
||||
|
||||
wechat_push:
|
||||
cd server/hooks/wechat_push && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o output/wechat_push_linux_amd64 wechat_push.go
|
||||
cd server/hooks/wechat_push && CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w" -o output/wechat_push_linux_arm64 wechat_push.go
|
||||
cd server/hooks/wechat_push && CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -o output/wechat_push_windows_amd64.exe wechat_push.go
|
||||
cd server/hooks/wechat_push && CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags "-s -w" -o output/wechat_push_windows_arm64.exe wechat_push.go
|
||||
cd server/hooks/wechat_push && CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w" -o output/wechat_push_mac_amd64 wechat_push.go
|
||||
cd server/hooks/wechat_push && CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "-s -w" -o output/wechat_push_mac_arm64 wechat_push.go
|
||||
mkdir -p out/plugins
|
||||
cd backend/hooks/wechat_push && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o ../../../out/plugins/wechat_push_linux_amd64 wechat_push.go
|
||||
cd backend/hooks/wechat_push && CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w" -o ../../../out/plugins/wechat_push_linux_arm64 wechat_push.go
|
||||
cd backend/hooks/wechat_push && CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -o ../../../out/plugins/wechat_push_windows_amd64.exe wechat_push.go
|
||||
cd backend/hooks/wechat_push && CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags "-s -w" -o ../../../out/plugins/wechat_push_windows_arm64.exe wechat_push.go
|
||||
cd backend/hooks/wechat_push && CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w" -o ../../../out/plugins/wechat_push_mac_amd64 wechat_push.go
|
||||
cd backend/hooks/wechat_push && CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "-s -w" -o ../../../out/plugins/wechat_push_mac_arm64 wechat_push.go
|
||||
|
||||
spam_block:
|
||||
cd server/hooks/spam_block && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o output/spam_block_linux_amd64 spam_block.go
|
||||
cd server/hooks/spam_block && CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w" -o output/spam_block_linux_arm64 spam_block.go
|
||||
cd server/hooks/spam_block && CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -o output/spam_block_windows_amd64.exe spam_block.go
|
||||
cd server/hooks/spam_block && CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags "-s -w" -o output/spam_block_windows_arm64.exe spam_block.go
|
||||
cd server/hooks/spam_block && CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w" -o output/spam_block_mac_amd64 spam_block.go
|
||||
cd server/hooks/spam_block && CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "-s -w" -o output/spam_block_mac_arm64 spam_block.go
|
||||
mkdir -p out/plugins
|
||||
cd backend/hooks/spam_block && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o ../../../out/plugins/spam_block_linux_amd64 spam_block.go
|
||||
cd backend/hooks/spam_block && CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w" -o ../../../out/plugins/spam_block_linux_arm64 spam_block.go
|
||||
cd backend/hooks/spam_block && CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -o ../../../out/plugins/spam_block_windows_amd64.exe spam_block.go
|
||||
cd backend/hooks/spam_block && CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags "-s -w" -o ../../../out/plugins/spam_block_windows_arm64.exe spam_block.go
|
||||
cd backend/hooks/spam_block && CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w" -o ../../../out/plugins/spam_block_mac_amd64 spam_block.go
|
||||
cd backend/hooks/spam_block && CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "-s -w" -o ../../../out/plugins/spam_block_mac_arm64 spam_block.go
|
||||
|
||||
telegram_push:
|
||||
mkdir -p out/plugins
|
||||
cd plugins/pmail_telegram_push && npm ci && npm run build
|
||||
cd plugins/pmail_telegram_push && mkdir -p output && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o output/pmail_telegram_push_linux_amd64 .
|
||||
cd plugins/pmail_telegram_push && CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w" -o output/pmail_telegram_push_linux_arm64 .
|
||||
cd plugins/pmail_telegram_push && CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -o output/pmail_telegram_push_windows_amd64.exe .
|
||||
cd plugins/pmail_telegram_push && CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags "-s -w" -o output/pmail_telegram_push_windows_arm64.exe .
|
||||
cd plugins/pmail_telegram_push && CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w" -o output/pmail_telegram_push_mac_amd64 .
|
||||
cd plugins/pmail_telegram_push && CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "-s -w" -o output/pmail_telegram_push_mac_arm64 .
|
||||
cd plugins/pmail_telegram_push && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o ../../out/plugins/pmail_telegram_push_linux_amd64 .
|
||||
cd plugins/pmail_telegram_push && CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w" -o ../../out/plugins/pmail_telegram_push_linux_arm64 .
|
||||
cd plugins/pmail_telegram_push && CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -o ../../out/plugins/pmail_telegram_push_windows_amd64.exe .
|
||||
cd plugins/pmail_telegram_push && CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags "-s -w" -o ../../out/plugins/pmail_telegram_push_windows_arm64.exe .
|
||||
cd plugins/pmail_telegram_push && CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w" -o ../../out/plugins/pmail_telegram_push_mac_amd64 .
|
||||
cd plugins/pmail_telegram_push && CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "-s -w" -o ../../out/plugins/pmail_telegram_push_mac_arm64 .
|
||||
|
||||
plugin: spam_block wechat_push telegram_push
|
||||
|
||||
package: clean
|
||||
mkdir output
|
||||
mkdir output/config
|
||||
mkdir output/plugins
|
||||
mv server/smail_linux_amd64 server/smail_linux_arm64 server/smail_windows_amd64.exe server/smail_windows_arm64.exe server/smail_mac_amd64 server/smail_mac_arm64 output/
|
||||
mv server/smail-admin_linux_amd64 server/smail-admin_linux_arm64 server/smail-admin_windows_amd64.exe server/smail-admin_windows_arm64.exe server/smail-admin_mac_amd64 server/smail-admin_mac_arm64 output/
|
||||
mkdir -p output/config/dkim && cp server/config/dkim/dkim.public output/config/dkim/ 2>/dev/null || true
|
||||
cp -r server/config/ssl output/config/
|
||||
cp -r server/config/config.json output/config/
|
||||
mv server/hooks/spam_block/output/* output/plugins
|
||||
mv server/hooks/wechat_push/output/* output/plugins
|
||||
mv plugins/pmail_telegram_push/output/* output/plugins
|
||||
cp README.md output/
|
||||
printf '{\n "version": "%s",\n "gitHash": "%s",\n "protocolVersion": 1,\n "createdAt": "%s"\n}\n' "$(SMAIL_VERSION)" "$(GIT_HASH)" "$(BUILD_TIME)" > output/release.json
|
||||
package:
|
||||
mkdir -p out/package/config out/package/plugins
|
||||
cp out/backend/smail* out/package/
|
||||
cp out/plugins/*_* out/package/plugins/
|
||||
cp README.md out/package/
|
||||
printf '{\n "version": "%s",\n "gitHash": "%s",\n "protocolVersion": 1,\n "createdAt": "%s"\n}\n' "$(SMAIL_VERSION)" "$(GIT_HASH)" "$(BUILD_TIME)" > out/package/release.json
|
||||
|
||||
test:
|
||||
@trap 'cd "$(CURDIR)/server" && git clean -f -x -- .codex-smail-*' EXIT; export setup_port=17888 && cd server && export SMAIL_ROOT=$(CURDIR)/server/ && go test -v -p 1 ./...
|
||||
node test/run.mjs all
|
||||
|
||||
test_mysql:
|
||||
@trap 'cd "$(CURDIR)/server" && git clean -f -x -- .codex-smail-*' EXIT; export setup_port=17888 && cd server && export SMAIL_ROOT=$(CURDIR)/server/ && go test -args "mysql" -v -p 1 ./...
|
||||
node test/run.mjs backend -v -p 1 ./... -args mysql
|
||||
|
||||
test_postgres:
|
||||
@trap 'cd "$(CURDIR)/server" && git clean -f -x -- .codex-smail-*' EXIT; export setup_port=17888 && cd server && export SMAIL_ROOT=$(CURDIR)/server/ && go test -args "postgres" -v -p 1 ./...
|
||||
node test/run.mjs backend -v -p 1 ./... -args postgres
|
||||
|
||||
10
README.md
10
README.md
@ -115,9 +115,9 @@ IMAP端口: 993(SSL)
|
||||
|
||||
# 插件
|
||||
|
||||
[微信推送](server/hooks/wechat_push/README.md)
|
||||
[微信推送](backend/hooks/wechat_push/README.md)
|
||||
|
||||
[垃圾邮件屏蔽](server/hooks/spam_block/README.md)
|
||||
[垃圾邮件屏蔽](backend/hooks/spam_block/README.md)
|
||||
|
||||
# 其他优秀开源插件
|
||||
|
||||
@ -131,15 +131,17 @@ IMAP端口: 993(SSL)
|
||||
|
||||
# 参与开发
|
||||
|
||||
目录约定:独立测试代码和脚本位于 `test/`,测试日志及截图位于 `test-log/`,编译输出位于 `out/`。构建与开发启动方式见 [文件目录整理说明](docs/文件目录整理说明.md),测试入口见 [测试说明](test/README.md)。
|
||||
|
||||
## 项目架构
|
||||
|
||||
1、前端: vue3+element-plus
|
||||
|
||||
前端代码位于 `fe`目录中,运行参考 `fe`目录中的README文件
|
||||
前端代码位于 `frontend` 目录中,运行参考 `frontend` 目录中的 README 文件。
|
||||
|
||||
2、后端: golang + MySQL/SQLite
|
||||
|
||||
后端代码进入 `server`文件夹,运行 `main.go`文件
|
||||
后端代码位于 `backend/`;推荐使用 `deploy/build.ps1` 构建后,通过 `deploy/start-dev.ps1` 启动开发服务。
|
||||
|
||||
3、编译项目
|
||||
|
||||
|
||||
10
README_CN.md
10
README_CN.md
@ -160,9 +160,9 @@ PMail 原版的协议登录流程与 Smail 不兼容,不能直接作为 Smail
|
||||
因此,使用 Smail 邮件服务时,请让客户端直接连接 Smail 的 SMTP、IMAP、POP3 服务,并按本节的开关、完整邮箱地址和协议授权码规则配置;不要把 PMail 原版的账号截断或旧密码逻辑复制到 Smail。
|
||||
# 插件
|
||||
|
||||
[微信推送](server/hooks/wechat_push/README.md)
|
||||
[微信推送](backend/hooks/wechat_push/README.md)
|
||||
|
||||
[垃圾邮件屏蔽](server/hooks/spam_block/README.md)
|
||||
[垃圾邮件屏蔽](backend/hooks/spam_block/README.md)
|
||||
|
||||
# 其他优秀开源插件
|
||||
|
||||
@ -176,15 +176,17 @@ PMail 原版的协议登录流程与 Smail 不兼容,不能直接作为 Smail
|
||||
|
||||
# 参与开发
|
||||
|
||||
目录约定:独立测试代码和脚本位于 `test/`,测试日志及截图位于 `test-log/`,编译输出位于 `out/`。构建与开发启动方式见 [文件目录整理说明](docs/文件目录整理说明.md),测试入口见 [测试说明](test/README.md)。
|
||||
|
||||
## 项目架构
|
||||
|
||||
1、前端: vue3+element-plus
|
||||
|
||||
前端代码位于 `fe`目录中,运行参考 `fe`目录中的README文件
|
||||
前端代码位于 `frontend` 目录中,运行参考 `frontend` 目录中的 README 文件。
|
||||
|
||||
2、后端: golang + MySQL/SQLite
|
||||
|
||||
后端代码进入 `server`文件夹,运行 `main.go`文件
|
||||
后端代码位于 `backend/`;推荐使用 `deploy/build.ps1` 构建后,通过 `deploy/start-dev.ps1` 启动开发服务。
|
||||
|
||||
3、编译项目
|
||||
|
||||
|
||||
3
backend/.gitignore
vendored
Normal file
3
backend/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
|
||||
!backend/config/
|
||||
!backend/config/*.go
|
||||
50
backend/cmd/smail-config/main.go
Normal file
50
backend/cmd/smail-config/main.go
Normal file
@ -0,0 +1,50 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"git.pcux.cn/xuans/Smail/config"
|
||||
)
|
||||
|
||||
// main 提供部署端显式配置迁移,不在网页或普通启动中自动改写配置。
|
||||
func main() {
|
||||
if err := run(os.Args[1:]); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run(args []string) error {
|
||||
flags := flag.NewFlagSet("smail-config", flag.ContinueOnError)
|
||||
root := flags.String("root", "", "Smail 服务根目录,包含 config 子目录")
|
||||
apply := flags.Bool("apply", false, "确认已停止后端,备份并执行迁移;默认只检查")
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if *root == "" || flags.NArg() != 0 {
|
||||
return fmt.Errorf("请指定 --root 服务根目录;迁移前必须停止后端")
|
||||
}
|
||||
absolute, err := filepath.Abs(*root)
|
||||
if err != nil {
|
||||
return fmt.Errorf("服务根目录无效")
|
||||
}
|
||||
options := config.RuntimeValidationOptions{HasSuperAdmin: true, Production: true}
|
||||
plan, err := config.PlanLegacyMigration(absolute, options)
|
||||
if err != nil {
|
||||
// 不输出配置原文、未知字段或底层错误,避免凭据进入终端记录。
|
||||
return fmt.Errorf("迁移检查未通过:未知字段 %d 项,需人工处理字段 %d 项;未执行迁移", len(plan.UnknownFields), len(plan.ManualFields))
|
||||
}
|
||||
if !*apply {
|
||||
fmt.Println("旧版字段检查通过;尚未写入配置。执行迁移前请停止后端,使用 --apply 继续完整校验和备份迁移。")
|
||||
return nil
|
||||
}
|
||||
result, err := config.MigrateLegacyConfig(absolute, options)
|
||||
if err != nil {
|
||||
return fmt.Errorf("配置迁移未完成,请保留原配置和备份,由部署管理员检查;不要手工删除分区文件")
|
||||
}
|
||||
fmt.Printf("配置迁移完成,代次:%d;原配置备份:%s。请使用最新版后端启动。\n", result.Generation, result.BackupPath)
|
||||
return nil
|
||||
}
|
||||
@ -172,18 +172,32 @@ func Init() error {
|
||||
}
|
||||
|
||||
loaded := &Config{}
|
||||
if err = json.Unmarshal(cfgData, loaded); err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
applyLegacyLoginPolicy(cfgData, loaded)
|
||||
migrated, err := unprotectCredentials(loaded)
|
||||
partitioned, err := isPartitionedConfig(cfgData)
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
return err
|
||||
}
|
||||
if migrated {
|
||||
if err = writeProtectedConfigFile(configName, loaded); err != nil {
|
||||
if partitioned {
|
||||
if DevelopmentMode {
|
||||
return fmt.Errorf("分区配置请通过 config.json 启动,不支持 config.dev.json")
|
||||
}
|
||||
loaded, err = loadPartitionedCompatibility()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err = json.Unmarshal(cfgData, loaded); err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
applyLegacyLoginPolicy(cfgData, loaded)
|
||||
migrated, err := unprotectCredentials(loaded)
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
if migrated {
|
||||
if err = writeProtectedConfigFile(configName, loaded); err != nil {
|
||||
return errors.Wrap(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Instance = loaded
|
||||
Instance.fixPath()
|
||||
@ -265,6 +279,17 @@ func WriteConfig(cfg *Config) error {
|
||||
}
|
||||
|
||||
func writeProtectedConfigFile(name string, cfg *Config) error {
|
||||
if data, err := readConfigFile(name); err == nil {
|
||||
partitioned, parseErr := isPartitionedConfig(data)
|
||||
if parseErr != nil {
|
||||
return parseErr
|
||||
}
|
||||
if partitioned {
|
||||
return fmt.Errorf("分区配置不能通过旧版写入接口覆盖,请使用配置管理")
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
stored, err := protectCredentials(cfg)
|
||||
if err != nil {
|
||||
return errors.Wrap(err)
|
||||
@ -303,6 +328,13 @@ func ReadConfig() (*Config, error) {
|
||||
log.Errorf("Read Config Error:%s", err.Error())
|
||||
return nil, errors.Wrap(err)
|
||||
}
|
||||
partitioned, err := isPartitionedConfig(cfgData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if partitioned {
|
||||
return loadPartitionedCompatibility()
|
||||
}
|
||||
if err = json.Unmarshal(cfgData, &configData); err != nil {
|
||||
log.Error("Read Config Unmarshal Error")
|
||||
return nil, errors.Wrap(err)
|
||||
111
backend/config/partitioned_compatibility.go
Normal file
111
backend/config/partitioned_compatibility.go
Normal file
@ -0,0 +1,111 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.pcux.cn/xuans/Smail/utils/credential"
|
||||
"github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
// isPartitionedConfig 在旧版解码前识别分区根文件,避免静默丢失数据库等字段。
|
||||
func isPartitionedConfig(data []byte) (bool, error) {
|
||||
var fields map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &fields); err != nil {
|
||||
return false, err
|
||||
}
|
||||
_, sections := fields["sections"]
|
||||
_, database := fields["database"]
|
||||
return sections || database, nil
|
||||
}
|
||||
|
||||
// loadPartitionedCompatibility 仅构造旧业务所需的只读快照,不重写分区配置。
|
||||
func loadPartitionedCompatibility() (*Config, error) {
|
||||
runtime, err := NewConfigLoader(ROOT_PATH, ConfigLoaderOptions{
|
||||
Validation: RuntimeValidationOptions{HasSuperAdmin: true, Production: !DevelopmentMode},
|
||||
}).Load()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vault, err := credential.NewVault(ROOT_PATH)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ValidateRuntimeSecrets(vault, runtime); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
driver, dsn, err := compatibilityDatabase(runtime.Root.Database, vault)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
root := runtime.Root
|
||||
cfg := &Config{
|
||||
LogLevel: root.LogLevel, Domain: root.Domains.Primary,
|
||||
Domains: append([]string(nil), root.Domains.Accepted...), WebDomain: root.Domains.Web,
|
||||
AccessHosts: append([]string(nil), root.Domains.AccessHosts...),
|
||||
DbType: driver, DbDSN: dsn, SetupState: runtime.SetupState,
|
||||
IsInit: runtime.SetupState == SetupStateReady,
|
||||
HttpPort: root.HTTP.Port, HttpsPort: root.HTTP.HTTPSPort,
|
||||
HttpsEnabled: 2, TLSMode: root.HTTP.TLSMode,
|
||||
TrustedProxyCIDRs: append([]string(nil), root.HTTP.TrustedProxyCIDRs...),
|
||||
DkimPrivateKeyPath: filepath.Join(ROOT_PATH, "config", "dkim", "dkim.priv"),
|
||||
SSLType: SSLTypeUser,
|
||||
SpamFilterLevel: runtime.Email.Policy.SpamFilterLevel,
|
||||
LoginPolicy: runtime.Email.Policy.LoginVerificationPolicy,
|
||||
ExternalRecoveryEnabled: runtime.Email.Policy.ExternalRecoveryEnabled,
|
||||
SMTPHost: runtime.SMTP.Relay.RelayHost, SMTPPort: runtime.SMTP.Relay.RelayPort,
|
||||
SMTPUsername: runtime.SMTP.Relay.Username, SMTPSecurity: runtime.SMTP.Relay.Security,
|
||||
SMTPFrom: runtime.SMTP.Relay.From, SMTPFromDomain: runtime.SMTP.Relay.FromDomain,
|
||||
VerificationEnabled: runtime.SMTP.Relay.VerificationEnabled,
|
||||
}
|
||||
if root.HTTP.HTTPSEnabled {
|
||||
cfg.HttpsEnabled = 1
|
||||
}
|
||||
if runtime.SMTP.Relay.Password.Configured() {
|
||||
cfg.SMTPPassword, err = OpenStoredSecret(vault, runtime.SMTP.Relay.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
tls := runtime.SSL.TLS
|
||||
if tls.Mode == ManagedSSLModeDefault {
|
||||
cfg.SSLType = SSLTypeAutoHTTP
|
||||
}
|
||||
// 迁移器保留的旧证书引用仍走严格的只读文件校验;新托管引用沿用托管解析器。
|
||||
if strings.HasPrefix(tls.CertificateRef, managedSSLRelativeRoot+"/") {
|
||||
cfg.SSL = tls
|
||||
} else {
|
||||
cfg.SSLPublicKeyPath = resolvePath(tls.CertificateRef)
|
||||
cfg.SSLPrivateKeyPath = resolvePath(tls.PrivateKeyRef)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func compatibilityDatabase(database DatabaseConfig, vault *credential.Vault) (string, string, error) {
|
||||
if database.Type == DatabaseTypeSQLite {
|
||||
return DBTypeSQLite, filepath.Join(ROOT_PATH, "config", database.LocalRef), nil
|
||||
}
|
||||
password, err := OpenStoredSecret(vault, database.Password)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
address := net.JoinHostPort(database.Host, strconv.Itoa(int(database.Port)))
|
||||
switch database.Type {
|
||||
case DatabaseTypeMySQL:
|
||||
settings := mysql.NewConfig()
|
||||
settings.User, settings.Passwd = database.Username, password
|
||||
settings.Net, settings.Addr, settings.DBName = "tcp", address, database.Name
|
||||
settings.ParseTime = true
|
||||
return DBTypeMySQL, settings.FormatDSN(), nil
|
||||
case DatabaseTypePostgreSQL:
|
||||
uri := url.URL{Scheme: "postgres", User: url.UserPassword(database.Username, password), Host: address, Path: "/" + database.Name}
|
||||
return DBTypePostgres, uri.String(), nil
|
||||
default:
|
||||
return "", "", fmt.Errorf("不支持的数据库类型")
|
||||
}
|
||||
}
|
||||
63
backend/config/partitioned_compatibility_test.go
Normal file
63
backend/config/partitioned_compatibility_test.go
Normal file
@ -0,0 +1,63 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPartitionedCompatibilityStartupAndWriteProtection(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
oldRoot, oldArgs, oldDevelopment := ROOT_PATH, os.Args, DevelopmentMode
|
||||
oldInstance, oldSnapshot := Instance, CurrentConfig()
|
||||
ROOT_PATH, os.Args = root, []string{"smail"}
|
||||
t.Cleanup(func() {
|
||||
ROOT_PATH, os.Args, DevelopmentMode = oldRoot, oldArgs, oldDevelopment
|
||||
Instance = oldInstance
|
||||
PublishConfigSnapshot(oldSnapshot)
|
||||
})
|
||||
writeLegacyMigrationFixture(t, root, map[string]any{
|
||||
"logLevel": "info", "domain": "mail.example.com", "webDomain": "mail.example.com",
|
||||
"dbType": "sqlite", "dbDSN": "./config/existing.db", "isInit": true,
|
||||
"sslType": "1", "SSLPublicKeyPath": "./config/ssl/site.crt", "SSLPrivateKeyPath": "./config/ssl/site.key",
|
||||
"httpPort": 80, "httpsPort": 443, "httpsEnabled": 1,
|
||||
})
|
||||
if _, err := MigrateLegacyConfig(root, RuntimeValidationOptions{HasSuperAdmin: true, Production: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := filepath.Join(root, "config", "config.json")
|
||||
before, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i := 0; i < 2; i++ {
|
||||
if err := Init(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg := CurrentConfig()
|
||||
if !cfg.IsSetupReady() || cfg.DbType != DBTypeSQLite || cfg.DbDSN != filepath.Join(root, "config", "existing.db") {
|
||||
t.Fatal("partitioned startup lost initialization or database identity")
|
||||
}
|
||||
if cfg.WebDomain != "mail.example.com" || cfg.SSLPublicKeyPath != filepath.Join(root, "config", "ssl", "site.crt") {
|
||||
t.Fatal("partitioned startup lost domain or certificate path")
|
||||
}
|
||||
}
|
||||
cfg, err := ReadConfig()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := WriteConfig(cfg); err == nil {
|
||||
t.Fatal("legacy writer must not overwrite partitioned config")
|
||||
}
|
||||
after, err := os.ReadFile(path)
|
||||
if err != nil || !bytes.Equal(before, after) {
|
||||
t.Fatal("loading or legacy writing changed partitioned root")
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "config", "smtp.json"), []byte("{}"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := Init(); err == nil {
|
||||
t.Fatal("corrupted partition must not silently fall back to legacy startup")
|
||||
}
|
||||
}
|
||||
@ -17,16 +17,21 @@ import (
|
||||
|
||||
// ConfigStatus 返回配置管理状态,不返回任何密文、路径或 DSN。
|
||||
func ConfigStatus(ctx *appcontext.Context, w http.ResponseWriter, req *http.Request) {
|
||||
manager, ok := configManagerOrError(w)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := authorizeCapability(ctx, access.CapabilityConfigRead, "", false); err != nil {
|
||||
writeConfigAccessError(w, err)
|
||||
return
|
||||
}
|
||||
manager := config.CurrentManager()
|
||||
if manager == nil {
|
||||
response.NewSuccessResponse(map[string]any{
|
||||
"available": false,
|
||||
"unavailableReason": "当前使用旧版配置,在线配置管理尚未启用。请由部署管理员完成分区配置迁移后重启服务。",
|
||||
}).FPrint(w)
|
||||
return
|
||||
}
|
||||
status := manager.Status()
|
||||
response.NewSuccessResponse(map[string]any{
|
||||
"available": true,
|
||||
"setupState": status.SetupState,
|
||||
"activeGeneration": status.ActiveGeneration,
|
||||
"lastValidAt": status.LastValidAt,
|
||||
@ -151,7 +156,8 @@ func ListConfigBackups(ctx *appcontext.Context, w http.ResponseWriter, req *http
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := authorizeCapability(ctx, access.CapabilityConfigRollback, "", false); err != nil {
|
||||
// 读取脱敏代次不执行回滚;预览、票据和执行仍要求高风险授权。
|
||||
if err := authorizeCapability(ctx, access.CapabilityConfigRead, "", false); err != nil {
|
||||
writeConfigAccessError(w, err)
|
||||
return
|
||||
}
|
||||
@ -330,7 +336,7 @@ func configRequestManager(w http.ResponseWriter, req *http.Request) (*config.Con
|
||||
func configManagerOrError(w http.ResponseWriter) (*config.ConfigManager, bool) {
|
||||
manager := config.CurrentManager()
|
||||
if manager == nil {
|
||||
writeConfigError(w, errors.New("configuration manager unavailable"))
|
||||
writeConfigError(w, config.ErrManagerStorage)
|
||||
return nil, false
|
||||
}
|
||||
return manager, true
|
||||
76
backend/controllers/config_backup_access_test.go
Normal file
76
backend/controllers/config_backup_access_test.go
Normal file
@ -0,0 +1,76 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.pcux.cn/xuans/Smail/config"
|
||||
"git.pcux.cn/xuans/Smail/services/access"
|
||||
appcontext "git.pcux.cn/xuans/Smail/utils/context"
|
||||
)
|
||||
|
||||
func TestConfigBackupReadDoesNotRequireRecentAuth(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(root, "config"), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
legacy := []byte(`{"domain":"mail.example.com","webDomain":"mail.example.com","dbType":"sqlite","dbDSN":"./config/test.db","isInit":true,"httpPort":80,"httpsPort":443,"httpsEnabled":1}`)
|
||||
if err := os.WriteFile(filepath.Join(root, "config", "config.json"), legacy, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
validation := config.RuntimeValidationOptions{HasSuperAdmin: true, Production: true}
|
||||
if _, err := config.MigrateLegacyConfig(root, validation); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
manager, err := config.NewConfigManager(root, config.ConfigLoaderOptions{Validation: validation})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
previous := config.CurrentManager()
|
||||
config.SetRuntimeManager(manager)
|
||||
t.Cleanup(func() { config.SetRuntimeManager(previous) })
|
||||
|
||||
for _, role := range []access.Role{access.RoleSuperAdmin, access.RoleAdmin, access.RoleUser} {
|
||||
t.Run(string(role), func(t *testing.T) {
|
||||
ctx := &appcontext.Context{UserID: 1, Role: role, RecentAuth: false}
|
||||
recorder := httptest.NewRecorder()
|
||||
ListConfigBackups(ctx, recorder, httptest.NewRequest(http.MethodGet, "/api/v1/config/backups", nil))
|
||||
if role == access.RoleUser {
|
||||
if recorder.Code != http.StatusForbidden {
|
||||
t.Fatalf("普通用户不应读取配置备份:%d", recorder.Code)
|
||||
}
|
||||
return
|
||||
}
|
||||
var payload struct {
|
||||
ErrorNo int `json:"errorNo"`
|
||||
}
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if recorder.Code != http.StatusOK || payload.ErrorNo != 0 {
|
||||
t.Fatalf("管理员读取脱敏备份不应要求近期认证:%d", recorder.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 列表读取放宽不能改变实际回滚链路的近期认证要求。
|
||||
for name, handler := range map[string]func(*appcontext.Context, http.ResponseWriter, *http.Request){
|
||||
"preview": PreviewConfigRollback,
|
||||
"ticket": IssueConfigRollbackTicket,
|
||||
"execute": RollbackConfig,
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
handler(&appcontext.Context{UserID: 1, Role: access.RoleSuperAdmin}, recorder,
|
||||
httptest.NewRequest(http.MethodPost, "/api/v1/config/rollback", strings.NewReader(`{"targetGeneration":1}`)))
|
||||
if recorder.Code != http.StatusForbidden || !strings.Contains(recorder.Body.String(), "RECENT_AUTH_REQUIRED") {
|
||||
t.Fatalf("回滚仍应要求近期认证:%d", recorder.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -99,11 +99,13 @@ func hashedLoginKey(parts ...string) string {
|
||||
return hex.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
// loginSecurityFields returns the non-sensitive state used by the post-login security guide.
|
||||
// loginSecurityFields 返回登录后安全引导所需的非敏感状态。
|
||||
func loginSecurityFields(user *models.User) map[string]any {
|
||||
fields := map[string]any{
|
||||
"recoveryCodesConfigured": false,
|
||||
"showRecoverySetupPrompt": false,
|
||||
"showSecuritySetupPrompt": user != nil && user.SecuritySetupAcknowledged == 0,
|
||||
"webAuthnEnabled": webAuthnRuntime() != nil,
|
||||
}
|
||||
if user == nil || user.EffectiveRole() == access.RoleSuperAdmin {
|
||||
return fields
|
||||
@ -272,11 +274,11 @@ func Login(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
func Logout(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
if err := session.Instance.Clear(ctx.Context); err != nil {
|
||||
csrf.Clear(w, req, !config.DevelopmentMode)
|
||||
if err := session.Instance.Destroy(ctx.Context); err != nil {
|
||||
log.WithContext(ctx).WithError(err).Error("session cleanup during logout failed")
|
||||
response.NewErrorResponse(response.ServerError, "退出失败", "").FPrint(w)
|
||||
return
|
||||
}
|
||||
csrf.Clear(w, req, !config.DevelopmentMode)
|
||||
response.NewSuccessResponse("Success").FPrint(w)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user