diff --git a/panel/src/lib/dkim.ts b/panel/src/lib/dkim.ts index e0b0ac1..35ec97c 100644 --- a/panel/src/lib/dkim.ts +++ b/panel/src/lib/dkim.ts @@ -1,7 +1,7 @@ import { and, eq } from 'drizzle-orm' import { db } from '@/db' import { dnsRecords } from '@/db/schema' -import { DMS_CONFIG_DIR, DmsResult, dkim as dmsDkim, readContainerFile } from './dms' +import { DMS_CONFIG_DIR, DmsResult, dkim as dmsDkim, readContainerFile, syncDkimRuntime } from './dms' /** * DMS 生成的 mail.txt 是 **DNS zone 片段**,不是裸值,例如: @@ -34,13 +34,19 @@ export async function resolveDkimPublicKey(domainId: number, domainName: string) const fromDms = await dmsDkim.publicKey(domainName) if (fromDms.ok) { const parsed = parseDkimDnsValue(fromDms.stdout) - if (parsed) return parsed + if (parsed) { + await applyDkimRuntimeConfig() + return parsed + } } const legacy = await readContainerFile(`${DMS_CONFIG_DIR}/opendkim/keys/${domainName}/mail.txt`) if (legacy.ok) { const parsed = parseDkimDnsValue(legacy.stdout) - if (parsed) return parsed + if (parsed) { + await applyDkimRuntimeConfig() + return parsed + } } // DMS 16:DKIM 密钥不是自动生成的,需要显式跑 `setup config dkim` @@ -63,7 +69,15 @@ export async function ensureDkim(domainName: string): Promise { // 刚导入、还没建邮箱的域必须用 `domain <域>` 才会生成(官方支持)。 const gen = await dmsDkim.regenerate(domainName) if (!(gen as DmsResult).ok) return null + await applyDkimRuntimeConfig() const again = await dmsDkim.publicKey(domainName) if (!again.ok) return null return parseDkimDnsValue(again.stdout) } + +async function applyDkimRuntimeConfig(): Promise { + const result = await syncDkimRuntime() + if (!result.ok) { + throw new Error(`DKIM 配置已生成,但同步到 OpenDKIM 运行配置失败:${result.error}${result.stderr ? ` (${result.stderr})` : ''}`) + } +} diff --git a/panel/src/lib/dms.ts b/panel/src/lib/dms.ts index 1dcea56..72310bf 100644 --- a/panel/src/lib/dms.ts +++ b/panel/src/lib/dms.ts @@ -49,6 +49,56 @@ export function readContainerFile(absPath: string): Promise { return dockerExec(['exec', config.dmsContainer, 'cat', absPath], 15_000) } +let dkimRuntimeSync: Promise | null = null + +/** + * DMS `setup config dkim` writes durable files under /tmp/docker-mailserver, + * while OpenDKIM reads its startup copy under /etc/opendkim. Keep the runtime + * copy current without restarting the whole mail server. Concurrent panel + * requests share one sync/reload operation. + */ +export function syncDkimRuntime(): Promise { + if (!dkimRuntimeSync) { + const script = ` +set -eu +src=/tmp/docker-mailserver/opendkim +dst=/etc/opendkim +[ -s "$src/SigningTable" ] && [ -s "$src/KeyTable" ] && [ -d "$src/keys" ] || exit 0 + +changed=0 +for name in SigningTable KeyTable TrustedHosts; do + if [ -f "$src/$name" ] && { [ ! -f "$dst/$name" ] || ! cmp -s "$src/$name" "$dst/$name"; }; then + changed=1 + fi +done +for source_file in $(find "$src/keys" -type f); do + relative_path=\${source_file#"$src/"} + if [ ! -f "$dst/$relative_path" ] || ! cmp -s "$source_file" "$dst/$relative_path"; then + changed=1 + break + fi +done +status=$(supervisorctl status opendkim 2>/dev/null || true) +if ! printf '%s' "$status" | grep -Eq '^opendkim[[:space:]]+RUNNING[[:space:]]'; then changed=1; fi +[ "$changed" -eq 1 ] || { echo unchanged; exit 0; } + +# Match DMS's own startup setup: copy durable config, set key ownership and +# permissions, then restart only the DKIM signer. +cp -a "$src/." "$dst/" +chown -R opendkim:opendkim "$dst" +chmod -R 0700 "$dst/keys" +if printf '%s' "$status" | grep -Eq '^opendkim[[:space:]]+RUNNING[[:space:]]'; then + supervisorctl restart opendkim +else + supervisorctl start opendkim +fi +` + dkimRuntimeSync = dockerExec(['exec', config.dmsContainer, 'sh', '-ec', script], 20_000) + .finally(() => { dkimRuntimeSync = null }) + } + return dkimRuntimeSync +} + /** 服务状态探活:supervisord 管理的各进程 RUNNING/STOPPED 一览 */ export async function supervisorStatus(): Promise { try {