소스 검색

fix: avoid postfix self mx loop

AI-Co-Authored-By: Codex
chendeben 1 개월 전
부모
커밋
6b938702ec
4개의 변경된 파일51개의 추가작업 그리고 16개의 파일을 삭제
  1. 2 1
      docker/postfix/entrypoint.sh
  2. 13 7
      scripts/deploy-remote.sh
  3. 32 8
      test/deploy-remote-script.test.js
  4. 4 0
      test/dovecot-config.test.js

+ 2 - 1
docker/postfix/entrypoint.sh

@@ -5,6 +5,7 @@ MAIL_HOSTNAME="${MAIL_HOSTNAME:-mailhub.local}"
 MAIL_ORIGIN_DOMAIN="${MAIL_ORIGIN_DOMAIN:-${MAIL_HOSTNAME#*.}}"
 POSTFIX_LOG_FILE="${POSTFIX_LOG_FILE:-/dev/stdout}"
 POSTFIX_PROXY_INTERFACES="${POSTFIX_PROXY_INTERFACES:-${SENDING_IP:-}}"
+POSTFIX_MYHOSTNAME="${POSTFIX_MYHOSTNAME:-postfix.${MAIL_HOSTNAME}}"
 
 if [[ "${POSTFIX_LOG_FILE}" != "/dev/stdout" ]]; then
   mkdir -p "$(dirname "${POSTFIX_LOG_FILE}")"
@@ -13,7 +14,7 @@ if [[ "${POSTFIX_LOG_FILE}" != "/dev/stdout" ]]; then
   tail -n 0 -F "${POSTFIX_LOG_FILE}" &
 fi
 
-postconf -e "myhostname = ${MAIL_HOSTNAME}"
+postconf -e "myhostname = ${POSTFIX_MYHOSTNAME}"
 postconf -e "myorigin = ${MAIL_ORIGIN_DOMAIN}"
 postconf -e "mydestination ="
 postconf -e "inet_interfaces = all"

+ 13 - 7
scripts/deploy-remote.sh

@@ -6,6 +6,7 @@ remote_dir="${MAILHUB_DEPLOY_DIR:-}"
 branch="${MAILHUB_DEPLOY_BRANCH:-$(git branch --show-current)}"
 git_url="${MAILHUB_DEPLOY_GIT_URL:-$(git remote get-url origin)}"
 stash_remote="${MAILHUB_DEPLOY_STASH_REMOTE:-0}"
+run_maildir_migration="${MAILHUB_DEPLOY_RUN_MAILDIR_MIGRATION:-0}"
 
 if [[ -z "${remote}" || -z "${remote_dir}" ]]; then
   echo "Set MAILHUB_DEPLOY_REMOTE and MAILHUB_DEPLOY_DIR before deploying." >&2
@@ -29,13 +30,14 @@ if [[ "${local_head}" != "${remote_head}" ]]; then
 fi
 
 ssh -o ServerAliveInterval=15 -o ServerAliveCountMax=4 "${remote}" \
-  'bash -s' -- "${remote_dir}" "${branch}" "${git_url}" "${stash_remote}" <<'REMOTE'
+  'bash -s' -- "${remote_dir}" "${branch}" "${git_url}" "${stash_remote}" "${run_maildir_migration}" <<'REMOTE'
 set -euo pipefail
 
 remote_dir="$1"
 branch="$2"
 git_url="$3"
 stash_remote="$4"
+run_maildir_migration="$5"
 
 cd "${remote_dir}"
 
@@ -245,13 +247,17 @@ else
 fi
 docker compose build app postfix </dev/null
 docker compose pull dovecot </dev/null
