dovecot-config.test.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. import assert from 'node:assert/strict';
  2. import { readFileSync } from 'node:fs';
  3. import { test } from 'node:test';
  4. const compose = readFileSync(new URL('../docker-compose.yml', import.meta.url), 'utf8');
  5. const appService = compose.match(/^ app:\n[\s\S]*?(?=^ dovecot:)/m)?.[0] || '';
  6. const dovecotService = compose.match(/^ dovecot:\n[\s\S]*?(?=^ postfix:)/m)?.[0] || '';
  7. const envExample = readFileSync(new URL('../.env.example', import.meta.url), 'utf8');
  8. const readme = readFileSync(new URL('../README.md', import.meta.url), 'utf8');
  9. const maildirSyncSource = readFileSync(new URL('../src/maildir-sync.js', import.meta.url), 'utf8');
  10. const serverSource = readFileSync(new URL('../src/server.js', import.meta.url), 'utf8');
  11. const authConfig = readFileSync(new URL('../docker/dovecot/auth.conf', import.meta.url), 'utf8');
  12. const mailConfig = readFileSync(new URL('../docker/dovecot/mailhub.conf', import.meta.url), 'utf8');
  13. const sslConfig = readFileSync(new URL('../docker/dovecot/ssl.conf', import.meta.url), 'utf8');
  14. const authLua = readFileSync(new URL('../docker/dovecot/auth.lua', import.meta.url), 'utf8');
  15. const postfixEntrypoint = readFileSync(new URL('../docker/postfix/entrypoint.sh', import.meta.url), 'utf8');
  16. test('Maildir reconciliation defaults to a five-minute polling interval', () => {
  17. assert.match(serverSource, /MAILDIR_SYNC_INTERVAL_MS \|\| 300000/);
  18. assert.match(maildirSyncSource, /intervalMs = 300_000/);
  19. assert.match(maildirSyncSource, /Number\(intervalMs\) \|\| 300_000/);
  20. assert.match(envExample, /^MAILDIR_SYNC_INTERVAL_MS=300000$/m);
  21. assert.match(readme, /MAILDIR_SYNC_INTERVAL_MS[^\n]*`300000` 毫秒(5 分钟)/);
  22. });
  23. test('Compose delegates public IMAP and POP3 ports to rootless Dovecot', () => {
  24. assert.match(compose, /image: dovecot\/dovecot:2\.4\.4/);
  25. for (const mapping of ['143:31143', '993:31993', '110:31110', '995:31995']) {
  26. assert.ok(compose.includes(mapping), `missing Dovecot port mapping ${mapping}`);
  27. }
  28. for (const oldMapping of ['143:143', '993:993', '110:110', '995:995']) {
  29. assert.equal(compose.includes(oldMapping), false, `app still owns ${oldMapping}`);
  30. }
  31. assert.match(compose, /dovecot_internal:\n\s+internal: true/);
  32. assert.match(dovecotService, /networks:\n\s+- dovecot_internal\n\s+- dovecot_public/);
  33. assert.match(compose, /^ dovecot_public:\s*$/m);
  34. assert.doesNotMatch(compose, /dovecot_public:\n\s+internal: true/);
  35. assert.match(appService, /networks:\n\s+- mailhub\n\s+- dovecot_internal/);
  36. assert.doesNotMatch(appService, /dovecot_public/);
  37. assert.match(compose, /file: \.\/data\/secrets\/dovecot_auth_secret/);
  38. assert.match(compose, /MAIL_ACCESS_BACKEND: dovecot/);
  39. assert.match(compose, /MAILDIR_ROOT: \/data\/maildir/);
  40. assert.match(compose, /test -r \/run\/secrets\/dovecot_auth_secret/);
  41. assert.match(compose, /test -s \/run\/secrets\/dovecot_auth_secret/);
  42. assert.match(compose, /test -w \/srv\/vmail/);
  43. assert.match(compose, /doveadm service status imap-login pop3-login/);
  44. });
  45. test('MailHub app healthcheck allows slow production startup before Dovecot depends on it', () => {
  46. assert.match(appService, /healthcheck:[\s\S]*interval: 30s/);
  47. assert.match(appService, /healthcheck:[\s\S]*retries: 4/);
  48. assert.match(appService, /healthcheck:[\s\S]*start_period: 120s/);
  49. assert.match(dovecotService, /condition: service_healthy/);
  50. });
  51. test('Dovecot and app defaults stay modest on low-resource mail hosts', () => {
  52. assert.match(appService, /UV_THREADPOOL_SIZE: \$\{UV_THREADPOOL_SIZE:-2\}/);
  53. assert.match(envExample, /^UV_THREADPOOL_SIZE=2$/m);
  54. assert.match(mailConfig, /service imap-login \{[\s\S]*process_min_avail = 1/);
  55. assert.match(mailConfig, /service imap-login \{[\s\S]*process_limit = 24/);
  56. assert.match(mailConfig, /service pop3-login \{[\s\S]*process_min_avail = 0/);
  57. assert.match(mailConfig, /service pop3-login \{[\s\S]*process_limit = 8/);
  58. });
  59. test('Dovecot uses Lua passdb, a static rootless userdb, and Maildir storage', () => {
  60. assert.match(authConfig, /passdb lua \{/);
  61. assert.match(authConfig, /SUBMISSION_TLS_CERT = %\{env:SUBMISSION_TLS_CERT\}/);
  62. assert.match(authConfig, /SUBMISSION_TLS_KEY = %\{env:SUBMISSION_TLS_KEY\}/);
  63. assert.match(authConfig, /lua_file = \/etc\/dovecot\/auth\.lua/);
  64. assert.match(authConfig, /userdb static \{/);
  65. assert.match(authConfig, /userdb static \{[\s\S]*allow_all_users = yes/);
  66. assert.match(authConfig, /uid = 1000/);
  67. assert.match(authConfig, /gid = 1000/);
  68. assert.match(authConfig, /home = \/srv\/vmail\/%\{user \| lower\}/);
  69. assert.match(mailConfig, /^protocols = imap pop3$/m);
  70. assert.match(mailConfig, /^mail_driver = maildir$/m);
  71. assert.match(mailConfig, /^mail_path = ~\/mail$/m);
  72. assert.match(mailConfig, /^mailbox_list_layout = maildir\+\+$/m);
  73. assert.match(mailConfig, /^mailbox_list_storage_escape_char = \^$/m);
  74. assert.match(mailConfig, /^mailbox_list_utf8 = no$/m);
  75. for (const [mailbox, specialUse] of [
  76. ['Archive', 'Archive'],
  77. ['Drafts', 'Drafts'],
  78. ['Junk', 'Junk'],
  79. ['Sent', 'Sent'],
  80. ['Trash', 'Trash']
  81. ]) {
  82. assert.match(
  83. mailConfig,
  84. new RegExp(`mailbox ${mailbox} \\{[\\s\\S]*?auto = subscribe[\\s\\S]*?special_use = \\\\${specialUse}`)
  85. );
  86. }
  87. assert.match(mailConfig, /service imap-login \{[\s\S]*chroot =/);
  88. assert.match(mailConfig, /service imap-login \{[\s\S]*inet_listener imaps \{[\s\S]*ssl = yes/);
  89. assert.match(mailConfig, /service pop3-login \{[\s\S]*inet_listener pop3s \{[\s\S]*ssl = yes/);
  90. assert.match(sslConfig, /^ssl_server_cert_file = \$ENV:SUBMISSION_TLS_CERT$/m);
  91. assert.match(sslConfig, /^ssl_server_key_file = \$ENV:SUBMISSION_TLS_KEY$/m);
  92. });
  93. test('Postfix hands self-referential MX destinations to the MailHub inbound listener', () => {
  94. assert.match(postfixEntrypoint, /postconf -e "mydestination ="/);
  95. assert.match(postfixEntrypoint, /postconf -e "best_mx_transport = smtp:\[app\]:25"/);
  96. assert.match(appService, /networks:\n\s+- mailhub/);
  97. });
  98. test('Lua passdb sends both IMAP and POP3 to the private auth bridge', () => {
  99. assert.match(authLua, /http:\/\/app:3001\/internal\/dovecot\/auth/);
  100. assert.match(authLua, /\/run\/secrets\/dovecot_auth_secret/);
  101. assert.match(authLua, /first_nonempty_string\(request\.protocol, request\.service\)/);
  102. assert.match(authLua, /request\.remote_ip,[\s\S]*request\.real_remote_ip/);
  103. assert.match(authLua, /protocol ~= "imap" and protocol ~= "pop3"/);
  104. assert.match(authLua, /request_max_attempts = 1/);
  105. assert.match(authLua, /auto_retry = "no"/);
  106. assert.match(authLua, /request_timeout = "30s"/);
  107. assert.match(authLua, /request_absolute_timeout = "30s"/);
  108. assert.match(authLua, /add_header\("connection", "close"\)/);
  109. assert.match(authLua, /status ~= 200[\s\S]*PASSDB_RESULT_INTERNAL_FAILURE/);
  110. assert.doesNotMatch(authLua, /status == (?:401|403|404)/);
  111. assert.match(
  112. authLua,
  113. /payload\.authenticated == false[\s\S]*PASSDB_RESULT_PASSWORD_MISMATCH[\s\S]*payload\.authenticated ~= true[\s\S]*PASSDB_RESULT_INTERNAL_FAILURE/
  114. );
  115. assert.match(authLua, /valid_user\(payload\.user\)/);
  116. assert.match(authLua, /PASSDB_RESULT_OK, \{ user = string\.lower\(payload\.user\) \}/);
  117. assert.doesNotMatch(authLua, /log_(?:debug|info|warning|error).*password/i);
  118. });