import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { test } from 'node:test'; const compose = readFileSync(new URL('../docker-compose.yml', import.meta.url), 'utf8'); const appService = compose.match(/^ app:\n[\s\S]*?(?=^ dovecot:)/m)?.[0] || ''; const dovecotService = compose.match(/^ dovecot:\n[\s\S]*?(?=^ postfix:)/m)?.[0] || ''; const envExample = readFileSync(new URL('../.env.example', import.meta.url), 'utf8'); const readme = readFileSync(new URL('../README.md', import.meta.url), 'utf8'); const maildirSyncSource = readFileSync(new URL('../src/maildir-sync.js', import.meta.url), 'utf8'); const serverSource = readFileSync(new URL('../src/server.js', import.meta.url), 'utf8'); const authConfig = readFileSync(new URL('../docker/dovecot/auth.conf', import.meta.url), 'utf8'); const mailConfig = readFileSync(new URL('../docker/dovecot/mailhub.conf', import.meta.url), 'utf8'); const sslConfig = readFileSync(new URL('../docker/dovecot/ssl.conf', import.meta.url), 'utf8'); const authLua = readFileSync(new URL('../docker/dovecot/auth.lua', import.meta.url), 'utf8'); const postfixEntrypoint = readFileSync(new URL('../docker/postfix/entrypoint.sh', import.meta.url), 'utf8'); test('Maildir reconciliation defaults to a five-minute polling interval', () => { assert.match(serverSource, /MAILDIR_SYNC_INTERVAL_MS \|\| 300000/); assert.match(maildirSyncSource, /intervalMs = 300_000/); assert.match(maildirSyncSource, /Number\(intervalMs\) \|\| 300_000/); assert.match(envExample, /^MAILDIR_SYNC_INTERVAL_MS=300000$/m); assert.match(readme, /MAILDIR_SYNC_INTERVAL_MS[^\n]*`300000` 毫秒(5 分钟)/); }); test('Compose delegates public IMAP and POP3 ports to rootless Dovecot', () => { assert.match(compose, /image: dovecot\/dovecot:2\.4\.4/); for (const mapping of ['143:31143', '993:31993', '110:31110', '995:31995']) { assert.ok(compose.includes(mapping), `missing Dovecot port mapping ${mapping}`); } for (const oldMapping of ['143:143', '993:993', '110:110', '995:995']) { assert.equal(compose.includes(oldMapping), false, `app still owns ${oldMapping}`); } assert.match(compose, /dovecot_internal:\n\s+internal: true/); assert.match(dovecotService, /networks:\n\s+- dovecot_internal\n\s+- dovecot_public/); assert.match(compose, /^ dovecot_public:\s*$/m); assert.doesNotMatch(compose, /dovecot_public:\n\s+internal: true/); assert.match(appService, /networks:\n\s+- mailhub\n\s+- dovecot_internal/); assert.doesNotMatch(appService, /dovecot_public/); assert.match(compose, /file: \.\/data\/secrets\/dovecot_auth_secret/); assert.match(compose, /MAIL_ACCESS_BACKEND: dovecot/); assert.match(compose, /MAILDIR_ROOT: \/data\/maildir/); assert.match(compose, /test -r \/run\/secrets\/dovecot_auth_secret/); assert.match(compose, /test -s \/run\/secrets\/dovecot_auth_secret/); assert.match(compose, /test -w \/srv\/vmail/); assert.match(compose, /doveadm service status imap-login pop3-login/); }); test('Dovecot uses Lua passdb, a static rootless userdb, and Maildir storage', () => { assert.match(authConfig, /passdb lua \{/); assert.match(authConfig, /SUBMISSION_TLS_CERT = %\{env:SUBMISSION_TLS_CERT\}/); assert.match(authConfig, /SUBMISSION_TLS_KEY = %\{env:SUBMISSION_TLS_KEY\}/); assert.match(authConfig, /lua_file = \/etc\/dovecot\/auth\.lua/); assert.match(authConfig, /userdb static \{/); assert.match(authConfig, /userdb static \{[\s\S]*allow_all_users = yes/); assert.match(authConfig, /uid = 1000/); assert.match(authConfig, /gid = 1000/); assert.match(authConfig, /home = \/srv\/vmail\/%\{user \| lower\}/); assert.match(mailConfig, /^protocols = imap pop3$/m); assert.match(mailConfig, /^mail_driver = maildir$/m); assert.match(mailConfig, /^mail_path = ~\/mail$/m); assert.match(mailConfig, /^mailbox_list_layout = maildir\+\+$/m); assert.match(mailConfig, /^mailbox_list_storage_escape_char = \^$/m); assert.match(mailConfig, /^mailbox_list_utf8 = no$/m); for (const [mailbox, specialUse] of [ ['Archive', 'Archive'], ['Drafts', 'Drafts'], ['Junk', 'Junk'], ['Sent', 'Sent'], ['Trash', 'Trash'] ]) { assert.match( mailConfig, new RegExp(`mailbox ${mailbox} \\{[\\s\\S]*?auto = subscribe[\\s\\S]*?special_use = \\\\${specialUse}`) ); } assert.match(mailConfig, /service imap-login \{[\s\S]*chroot =/); assert.match(mailConfig, /service imap-login \{[\s\S]*inet_listener imaps \{[\s\S]*ssl = yes/); assert.match(mailConfig, /service pop3-login \{[\s\S]*inet_listener pop3s \{[\s\S]*ssl = yes/); assert.match(sslConfig, /^ssl_server_cert_file = \$ENV:SUBMISSION_TLS_CERT$/m); assert.match(sslConfig, /^ssl_server_key_file = \$ENV:SUBMISSION_TLS_KEY$/m); }); test('Postfix hands self-referential MX destinations to the MailHub inbound listener', () => { assert.match(postfixEntrypoint, /postconf -e "mydestination ="/); assert.match(postfixEntrypoint, /postconf -e "best_mx_transport = smtp:\[app\]:25"/); assert.match(appService, /networks:\n\s+- mailhub/); }); test('Lua passdb sends both IMAP and POP3 to the private auth bridge', () => { assert.match(authLua, /http:\/\/app:3001\/internal\/dovecot\/auth/); assert.match(authLua, /\/run\/secrets\/dovecot_auth_secret/); assert.match(authLua, /first_nonempty_string\(request\.protocol, request\.service\)/); assert.match(authLua, /request\.remote_ip,[\s\S]*request\.real_remote_ip/); assert.match(authLua, /protocol ~= "imap" and protocol ~= "pop3"/); assert.match(authLua, /request_max_attempts = 1/); assert.match(authLua, /auto_retry = "no"/); assert.match(authLua, /request_absolute_timeout = "2s"/); assert.match(authLua, /add_header\("connection", "close"\)/); assert.match(authLua, /status ~= 200[\s\S]*PASSDB_RESULT_INTERNAL_FAILURE/); assert.doesNotMatch(authLua, /status == (?:401|403|404)/); assert.match( authLua, /payload\.authenticated == false[\s\S]*PASSDB_RESULT_PASSWORD_MISMATCH[\s\S]*payload\.authenticated ~= true[\s\S]*PASSDB_RESULT_INTERNAL_FAILURE/ ); assert.match(authLua, /valid_user\(payload\.user\)/); assert.match(authLua, /PASSDB_RESULT_OK, \{ user = string\.lower\(payload\.user\) \}/); assert.doesNotMatch(authLua, /log_(?:debug|info|warning|error).*password/i); });