-stopped_app_container="$(docker compose ps --all --quiet app </dev/null 2>/dev/null | tail -n 1 || true)"
-stopped_dovecot_container="$(docker compose ps --all --quiet dovecot </dev/null 2>/dev/null | tail -n 1 || true)"
-mail_services_stopped_for_migration=1
-docker compose stop app dovecot </dev/null
-docker compose run --rm --no-deps -T app </dev/null node scripts/migrate-sqlite-maildir.js
+if [[ "${run_maildir_migration}" == "1" ]]; then
+  stopped_app_container="$(docker compose ps --all --quiet app </dev/null 2>/dev/null | tail -n 1 || true)"
+  stopped_dovecot_container="$(docker compose ps --all --quiet dovecot </dev/null 2>/dev/null | tail -n 1 || true)"
+  mail_services_stopped_for_migration=1
+  docker compose stop app dovecot </dev/null
+  docker compose run --rm --no-deps -T app </dev/null node scripts/migrate-sqlite-maildir.js
+  mail_services_stopped_for_migration=0
+else
+  echo "Skipping Maildir migration check; set MAILHUB_DEPLOY_RUN_MAILDIR_MIGRATION=1 to run it."
+fi
 maildir_cutover_committed=1
-mail_services_stopped_for_migration=0
 docker compose up -d </dev/null
 wait_for_compose_health app postfix dovecot
 verify_mail_runtime

+ 32 - 8
test/deploy-remote-script.test.js

@@ -23,11 +23,14 @@ test('checks Maildir access with Dovecot mail worker uid instead of container ro
   assert.doesNotMatch(scriptSource, /trap .*rm -f -- \"\$probe\"/);
 });
 
-test('runs the Maildir migration without consuming the SSH heredoc stdin', () => {
+test('keeps the Maildir migration as an explicit deployment option', () => {
+  assert.match(scriptSource, /run_maildir_migration="\$\{MAILHUB_DEPLOY_RUN_MAILDIR_MIGRATION:-0\}"/);
+  assert.match(scriptSource, /if \[\[ "\$\{run_maildir_migration\}" == "1" \]\]; then/);
   assert.match(
     scriptSource,
     /docker compose run --rm --no-deps -T app <\/dev\/null node scripts\/migrate-sqlite-maildir\.js/
   );
+  assert.match(scriptSource, /Skipping Maildir migration check; set MAILHUB_DEPLOY_RUN_MAILDIR_MIGRATION=1 to run it\./);
 });
 
 test('isolates runtime probes and setup scripts from the SSH heredoc stdin', () => {
@@ -76,24 +79,41 @@ test('continues after remote tools actively consume their stdin', { skip: !canRu
 
   assert.equal(result.status, 0, result.stderr);
   const events = readEvents(fixture.logFile);
-  assert.ok(events.includes('migrate'), events.join('\n'));
+  assert.equal(events.includes('migrate'), false, events.join('\n'));
   assert.ok(events.includes('sync:1'), events.join('\n'));
   assert.equal(events.at(-1), 'final:ps', events.join('\n'));
 });
 
-test('prepares the certificate and Dovecot image before entering the maintenance window', { skip: !canRun }, (t) => {
+test('skips the Maildir migration maintenance window by default', { skip: !canRun }, (t) => {
   const fixture = createFixture(t);
   const result = runDeploy(fixture);
 
+  assert.equal(result.status, 0, result.stderr);
+  const events = readEvents(fixture.logFile);
+  const offlineSync = events.indexOf('sync:0');
+  const pull = events.indexOf('pull:dovecot');
+  const up = events.indexOf('up');
+  assert.equal(events.includes('stop:app,dovecot'), false, events.join('\n'));
+  assert.equal(events.includes('migrate'), false, events.join('\n'));
+  assert.ok(offlineSync >= 0 && offlineSync < up, events.join('\n'));
+  assert.ok(pull >= 0 && pull < up, events.join('\n'));
+});
+
+test('runs the opt-in Maildir migration without consuming the SSH heredoc stdin', { skip: !canRun }, (t) => {
+  const fixture = createFixture(t);
+  const result = runDeploy(fixture, { runMaildirMigration: '1' });
+
   assert.equal(result.status, 0, result.stderr);
   const events = readEvents(fixture.logFile);
   const offlineSync = events.indexOf('sync:0');
   const pull = events.indexOf('pull:dovecot');
   const stop = events.indexOf('stop:app,dovecot');
+  const migrate = events.indexOf('migrate');
   const up = events.indexOf('up');
   assert.ok(offlineSync >= 0 && offlineSync < stop, events.join('\n'));
   assert.ok(pull >= 0 && pull < stop, events.join('\n'));
-  assert.ok(stop < up, events.join('\n'));
+  assert.ok(stop >= 0 && stop < migrate, events.join('\n'));
+  assert.ok(migrate >= 0 && migrate < up, events.join('\n'));
 });
 
 test('reports the previous revision when deployment fails', { skip: !canRun }, (t) => {
@@ -116,7 +136,7 @@ test('reports the previous revision when deployment fails', { skip: !canRun }, (
 
 test('stops the app for migration and restarts the previous container if migration fails', { skip: !canRun }, (t) => {
   const fixture = createFixture(t);
-  const result = runDeploy(fixture, { migrationStatus: '23' });
+  const result = runDeploy(fixture, { migrationStatus: '23', runMaildirMigration: '1' });
 
   assert.equal(result.status, 23);
   assert.match(result.stderr, /Restarting the pre-migration MailHub mail services\./);
@@ -133,7 +153,7 @@ test('stops the app for migration and restarts the previous container if migrati
 
 test('keeps the maintenance window explicit after cutover health checks fail', { skip: !canRun }, (t) => {
   const fixture = createFixture(t);
-  const result = runDeploy(fixture, { runtimeStatus: '29' });
+  const result = runDeploy(fixture, { runtimeStatus: '29', runMaildirMigration: '1' });
 
   assert.notEqual(result.status, 0);
   assert.match(result.stderr, /Maildir cutover is already committed; legacy mail services will not be restarted/);
@@ -253,7 +273,10 @@ exit 0
   return { root, fakeBin, remoteDir, logFile };
 }
 
-function runDeploy(fixture, { syncStatus = '0', migrationStatus = '0', runtimeStatus = '0' } = {}) {
+function runDeploy(
+  fixture,
+  { syncStatus = '0', migrationStatus = '0', runtimeStatus = '0', runMaildirMigration = '0' } = {}
+) {
   return spawnSync('bash', [scriptPath], {
     cwd: fixture.root,
     encoding: 'utf8',
@@ -268,7 +291,8 @@ function runDeploy(fixture, { syncStatus = '0', migrationStatus = '0', runtimeSt
       MAILHUB_DEPLOY_TEST_REMOTE_DIR: fixture.remoteDir,
       MAILHUB_DEPLOY_TEST_SYNC_STATUS: syncStatus,
       MAILHUB_DEPLOY_TEST_MIGRATION_STATUS: migrationStatus,
-      MAILHUB_DEPLOY_TEST_RUNTIME_STATUS: runtimeStatus
+      MAILHUB_DEPLOY_TEST_RUNTIME_STATUS: runtimeStatus,
+      MAILHUB_DEPLOY_RUN_MAILDIR_MIGRATION: runMaildirMigration
     }
   });
 }

+ 4 - 0
test/dovecot-config.test.js

@@ -99,10 +99,14 @@ test('Dovecot uses Lua passdb, a static rootless userdb, and Maildir storage', (
 });
 
 test('Postfix hands self-referential MX destinations to the MailHub inbound listener', () => {
+  assert.match(postfixEntrypoint, /POSTFIX_MYHOSTNAME="\$\{POSTFIX_MYHOSTNAME:-postfix\.\$\{MAIL_HOSTNAME\}\}"/);
+  assert.match(postfixEntrypoint, /postconf -e "myhostname = \$\{POSTFIX_MYHOSTNAME\}"/);
   assert.match(postfixEntrypoint, /postconf -e "mydestination ="/);
   assert.match(postfixEntrypoint, /POSTFIX_PROXY_INTERFACES="\$\{POSTFIX_PROXY_INTERFACES:-\$\{SENDING_IP:-\}\}"/);
   assert.match(postfixEntrypoint, /postconf -e "proxy_interfaces = \$\{POSTFIX_PROXY_INTERFACES\}"/);
   assert.match(postfixEntrypoint, /postconf -e "best_mx_transport = smtp:\[app\]:25"/);
+  assert.match(postfixEntrypoint, /postconf -e "smtp_helo_name = \$\{MAIL_HOSTNAME\}"/);
+  assert.doesNotMatch(postfixEntrypoint, /postconf -e "myhostname = \$\{MAIL_HOSTNAME\}"/);
   assert.match(appService, /networks:\n\s+- mailhub/);
 });