Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
import assert from 'node:assert' ;
import BoxError from './boxerror.js' ;
import constants from './constants.js' ;
2026-02-14 15:43:24 +01:00
import database from './database.js' ;
2026-03-12 22:55:28 +05:30
import logger from './logger.js' ;
2026-02-14 15:43:24 +01:00
import dig from './dig.js' ;
import dns from './dns.js' ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
import eventlog from './eventlog.js' ;
2026-02-17 14:06:40 +01:00
import groups from './groups.js' ;
2026-02-14 15:43:24 +01:00
import mailer from './mailer.js' ;
import mailServer from './mailserver.js' ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
import net from 'node:net' ;
2026-02-14 15:43:24 +01:00
import network from './network.js' ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
import nodemailer from 'nodemailer' ;
2026-02-14 15:43:24 +01:00
import notifications from './notifications.js' ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
import path from 'node:path' ;
2026-02-14 15:43:24 +01:00
import platform from './platform.js' ;
2026-04-01 09:40:28 +02:00
import safe from '@cloudron/safetydance' ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
import services from './services.js' ;
import shellModule from './shell.js' ;
import superagent from '@cloudron/superagent' ;
2026-02-14 15:43:24 +01:00
import validator from './validator.js' ;
import _ from './underscore.js' ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
2026-03-12 23:23:23 +05:30
const { log } = logger ( 'mail' ) ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
const shell = shellModule ( 'mail' ) ;
const OWNERTYPE _USER = 'user' ;
const OWNERTYPE _GROUP = 'group' ;
const OWNERTYPE _APP = 'app' ;
const TYPE _MAILBOX = 'mailbox' ;
const TYPE _LIST = 'list' ;
const TYPE _ALIAS = 'alias' ;
2025-10-08 20:11:55 +02:00
2025-03-26 14:47:27 +01:00
const DNS _OPTIONS = { timeout : 20000 , tries : 4 } ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
const REMOVE _MAILBOX _CMD = path . join ( import . meta . dirname , 'scripts/rmmailbox.sh' ) ;
2018-01-20 18:56:17 -08:00
2025-12-05 12:54:48 +01:00
// if you add a field here, listMailboxes* has to be updated
2022-08-17 23:18:38 +02:00
const MAILBOX _FIELDS = [ 'name' , 'type' , 'ownerId' , 'ownerType' , 'aliasName' , 'aliasDomain' , 'creationTime' , 'membersJson' , 'membersOnly' , 'domain' , 'active' , 'enablePop3' , 'storageQuota' , 'messagesQuota' ] . join ( ',' ) ;
2021-10-11 19:51:29 -07:00
const MAILDB _FIELDS = [ 'domain' , 'enabled' , 'mailFromValidation' , 'catchAllJson' , 'relayJson' , 'dkimKeyJson' , 'dkimSelector' , 'bannerJson' ] . join ( ',' ) ;
2021-06-29 14:26:34 -07:00
2021-08-17 15:45:57 -07:00
function postProcessMailbox ( data ) {
data . members = safe . JSON . parse ( data . membersJson ) || [ ] ;
delete data . membersJson ;
data . membersOnly = ! ! data . membersOnly ;
data . active = ! ! data . active ;
2021-10-08 10:15:48 -07:00
data . enablePop3 = ! ! data . enablePop3 ;
2021-08-17 15:45:57 -07:00
return data ;
}
function postProcessAliases ( data ) {
const aliasNames = JSON . parse ( data . aliasNames ) , aliasDomains = JSON . parse ( data . aliasDomains ) ;
delete data . aliasNames ;
delete data . aliasDomains ;
data . aliases = [ ] ;
for ( let i = 0 ; i < aliasNames . length ; i ++ ) { // NOTE: aliasNames is [ null ] when no aliases
if ( aliasNames [ i ] ) data . aliases [ i ] = { name : aliasNames [ i ] , domain : aliasDomains [ i ] } ;
}
return data ;
}
function postProcessDomain ( data ) {
2021-06-29 14:26:34 -07:00
data . enabled = ! ! data . enabled ; // int to boolean
data . mailFromValidation = ! ! data . mailFromValidation ; // int to boolean
data . catchAll = safe . JSON . parse ( data . catchAllJson ) || [ ] ;
delete data . catchAllJson ;
data . relay = safe . JSON . parse ( data . relayJson ) || { provider : 'cloudron-smtp' } ;
delete data . relayJson ;
data . banner = safe . JSON . parse ( data . bannerJson ) || { text : null , html : null } ;
delete data . bannerJson ;
2021-10-11 19:51:29 -07:00
data . dkimKey = safe . JSON . parse ( data . dkimKeyJson ) || null ;
delete data . dkimKeyJson ;
2021-06-29 14:26:34 -07:00
return data ;
}
2018-04-03 09:36:41 -07:00
function validateName ( name ) {
assert . strictEqual ( typeof name , 'string' ) ;
2018-01-25 18:03:02 +01:00
2019-10-24 13:34:14 -07:00
if ( name . length < 1 ) return new BoxError ( BoxError . BAD _FIELD , 'mailbox name must be atleast 1 char' ) ;
if ( name . length >= 200 ) return new BoxError ( BoxError . BAD _FIELD , 'mailbox name too long' ) ;
2018-01-25 18:03:02 +01:00
2022-01-05 09:55:08 -08:00
// also need to consider valid LDAP characters here (e.g '+' is reserved). keep hyphen at the end so it doesn't become a range.
if ( /[^a-zA-Z0-9._-]/ . test ( name ) ) return new BoxError ( BoxError . BAD _FIELD , 'mailbox name can only contain alphanumerals, dot, hyphen or underscore' ) ;
2018-01-25 18:03:02 +01:00
return null ;
}
2022-08-18 13:21:24 +02:00
function validateAlias ( name ) {
assert . strictEqual ( typeof name , 'string' ) ;
if ( name . length < 1 ) return new BoxError ( BoxError . BAD _FIELD , 'mailbox name must be atleast 1 char' ) ;
if ( name . length >= 200 ) return new BoxError ( BoxError . BAD _FIELD , 'mailbox name too long' ) ;
// also need to consider valid LDAP characters here (e.g '+' is reserved). keep hyphen at the end so it doesn't become a range.
if ( /[^a-zA-Z0-9._*-]/ . test ( name ) ) return new BoxError ( BoxError . BAD _FIELD , 'mailbox name can only contain alphanumerals, dot, hyphen, asterisk or underscore' ) ;
return null ;
}
2022-05-31 17:53:09 -07:00
function validateDisplayName ( name ) {
assert . strictEqual ( typeof name , 'string' ) ;
if ( name . length < 1 ) return new BoxError ( BoxError . BAD _FIELD , 'mailbox display name must be atleast 1 char' ) ;
2022-06-01 08:13:19 -07:00
if ( name . length >= 100 ) return new BoxError ( BoxError . BAD _FIELD , 'mailbox display name too long' ) ;
2022-11-04 08:50:47 +01:00
// technically only ":" is disallowed it seems (https://www.rfc-editor.org/rfc/rfc5322#section-2.2)
// in https://www.rfc-editor.org/rfc/rfc2822.html, display-name is a "phrase"
2026-02-09 11:01:47 +01:00
if ( /["<>)(;\\@:]/ . test ( name ) ) return new BoxError ( BoxError . BAD _FIELD , 'mailbox display name is not valid' ) ;
2022-05-31 17:53:09 -07:00
return null ;
}
2025-10-08 20:11:55 +02:00
function validateOwnerType ( type ) {
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
const OWNERTYPES = [ OWNERTYPE _USER , OWNERTYPE _GROUP , OWNERTYPE _APP ] ;
2025-10-08 20:11:55 +02:00
return OWNERTYPES . includes ( type ) ;
}
2025-10-08 20:01:18 +02:00
async function getDomain ( domain ) {
assert . strictEqual ( typeof domain , 'string' ) ;
const result = await database . query ( ` SELECT ${ MAILDB _FIELDS } FROM mail WHERE domain = ? ` , [ domain ] ) ;
if ( result . length === 0 ) return null ;
return postProcessDomain ( result [ 0 ] ) ;
}
async function updateDomain ( domain , data ) {
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof data , 'object' ) ;
const args = [ ] ;
const fields = [ ] ;
for ( const k in data ) {
if ( k === 'catchAll' || k === 'banner' ) {
fields . push ( ` ${ k } Json = ? ` ) ;
args . push ( JSON . stringify ( data [ k ] ) ) ;
} else if ( k === 'relay' ) {
fields . push ( 'relayJson = ?' ) ;
args . push ( JSON . stringify ( data [ k ] ) ) ;
} else {
fields . push ( k + ' = ?' ) ;
args . push ( data [ k ] ) ;
}
}
args . push ( domain ) ;
const result = await database . query ( 'UPDATE mail SET ' + fields . join ( ', ' ) + ' WHERE domain=?' , args ) ;
if ( result . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'Mail domain not found' ) ;
}
async function listDomains ( ) {
const results = await database . query ( ` SELECT ${ MAILDB _FIELDS } FROM mail ORDER BY domain ` ) ;
results . forEach ( function ( result ) { postProcessDomain ( result ) ; } ) ;
return results ;
}
2025-11-19 16:29:53 +01:00
async function checkOutboundPort25 ( family ) {
const ip = family === 4 ? await network . getIPv4 ( ) : await network . getIPv6 ( ) ;
if ( ip === null ) return ; // ipv4/ipv6 is disabled
return await new Promise ( ( resolve , reject ) => {
2021-08-27 09:52:24 -07:00
const client = new net . Socket ( ) ;
client . setTimeout ( 5000 ) ;
2025-11-19 16:29:53 +01:00
client . connect ( { port : 25 , host : constants . PORT25 _CHECK _SERVER , family } ) ; // family is 4 to keep it predictable
2021-08-27 09:52:24 -07:00
client . on ( 'connect' , function ( ) {
client . destroy ( ) ; // do not use end() because it still triggers timeout
2025-11-19 16:29:53 +01:00
resolve ( ) ;
2021-08-27 09:52:24 -07:00
} ) ;
client . on ( 'timeout' , function ( ) {
client . destroy ( ) ;
2025-11-19 16:29:53 +01:00
reject ( new Error ( ` IPv ${ family } connect to ${ constants . PORT25 _CHECK _SERVER } timed out ` ) ) ;
2021-08-27 09:52:24 -07:00
} ) ;
client . on ( 'error' , function ( error ) {
client . destroy ( ) ;
2025-11-19 16:29:53 +01:00
reject ( new Error ( ` IPv ${ family } connect to ${ constants . PORT25 _CHECK _SERVER } failed: ${ error . message } ` ) ) ;
2021-08-27 09:52:24 -07:00
} ) ;
2017-06-28 21:38:51 -05:00
} ) ;
}
2021-08-27 09:52:24 -07:00
async function checkSmtpRelay ( relay ) {
2025-06-27 18:51:03 +02:00
assert . strictEqual ( typeof relay , 'object' ) ;
if ( relay . provider === 'noop' ) return { status : 'skipped' , message : 'Outbound disabled' } ;
2025-11-19 16:29:53 +01:00
if ( relay . provider === 'cloudron-smtp' ) {
const results = await Promise . allSettled ( [ checkOutboundPort25 ( 4 ) , checkOutboundPort25 ( 6 ) ] ) ;
2025-11-20 16:03:13 +01:00
if ( results [ 0 ] . status === 'fulfilled' && results [ 1 ] . status === 'fulfilled' ) return { status : 'passed' , message : 'Port 25 outbound is unblocked' } ;
2025-11-22 08:18:18 +01:00
if ( results [ 0 ] . status === 'fulfilled' ) return { status : 'passed' , message : 'IPv4 port 25 outbound is unblocked. IPv6 port 25 outbound is blocked and delivery to IPv6 only servers will fail' } ; // ipv6 only servers are not really common
if ( results [ 1 ] . status === 'fulfilled' ) return { status : 'failed' , message : ` IPv4 port 25 outbound is blocked: ${ results [ 0 ] . reason . message } ` } ; // only IPv6 worked
2025-11-20 16:03:13 +01:00
return { status : 'failed' , message : ` Port 25 outbound is blocked. ${ results [ 0 ] . reason . message } ` } ; // both failed
2025-11-19 16:29:53 +01:00
}
2017-06-28 17:06:12 -05:00
2021-08-27 09:52:24 -07:00
const options = {
2018-07-23 17:05:15 -07:00
connectionTimeout : 5000 ,
greetingTimeout : 5000 ,
2017-06-28 17:06:12 -05:00
host : relay . host ,
2022-09-14 17:41:01 +02:00
port : relay . port ,
2025-06-27 10:22:17 +02:00
secure : false , // true is for implicit TLS, false is for maybe STARTTLS
requireTLS : true // force STARTTLS . haraka only supports STARTTLS in outbound plugin
2019-04-22 14:41:44 +02:00
} ;
// only set auth if either username or password is provided, some relays auth based on IP (range)
if ( relay . username || relay . password ) {
options . auth = {
2017-06-28 17:06:12 -05:00
user : relay . username ,
pass : relay . password
2019-04-22 14:41:44 +02:00
} ;
}
2019-04-23 15:19:33 -07:00
if ( relay . acceptSelfSignedCerts ) options . tls = { rejectUnauthorized : false } ;
2022-09-14 17:41:01 +02:00
const transporter = nodemailer . createTransport ( options ) ;
2017-06-28 17:06:12 -05:00
2022-09-14 17:41:01 +02:00
const [ error ] = await safe ( transporter . verify ( ) ) ;
2025-06-27 18:51:03 +02:00
const result = {
status : error ? 'failed' : 'passed' ,
message : error ? error . message : ` Connection to ${ relay . host } : ${ relay . port } succeeded `
} ;
2017-06-28 17:06:12 -05:00
2021-08-27 09:52:24 -07:00
return result ;
2017-06-28 17:06:12 -05:00
}
2025-10-08 20:01:18 +02:00
function txtToDict ( txt ) {
const dict = { } ;
txt . split ( ';' ) . forEach ( function ( v ) {
const p = v . trim ( ) . split ( '=' ) ;
dict [ p [ 0 ] ] = p [ 1 ] ;
} ) ;
return dict ;
}
2021-08-27 09:52:24 -07:00
async function checkDkim ( mailDomain ) {
2019-06-10 12:23:29 -07:00
assert . strictEqual ( typeof mailDomain , 'object' ) ;
2025-06-27 18:51:03 +02:00
if ( mailDomain . relay . provider === 'noop' ) return { status : 'skipped' , message : 'Outbound disabled' } ;
if ( mailDomain . relay . provider !== 'cloudron-smtp' ) return { status : 'skipped' , message : 'DKIM check skipped, email is sent through a relay service' } ;
const { domain } = mailDomain ;
const result = {
2019-06-10 12:23:29 -07:00
domain : ` ${ mailDomain . dkimSelector } ._domainkey. ${ domain } ` ,
name : ` ${ mailDomain . dkimSelector } ._domainkey ` ,
2017-06-28 21:38:51 -05:00
type : 'TXT' ,
expected : null ,
value : null ,
2025-06-27 18:51:03 +02:00
status : 'failed' ,
message : ''
2017-06-28 21:38:51 -05:00
} ;
2017-06-28 17:06:12 -05:00
2021-10-11 19:51:29 -07:00
const publicKey = mailDomain . dkimKey . publicKey . split ( '\n' ) . slice ( 1 , - 2 ) . join ( '' ) ; // remove header, footer and new lines
2017-06-28 17:06:12 -05:00
2025-06-27 18:51:03 +02:00
result . expected = ` v=DKIM1; t=s; p= ${ publicKey } ` ;
2017-06-28 17:06:12 -05:00
2025-06-27 18:51:03 +02:00
const [ error , txtRecords ] = await safe ( dig . resolve ( result . domain , result . type , DNS _OPTIONS ) ) ;
if ( error ) return Object . assign ( result , { status : 'failed' , message : error . message } ) ;
if ( txtRecords . length === 0 ) return Object . assign ( result , { status : 'failed' , message : 'No DKIM record' } ) ;
2017-06-28 17:06:12 -05:00
2025-06-27 18:51:03 +02:00
result . value = txtRecords [ 0 ] . join ( '' ) ;
const actual = txtToDict ( result . value ) ;
result . status = actual . p === publicKey ? 'passed' : 'failed' ;
2017-06-28 17:06:12 -05:00
2025-06-27 18:51:03 +02:00
return result ;
2017-06-28 21:38:51 -05:00
}
2017-06-28 17:06:12 -05:00
2025-06-27 18:51:03 +02:00
async function checkSpf ( mailDomain , mailFqdn ) {
assert . strictEqual ( typeof mailDomain , 'object' ) ;
2019-01-31 15:08:14 -08:00
assert . strictEqual ( typeof mailFqdn , 'string' ) ;
2025-06-27 18:51:03 +02:00
if ( mailDomain . relay . provider === 'noop' ) return { status : 'skipped' , message : 'Outbound disabled' } ;
if ( mailDomain . relay . provider !== 'cloudron-smtp' ) return { status : 'skipped' , message : 'SPF check skipped. Please check that the relay provider has correct SPF settings for this domain' } ;
const result = {
domain : mailDomain . domain ,
2018-07-24 14:03:39 -07:00
name : '@' ,
2017-06-28 21:38:51 -05:00
type : 'TXT' ,
value : null ,
2023-08-04 21:37:38 +05:30
expected : ` v=spf1 a: ${ mailFqdn } ~all ` ,
2025-06-27 18:51:03 +02:00
status : 'failed' ,
message : ''
2017-06-28 21:38:51 -05:00
} ;
2025-06-27 18:51:03 +02:00
const [ error , txtRecords ] = await safe ( dig . resolve ( result . domain , result . type , DNS _OPTIONS ) ) ;
2025-06-28 12:57:05 +02:00
if ( error ) return Object . assign ( result , { status : 'failed' , message : error . message } ) ;
2017-06-28 21:38:51 -05:00
2021-08-27 09:52:24 -07:00
let i ;
for ( i = 0 ; i < txtRecords . length ; i ++ ) {
2024-05-23 10:58:59 +02:00
const txtRecord = txtRecords [ i ] . join ( '' ) ; // https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
2021-08-27 09:52:24 -07:00
if ( txtRecord . indexOf ( 'v=spf1 ' ) !== 0 ) continue ; // not SPF
2025-06-27 18:51:03 +02:00
result . value = txtRecord ;
result . status = result . value . indexOf ( ` a: ${ mailFqdn } ` ) !== - 1 ? 'passed' : 'failed' ;
2021-08-27 09:52:24 -07:00
break ;
}
2017-06-28 17:06:12 -05:00
2025-06-27 18:51:03 +02:00
if ( result . status === 'passed' ) {
result . expected = result . value ;
2021-08-27 09:52:24 -07:00
} else if ( i !== txtRecords . length ) {
2025-06-27 18:51:03 +02:00
result . expected = ` v=spf1 a: ${ mailFqdn } ` + result . value . slice ( 'v=spf1 ' . length ) ;
2021-08-27 09:52:24 -07:00
}
2017-06-28 17:06:12 -05:00
2025-06-27 18:51:03 +02:00
return result ;
2017-06-28 21:38:51 -05:00
}
2017-06-28 17:06:12 -05:00
2025-06-27 18:51:03 +02:00
async function checkMx ( mailDomain , mailFqdn ) {
assert . strictEqual ( typeof mailDomain , 'object' ) ;
2019-01-31 15:08:14 -08:00
assert . strictEqual ( typeof mailFqdn , 'string' ) ;
2025-06-27 18:51:03 +02:00
if ( ! mailDomain . enabled ) return { status : 'skipped' , message : 'MX check skipped, server does not handle incoming email for this domain' } ;
const { domain } = mailDomain ;
const result = {
2023-06-30 11:35:34 +05:30
domain ,
2018-07-24 14:03:39 -07:00
name : '@' ,
2017-06-28 21:38:51 -05:00
type : 'MX' ,
value : null ,
2023-08-04 21:37:38 +05:30
expected : ` 10 ${ mailFqdn } . ` ,
2025-06-27 18:51:03 +02:00
status : 'failed' ,
message : ''
2017-06-28 21:38:51 -05:00
} ;
2025-06-27 18:51:03 +02:00
const [ error , mxRecords ] = await safe ( dig . resolve ( result . domain , result . type , DNS _OPTIONS ) ) ;
if ( error ) return Object . assign ( result , { status : 'failed' , message : error . message } ) ;
if ( mxRecords . length === 0 ) return Object . assign ( result , { status : 'failed' , message : 'No MX record' } ) ;
2017-06-28 21:38:51 -05:00
2025-06-27 18:51:03 +02:00
result . status = mxRecords . some ( mx => mx . exchange === mailFqdn ) ? 'passed' : 'failed' ; // this lets use change priority and/or setup backup MX
result . value = mxRecords . map ( function ( r ) { return r . priority + ' ' + r . exchange + '.' ; } ) . join ( ' ' ) ;
2019-05-20 17:56:16 -07:00
2025-06-27 18:51:03 +02:00
if ( result . status === 'passed' ) return result ; // MX record is "my."
2019-05-20 17:56:16 -07:00
2021-08-27 09:52:24 -07:00
// cloudflare might create a conflict subdomain (https://support.cloudflare.com/hc/en-us/articles/360020296512-DNS-Troubleshooting-FAQ)
2022-02-03 16:15:14 -08:00
const [ error2 , mxIps ] = await safe ( dig . resolve ( mxRecords [ 0 ] . exchange , 'A' , DNS _OPTIONS ) ) ;
2025-06-27 18:51:03 +02:00
if ( error2 || mxIps . length !== 1 ) return result ;
2019-05-20 17:56:16 -07:00
2023-08-03 13:38:42 +05:30
const [ error3 , ip ] = await safe ( network . getIPv4 ( ) ) ;
2025-06-27 18:51:03 +02:00
if ( error3 ) return result ;
2019-05-20 17:56:16 -07:00
2025-06-27 18:51:03 +02:00
result . status = mxIps [ 0 ] === ip ? 'passed' : 'failed' ;
2017-06-28 17:06:12 -05:00
2025-06-27 18:51:03 +02:00
return result ;
2017-06-28 21:38:51 -05:00
}
2017-06-28 17:06:12 -05:00
2025-06-27 18:51:03 +02:00
async function checkDmarc ( mailDomain ) {
assert . strictEqual ( typeof mailDomain , 'object' ) ;
2021-08-27 09:52:24 -07:00
2025-06-27 18:51:03 +02:00
if ( ! mailDomain . enabled ) return { status : 'skipped' , message : 'DMARC check skipped, server does not handle incoming email for this domain' } ;
const { domain } = mailDomain ;
const result = {
2023-06-30 11:35:34 +05:30
domain : ` _dmarc. ${ domain } ` ,
2018-07-24 14:03:39 -07:00
name : '_dmarc' ,
2017-06-28 21:38:51 -05:00
type : 'TXT' ,
value : null ,
2018-02-08 10:21:31 -08:00
expected : 'v=DMARC1; p=reject; pct=100' ,
2025-06-27 18:51:03 +02:00
status : 'failed' ,
message : ''
2017-06-28 21:38:51 -05:00
} ;
2025-06-27 18:51:03 +02:00
const [ error , txtRecords ] = await safe ( dig . resolve ( result . domain , result . type , DNS _OPTIONS ) ) ;
if ( error ) return Object . assign ( result , { status : 'failed' , message : error . message } ) ;
2025-06-28 12:57:05 +02:00
if ( txtRecords . length === 0 ) return Object . assign ( result , { status : 'failed' , message : 'No DMARC record' } ) ;
2017-06-28 21:38:51 -05:00
2025-06-27 18:51:03 +02:00
result . value = txtRecords [ 0 ] . join ( '' ) ;
const actual = txtToDict ( result . value ) ;
result . status = actual . v === 'DMARC1' ? 'passed' : 'failed' ; // see box#666
2017-06-28 17:06:12 -05:00
2025-06-27 18:51:03 +02:00
return result ;
2017-06-28 21:38:51 -05:00
}
2017-06-28 17:06:12 -05:00
2025-06-28 13:19:03 +02:00
function reverseIPv6 ( ipv6 ) {
const parts = ipv6 . split ( '::' ) ;
const left = parts [ 0 ] . split ( ':' ) ;
const right = parts [ 1 ] ? parts [ 1 ] . split ( ':' ) : [ ] ;
const fill = new Array ( 8 - left . length - right . length ) . fill ( '0' ) ;
const full = [ ... left , ... fill , ... right ] ;
const expanded = full . map ( part => part . padStart ( 4 , '0' ) ) . join ( '' ) ;
const reversed = expanded . split ( '' ) . reverse ( ) . join ( '' ) ;
const reversedWithDots = reversed . split ( '' ) . join ( '.' ) ;
return reversedWithDots ;
}
2025-06-27 18:51:03 +02:00
async function checkPtr6 ( mailDomain , mailFqdn ) {
assert . strictEqual ( typeof mailDomain , 'object' ) ;
2025-01-08 17:20:33 +01:00
assert . strictEqual ( typeof mailFqdn , 'string' ) ;
2025-06-27 18:51:03 +02:00
if ( mailDomain . relay . provider === 'noop' ) return { status : 'skipped' , message : 'Outbound disabled' } ;
if ( mailDomain . relay . provider !== 'cloudron-smtp' ) return { status : 'skipped' , message : 'PTR6 check was skipped, email is sent through a relay service' } ;
const result = {
2025-01-08 17:20:33 +01:00
domain : null ,
name : null ,
type : 'PTR' ,
value : null ,
expected : mailFqdn , // any trailing '.' is added by client software (https://lists.gt.net/spf/devel/7918)
2025-06-27 18:51:03 +02:00
status : 'failed' ,
message : ''
2025-01-08 17:20:33 +01:00
} ;
const [ error , ip ] = await safe ( network . getIPv6 ( ) ) ;
2025-06-27 18:51:03 +02:00
if ( error ) return Object . assign ( result , { status : 'failed' , message : error . message } ) ;
if ( ip === null ) return Object . assign ( result , { status : 'skipped' , message : 'PTR6 check was skipped, server has no IPv6' } ) ;
2025-01-08 17:20:33 +01:00
2025-06-28 13:19:03 +02:00
const reversed = reverseIPv6 ( ip ) ;
result . domain = ` ${ reversed } .ip6.arpa ` ;
2025-06-27 18:51:03 +02:00
result . name = ip ;
2025-01-08 17:20:33 +01:00
2025-06-27 18:51:03 +02:00
const [ error2 , ptrRecords ] = await safe ( dig . resolve ( result . domain , 'PTR' , DNS _OPTIONS ) ) ;
if ( error2 ) return Object . assign ( result , { status : 'failed' , message : error2 . message } ) ;
if ( ptrRecords . length === 0 ) return Object . assign ( result , { status : 'failed' , message : 'No PTR6 record' } ) ;
2025-01-08 17:20:33 +01:00
2025-06-27 18:51:03 +02:00
result . value = ptrRecords . join ( ' ' ) ;
result . status = ptrRecords . some ( function ( v ) { return v === result . expected ; } ) ? 'passed' : 'failed' ;
2025-01-08 17:20:33 +01:00
2025-06-27 18:51:03 +02:00
return result ;
2025-01-08 17:20:33 +01:00
}
2025-06-27 18:51:03 +02:00
async function checkPtr4 ( mailDomain , mailFqdn ) {
assert . strictEqual ( typeof mailDomain , 'object' ) ;
2019-01-31 15:08:14 -08:00
assert . strictEqual ( typeof mailFqdn , 'string' ) ;
2025-06-27 18:51:03 +02:00
if ( mailDomain . relay . provider === 'noop' ) return { status : 'skipped' , message : 'Outbound disabled' } ;
if ( mailDomain . relay . provider !== 'cloudron-smtp' ) return { status : 'skipped' , message : 'PTR4 check was skipped, email is sent through a relay service' } ;
const result = {
2017-06-28 21:38:51 -05:00
domain : null ,
2019-10-25 10:05:53 -07:00
name : null ,
2017-06-28 21:38:51 -05:00
type : 'PTR' ,
value : null ,
2019-01-31 15:08:14 -08:00
expected : mailFqdn , // any trailing '.' is added by client software (https://lists.gt.net/spf/devel/7918)
2025-06-27 18:51:03 +02:00
status : 'failed' ,
message : ''
2017-06-28 21:38:51 -05:00
} ;
2017-06-28 17:06:12 -05:00
2023-08-03 13:38:42 +05:30
const [ error , ip ] = await safe ( network . getIPv4 ( ) ) ;
2025-06-27 18:51:03 +02:00
if ( error ) return Object . assign ( result , { status : 'failed' , message : error . message } ) ;
if ( ip === null ) return Object . assign ( result , { status : 'skipped' , message : 'PTR4 check was skipped, server has no IPv4' } ) ;
2017-06-28 17:06:12 -05:00
2025-06-27 18:51:03 +02:00
result . domain = ip . split ( '.' ) . reverse ( ) . join ( '.' ) + '.in-addr.arpa' ;
result . name = ip ;
2017-06-28 17:06:12 -05:00
2025-06-27 18:51:03 +02:00
const [ error2 , ptrRecords ] = await safe ( dig . resolve ( result . domain , 'PTR' , DNS _OPTIONS ) ) ;
if ( error2 ) return Object . assign ( result , { status : 'failed' , message : error2 . message } ) ;
if ( ptrRecords . length === 0 ) return Object . assign ( result , { status : 'failed' , message : 'No PTR4 record' } ) ;
2017-06-28 17:06:12 -05:00
2025-06-27 18:51:03 +02:00
result . value = ptrRecords . join ( ' ' ) ;
result . status = ptrRecords . some ( function ( v ) { return v === result . expected ; } ) ? 'passed' : 'failed' ;
2017-06-28 17:06:12 -05:00
2025-06-27 18:51:03 +02:00
return result ;
2017-06-28 21:38:51 -05:00
}
2017-06-28 17:06:12 -05:00
2025-06-27 18:51:03 +02:00
// https://raw.githubusercontent.com/jawsome/node-dnsbl/master/list.json https://multirbl.valli.org/list/
2017-09-08 11:50:11 -07:00
const RBL _LIST = [
2018-03-05 14:26:53 -08:00
{
2025-06-28 13:19:03 +02:00
name : 'Barracuda' ,
dns : 'b.barracudacentral.org' ,
site : 'https://barracudacentral.org/' ,
removal : 'http://www.barracudacentral.org/rbl/removal-request' ,
2017-09-08 11:50:11 -07:00
} ,
{
2025-06-28 13:19:03 +02:00
name : 'Multi SURBL' ,
dns : 'multi.surbl.org' ,
site : 'http://www.surbl.org' ,
removal : 'https://surbl.org/surbl-analysis' ,
2017-09-08 11:50:11 -07:00
} ,
{
2025-06-28 13:19:03 +02:00
name : 'Passive Spam Block List' ,
dns : 'psbl.surriel.com' ,
site : 'https://psbl.org' ,
removal : 'https://psbl.org' ,
2017-09-08 11:50:11 -07:00
} ,
{
2025-06-28 13:19:03 +02:00
name : 'SpamCop' ,
dns : 'bl.spamcop.net' ,
site : 'http://spamcop.net' ,
removal : 'https://www.spamcop.net/bl.shtml' ,
2018-03-05 14:26:53 -08:00
} ,
{
2025-06-28 13:19:03 +02:00
name : 'SpamHaus Zen' ,
dns : 'zen.spamhaus.org' ,
site : 'https://www.spamhaus.org/blocklists/zen-blocklist/' ,
removal : 'https://check.spamhaus.org/' ,
ipv6 : true
2017-09-08 11:50:11 -07:00
} ,
{
2025-06-28 13:19:03 +02:00
name : 'The Unsubscribe Blacklist(UBL)' ,
dns : 'ubl.unsubscore.com ' ,
site : 'https://blacklist.lashback.com/' ,
removal : 'https://blacklist.lashback.com/' ,
2017-09-08 11:50:11 -07:00
} ,
{
2025-06-28 13:19:03 +02:00
name : 'UCEPROTECT Network' ,
dns : 'dnsbl-1.uceprotect.net' , // it has 3 "zones"
site : 'http://www.uceprotect.net/en' ,
removal : 'https://www.uceprotect.net/en/index.php?m=7&s=0' ,
2017-09-08 11:50:11 -07:00
}
] ;
2025-06-28 16:52:37 +02:00
// https://tools.ietf.org/html/rfc5782
2025-06-28 13:19:03 +02:00
async function checkRbl ( type , mailDomain ) {
assert . strictEqual ( typeof type , 'string' ) ;
2025-06-27 18:51:03 +02:00
assert . strictEqual ( typeof mailDomain , 'object' ) ;
if ( mailDomain . relay . provider === 'noop' ) return { status : 'skipped' , message : 'Outbound disabled' } ;
if ( mailDomain . relay . provider !== 'cloudron-smtp' ) return { status : 'skipped' , message : 'RBL check was skipped, email is sent through a relay service' } ;
const { domain } = mailDomain ;
2025-06-28 13:19:03 +02:00
const [ error , ip ] = await safe ( type === 'ipv4' ? network . getIPv4 ( ) : network . getIPv6 ( ) ) ;
if ( error ) return { status : 'failed' , ip : null , servers : [ ] , message : ` Unable to determine server ${ type } : ${ error . message } ` } ;
if ( ip === null ) return { status : 'skipped' , ip : null , servers : [ ] , message : ` RBL check was skipped, server has no ${ type } ` } ;
2017-09-08 11:50:11 -07:00
2025-06-28 13:19:03 +02:00
const flippedIp = type === 'ipv4' ? ip . split ( '.' ) . reverse ( ) . join ( '.' ) : reverseIPv6 ( ip ) ;
2017-09-08 11:50:11 -07:00
2024-05-23 09:52:05 +02:00
const blockedServers = [ ] ;
2021-08-27 09:52:24 -07:00
for ( const rblServer of RBL _LIST ) {
2025-06-28 13:19:03 +02:00
if ( type === 'ipv6' && rblServer [ type ] !== true ) continue ; // all support ipv4
2026-02-18 08:18:37 +01:00
const [ rblError , records ] = await safe ( dig . resolve ( ` ${ flippedIp } . ${ rblServer . dns } ` , 'A' , DNS _OPTIONS ) ) ;
if ( rblError || records . length === 0 ) continue ; // not listed
2017-09-08 11:50:11 -07:00
2026-03-12 22:55:28 +05:30
log ( ` checkRbl ( ${ domain } ) flippedIp: ${ flippedIp } is in the blocklist of ${ rblServer . dns } : ${ JSON . stringify ( records ) } ` ) ;
2017-09-08 11:50:11 -07:00
2023-05-25 11:27:23 +02:00
const result = Object . assign ( { } , rblServer ) ;
2017-09-08 11:50:11 -07:00
2025-06-28 13:19:03 +02:00
const [ error2 , txtRecords ] = await safe ( dig . resolve ( ` ${ flippedIp } . ${ rblServer . dns } ` , 'TXT' , DNS _OPTIONS ) ) ;
2025-11-28 12:01:50 +01:00
result . txtRecords = error2 || ! txtRecords ? [ ] : txtRecords . map ( x => x . join ( '' ) ) ;
2017-09-08 11:50:11 -07:00
2026-03-12 22:55:28 +05:30
log ( ` checkRbl ( ${ domain } ) error: ${ error2 ? . message || null } txtRecords: ${ JSON . stringify ( txtRecords ) } ` ) ;
2017-09-08 11:50:11 -07:00
2024-05-23 09:52:05 +02:00
blockedServers . push ( result ) ;
2021-08-27 09:52:24 -07:00
}
2017-09-08 11:50:11 -07:00
2025-06-28 13:19:03 +02:00
return {
status : blockedServers . length === 0 ? 'passed' : 'failed' ,
ip ,
servers : blockedServers ,
2025-06-28 16:52:37 +02:00
message : ''
2025-06-28 13:19:03 +02:00
} ;
2017-09-13 22:39:42 -07:00
}
2021-08-27 09:52:24 -07:00
async function getStatus ( domain ) {
2018-01-21 00:40:30 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
2017-09-13 22:39:42 -07:00
2023-08-04 21:37:38 +05:30
const { fqdn } = await mailServer . getLocation ( ) ;
2017-09-13 22:39:42 -07:00
2021-08-27 09:52:24 -07:00
const mailDomain = await getDomain ( domain ) ;
if ( ! mailDomain ) throw new BoxError ( BoxError . NOT _FOUND , 'mail domain not found' ) ;
2017-09-13 22:39:42 -07:00
2025-06-27 18:51:03 +02:00
// mx/dmarc/dkim/spf/ptr: { expected, value, name, domain, type, status } }
2025-07-15 11:20:05 +02:00
// rbl4/rbl6: { status, ip, servers: [{name,site,dns}]}
2025-06-27 18:51:03 +02:00
// relay: { status, message } always checked
const results = { } ;
const checks = [
{ what : 'mx' , promise : checkMx ( mailDomain , fqdn ) } ,
{ what : 'dmarc' , promise : checkDmarc ( mailDomain ) } ,
{ what : 'spf' , promise : checkSpf ( mailDomain , fqdn ) } ,
{ what : 'dkim' , promise : checkDkim ( mailDomain ) } ,
{ what : 'ptr4' , promise : checkPtr4 ( mailDomain , fqdn ) } ,
{ what : 'ptr6' , promise : checkPtr6 ( mailDomain , fqdn ) } ,
2025-06-28 13:19:03 +02:00
{ what : 'rbl4' , promise : checkRbl ( 'ipv4' , mailDomain ) } ,
{ what : 'rbl6' , promise : checkRbl ( 'ipv6' , mailDomain ) } ,
2025-06-27 18:51:03 +02:00
{ what : 'relay' , promise : checkSmtpRelay ( mailDomain . relay ) }
] ;
const responses = await Promise . allSettled ( checks . map ( c => c . promise ) ) ; // wait for all the checks and record the result
2021-08-27 09:52:24 -07:00
for ( let i = 0 ; i < checks . length ; i ++ ) {
const response = responses [ i ] , check = checks [ i ] ;
if ( response . status !== 'fulfilled' ) {
2026-03-12 22:55:28 +05:30
log ( ` check ${ check . what } was rejected. This is not expected. reason: ${ response . reason } ` ) ;
2021-08-27 09:52:24 -07:00
continue ;
2017-09-13 22:39:42 -07:00
}
2026-03-12 22:55:28 +05:30
if ( response . value . message ) log ( ` ${ check . what } ( ${ domain } ): ${ response . value . message } ` ) ;
2021-08-27 09:52:24 -07:00
safe . set ( results , checks [ i ] . what , response . value || { } ) ;
}
return results ;
2017-09-08 11:50:11 -07:00
}
2018-01-20 18:56:17 -08:00
2021-08-17 15:45:57 -07:00
async function checkConfiguration ( ) {
2024-05-23 09:52:05 +02:00
const messages = { } ;
2019-02-28 16:46:30 -08:00
2021-08-17 15:45:57 -07:00
const allDomains = await listDomains ( ) ;
2019-02-28 16:46:30 -08:00
2021-08-17 15:45:57 -07:00
for ( const domainObject of allDomains ) {
2021-09-03 11:38:21 -07:00
const result = await getStatus ( domainObject . domain ) ;
2019-02-28 16:46:30 -08:00
2024-05-23 09:52:05 +02:00
const message = [ ] ;
2019-02-28 16:46:30 -08:00
2025-06-28 12:57:05 +02:00
[ 'mx' , 'dmarc' , 'spf' , 'dkim' , 'ptr4' , 'ptr6' ] . forEach ( ( type ) => {
const record = result [ type ] ;
if ( record . status === 'failed' ) message . push ( ` ${ type . toUpperCase ( ) } DNS record ( ${ record . type } ) did not match. \n * Hostname: \` ${ record . name } \` \n * Expected: \` ${ record . expected } \` \n * Actual: \` ${ record . value || record . message } \` ` ) ;
2021-08-17 15:45:57 -07:00
} ) ;
2025-06-28 12:57:05 +02:00
if ( result . relay . status === 'failed' ) message . push ( ` Relay error: ${ result . relay . message } ` ) ;
if ( result . rbl4 . status === 'failed' ) {
2021-08-17 15:45:57 -07:00
const servers = result . rbl . servers . map ( ( bs ) => ` [ ${ bs . name } ]( ${ bs . site } ) ` ) ; // in markdown
2024-05-23 09:52:05 +02:00
message . push ( ` This server's IP \` ${ result . rbl . ip } \` is blocked in the following servers - ${ servers . join ( ', ' ) } ` ) ;
2021-08-17 15:45:57 -07:00
}
2019-02-28 16:46:30 -08:00
2021-08-17 15:45:57 -07:00
if ( message . length ) messages [ domainObject . domain ] = message ;
}
2019-02-28 16:46:30 -08:00
2021-08-17 15:45:57 -07:00
// create bulleted list for each domain
let markdownMessage = '' ;
Object . keys ( messages ) . forEach ( ( domain ) => {
markdownMessage += ` ** ${ domain } ** \n ` ;
markdownMessage += messages [ domain ] . map ( ( m ) => ` * ${ m } \n ` ) . join ( '' ) ;
markdownMessage += '\n\n' ;
} ) ;
2019-02-28 16:46:30 -08:00
2026-01-10 19:59:27 +01:00
if ( markdownMessage ) markdownMessage += 'See [Cloudron Documentaion - Email PTR record](https://docs.cloudron.io/email#ptr-record) for more information.\n' ;
2019-03-01 11:24:10 -08:00
2023-04-04 11:21:04 +02:00
return { status : markdownMessage === '' , message : markdownMessage } ;
2019-02-28 16:46:30 -08:00
}
2018-01-25 13:48:53 -08:00
// https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
2021-08-27 09:52:24 -07:00
async function txtRecordsWithSpf ( domain , mailFqdn ) {
2018-02-06 23:04:27 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
2019-01-31 15:08:14 -08:00
assert . strictEqual ( typeof mailFqdn , 'string' ) ;
2018-01-25 13:48:53 -08:00
2021-08-27 09:52:24 -07:00
const txtRecords = await dns . getDnsRecords ( '' , domain , 'TXT' ) ;
2018-01-25 13:48:53 -08:00
2026-03-12 22:55:28 +05:30
log ( 'txtRecordsWithSpf: current txt records - %j' , txtRecords ) ;
2018-01-25 13:48:53 -08:00
2021-08-27 09:52:24 -07:00
let i , matches , validSpf ;
2018-01-25 13:48:53 -08:00
2021-08-27 09:52:24 -07:00
for ( i = 0 ; i < txtRecords . length ; i ++ ) {
matches = txtRecords [ i ] . match ( /^("?v=spf1) / ) ; // DO backend may return without quotes
if ( matches === null ) continue ;
2018-01-25 13:48:53 -08:00
2021-08-27 09:52:24 -07:00
// this won't work if the entry is arbitrarily "split" across quoted strings
validSpf = txtRecords [ i ] . indexOf ( 'a:' + mailFqdn ) !== - 1 ;
break ; // there can only be one SPF record
}
2018-01-25 13:48:53 -08:00
2021-08-27 09:52:24 -07:00
if ( validSpf ) return null ;
2018-01-25 13:48:53 -08:00
2021-08-27 09:52:24 -07:00
if ( ! matches ) { // no spf record was found, create one
txtRecords . push ( '"v=spf1 a:' + mailFqdn + ' ~all"' ) ;
2026-03-12 22:55:28 +05:30
log ( 'txtRecordsWithSpf: adding txt record' ) ;
2021-08-27 09:52:24 -07:00
} else { // just add ourself
txtRecords [ i ] = matches [ 1 ] + ' a:' + mailFqdn + txtRecords [ i ] . slice ( matches [ 1 ] . length ) ;
2026-03-12 22:55:28 +05:30
log ( 'txtRecordsWithSpf: inserting txt record' ) ;
2021-08-27 09:52:24 -07:00
}
2018-01-25 13:48:53 -08:00
2021-08-27 09:52:24 -07:00
return txtRecords ;
2018-01-25 13:48:53 -08:00
}
2021-08-27 09:52:24 -07:00
async function upsertDnsRecords ( domain , mailFqdn ) {
2018-01-25 13:48:53 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
2019-01-31 15:08:14 -08:00
assert . strictEqual ( typeof mailFqdn , 'string' ) ;
2018-01-25 13:48:53 -08:00
2026-03-12 22:55:28 +05:30
log ( ` upsertDnsRecords: updating mail dns records domain: ${ domain } mailFqdn: ${ mailFqdn } ` ) ;
2019-04-08 12:23:11 -07:00
2021-08-27 09:52:24 -07:00
const mailDomain = await getDomain ( domain ) ;
if ( ! mailDomain ) throw new BoxError ( BoxError . NOT _FOUND , 'mail domain not found' ) ;
2018-03-08 12:04:32 -08:00
2023-10-01 13:52:19 +05:30
if ( constants . TEST ) return ;
2018-01-25 13:48:53 -08:00
2021-10-11 19:51:29 -07:00
const publicKey = mailDomain . dkimKey . publicKey . split ( '\n' ) . slice ( 1 , - 2 ) . join ( '' ) ; // remove header, footer and new lines
2018-01-25 13:48:53 -08:00
2021-08-27 09:52:24 -07:00
// t=s limits the domainkey to this domain and not it's subdomains
2023-06-30 11:35:34 +05:30
const dkimRecord = { subdomain : ` ${ mailDomain . dkimSelector } ._domainkey ` , domain , type : 'TXT' , values : [ ` "v=DKIM1; t=s; p= ${ publicKey } " ` ] } ;
2018-01-25 13:48:53 -08:00
2021-10-11 19:51:29 -07:00
const records = [ ] ;
2021-08-27 09:52:24 -07:00
records . push ( dkimRecord ) ;
2023-06-30 11:35:34 +05:30
if ( mailDomain . enabled ) records . push ( { subdomain : '' , domain , type : 'MX' , values : [ '10 ' + mailFqdn + '.' ] } ) ;
2018-01-25 13:48:53 -08:00
2021-08-27 09:52:24 -07:00
const txtRecords = await txtRecordsWithSpf ( domain , mailFqdn ) ;
2023-06-30 11:35:34 +05:30
if ( txtRecords ) records . push ( { subdomain : '' , domain , type : 'TXT' , values : txtRecords } ) ;
2018-01-25 13:48:53 -08:00
2021-08-27 09:52:24 -07:00
const dmarcRecords = await dns . getDnsRecords ( '_dmarc' , domain , 'TXT' ) ; // only update dmarc if absent. this allows user to set email for reporting
2023-06-30 11:35:34 +05:30
if ( dmarcRecords . length === 0 ) records . push ( { subdomain : '_dmarc' , domain , type : 'TXT' , values : [ '"v=DMARC1; p=reject; pct=100"' ] } ) ;
2018-01-25 13:48:53 -08:00
2026-03-12 22:55:28 +05:30
log ( ` upsertDnsRecords: updating ${ domain } with ${ records . length } records: ${ JSON . stringify ( records ) } ` ) ;
2018-09-06 12:26:11 -07:00
2021-08-27 09:52:24 -07:00
for ( const record of records ) {
await dns . upsertDnsRecords ( record . subdomain , record . domain , record . type , record . values ) ;
}
2021-02-24 09:02:32 -08:00
2026-03-12 22:55:28 +05:30
log ( ` upsertDnsRecords: records of ${ domain } added ` ) ;
2018-01-25 13:48:53 -08:00
}
2021-08-27 09:52:24 -07:00
async function setDnsRecords ( domain ) {
2019-02-04 20:51:26 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
2023-08-04 21:37:38 +05:30
const { fqdn } = await mailServer . getLocation ( ) ;
await upsertDnsRecords ( domain , fqdn ) ;
2019-02-04 20:51:26 -08:00
}
2021-06-29 14:26:34 -07:00
async function clearDomains ( ) {
await database . query ( 'DELETE FROM mail' , [ ] ) ;
2018-12-07 14:35:04 -08:00
}
2019-02-15 10:55:15 -08:00
function removePrivateFields ( domain ) {
2025-02-13 14:03:25 +01:00
const result = _ . pick ( domain , [ 'domain' , 'enabled' , 'mailFromValidation' , 'catchAll' , 'relay' , 'banner' ] ) ;
2025-06-27 12:59:44 +02:00
if ( 'password' in result . relay ) {
2025-10-08 20:01:18 +02:00
if ( 'username' in result . relay && result . relay . username === result . relay . password ) delete result . relay . username ;
delete result . relay . password ;
2019-02-15 11:44:33 -08:00
}
2019-02-15 10:55:15 -08:00
return result ;
}
2021-06-29 14:26:34 -07:00
async function setMailFromValidation ( domain , enabled ) {
2018-01-20 23:17:39 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
2018-01-20 18:56:17 -08:00
assert . strictEqual ( typeof enabled , 'boolean' ) ;
2021-06-29 14:26:34 -07:00
await updateDomain ( domain , { mailFromValidation : enabled } ) ;
2018-01-20 18:56:17 -08:00
2026-03-12 22:55:28 +05:30
safe ( mailServer . restart ( ) , { debug : log } ) ; // have to restart mail container since haraka cannot watch symlinked config files (mail.ini)
2018-01-20 18:56:17 -08:00
}
2021-06-29 14:26:34 -07:00
async function setBanner ( domain , banner ) {
2020-08-23 14:33:58 -07:00
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof banner , 'object' ) ;
2021-06-29 14:26:34 -07:00
await updateDomain ( domain , { banner } ) ;
2020-08-23 14:33:58 -07:00
2026-03-12 22:55:28 +05:30
safe ( mailServer . restart ( ) , { debug : log } ) ;
2020-08-23 14:33:58 -07:00
}
2021-06-29 14:26:34 -07:00
async function setCatchAllAddress ( domain , addresses ) {
2018-01-20 23:17:39 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
2018-04-12 12:35:56 +02:00
assert ( Array . isArray ( addresses ) ) ;
2018-01-20 18:56:17 -08:00
2022-09-11 11:15:21 +02:00
for ( const address of addresses ) {
if ( ! validator . isEmail ( address ) ) throw new BoxError ( BoxError . BAD _FIELD , ` Invalid catch all address: ${ address } ` ) ;
}
2021-06-29 14:26:34 -07:00
await updateDomain ( domain , { catchAll : addresses } ) ;
2018-01-20 18:56:17 -08:00
2026-03-12 22:55:28 +05:30
safe ( mailServer . restart ( ) , { debug : log } ) ; // have to restart mail container since haraka cannot watch symlinked config files (mail.ini)
2018-01-20 18:56:17 -08:00
}
2021-06-29 14:26:34 -07:00
async function setMailRelay ( domain , relay , options ) {
2018-01-20 23:17:39 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
2018-01-20 18:56:17 -08:00
assert . strictEqual ( typeof relay , 'object' ) ;
2021-06-29 14:26:34 -07:00
assert . strictEqual ( typeof options , 'object' ) ;
2018-01-20 18:56:17 -08:00
2021-06-29 14:26:34 -07:00
const result = await getDomain ( domain ) ;
if ( ! domain ) throw new BoxError ( BoxError . NOT _FOUND , 'Mail domain not found' ) ;
2018-01-20 18:56:17 -08:00
2021-06-29 14:26:34 -07:00
// inject current username/password
2025-10-08 20:01:18 +02:00
if ( 'password' in result . relay ) {
if ( ! Object . hasOwn ( relay , 'username' ) ) relay . username = result . relay . username ;
if ( ! Object . hasOwn ( relay , 'password' ) ) relay . password = result . relay . password ;
}
2018-01-20 18:56:17 -08:00
2021-08-27 09:52:24 -07:00
if ( ! options . skipVerify ) {
2026-02-18 08:18:37 +01:00
const relayResult = await checkSmtpRelay ( relay ) ;
if ( relayResult . status === 'failed' ) throw new BoxError ( BoxError . BAD _FIELD , relayResult . message ) ;
2021-08-27 09:52:24 -07:00
}
2019-02-15 10:55:15 -08:00
2021-10-16 21:47:23 -07:00
await updateDomain ( domain , { relay } ) ;
2019-02-15 10:55:15 -08:00
2026-03-12 22:55:28 +05:30
safe ( mailServer . restart ( ) , { debug : log } ) ;
2018-01-20 18:56:17 -08:00
}
2021-06-29 14:26:34 -07:00
async function setMailEnabled ( domain , enabled , auditSource ) {
2018-01-20 23:17:39 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof enabled , 'boolean' ) ;
2018-11-09 18:51:58 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2018-01-24 11:33:09 -08:00
2025-03-10 21:14:55 +01:00
await updateDomain ( domain , { enabled } ) ;
2018-01-24 11:33:09 -08:00
2025-03-10 21:14:55 +01:00
await mailServer . restart ( ) ;
2025-03-10 21:23:55 +01:00
await platform . onMailServerIncomingDomainsChanged ( auditSource ) ;
2018-11-09 18:51:58 -08:00
2021-06-29 14:26:34 -07:00
await eventlog . add ( enabled ? eventlog . ACTION _MAIL _ENABLED : eventlog . ACTION _MAIL _DISABLED , auditSource , { domain } ) ;
2018-07-25 10:29:26 -07:00
}
2021-08-17 15:45:57 -07:00
async function sendTestMail ( domain , to ) {
2018-01-23 16:10:23 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
2018-02-03 18:27:55 -08:00
assert . strictEqual ( typeof to , 'string' ) ;
2018-01-23 16:10:23 -08:00
2021-08-17 15:45:57 -07:00
const result = await getDomain ( domain ) ;
if ( ! result ) throw new BoxError ( BoxError . NOT _FOUND , 'mail domain not found' ) ;
2018-01-23 16:10:23 -08:00
2021-08-22 09:40:06 -07:00
await mailer . sendTestMail ( result . domain , to ) ;
2018-01-24 13:11:35 +01:00
}
2025-12-05 12:54:48 +01:00
async function listMailboxesByDomain ( domain , page , perPage ) {
2018-01-24 13:11:35 +01:00
assert . strictEqual ( typeof domain , 'string' ) ;
2019-10-22 10:11:35 -07:00
assert . strictEqual ( typeof page , 'number' ) ;
assert . strictEqual ( typeof perPage , 'number' ) ;
2018-01-24 13:11:35 +01:00
2025-12-05 12:54:48 +01:00
// const escapedSearch = mysql.escape('%' + search + '%'); // this also quotes the string
// const searchQuery = search ? ` HAVING (name LIKE ${escapedSearch} OR aliasNames LIKE ${escapedSearch} OR aliasDomains LIKE ${escapedSearch})` : ''; // having instead of where because of aggregated columns use
2018-01-24 13:11:35 +01:00
2022-08-17 23:18:38 +02:00
const query = 'SELECT m1.name AS name, m1.domain AS domain, m1.ownerId AS ownerId, m1.ownerType as ownerType, m1.active as active, JSON_ARRAYAGG(m2.name) AS aliasNames, JSON_ARRAYAGG(m2.domain) AS aliasDomains, m1.enablePop3 AS enablePop3, m1.storageQuota AS storageQuota, m1.messagesQuota AS messagesQuota '
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
+ ` FROM (SELECT * FROM mailboxes WHERE type=' ${ TYPE _MAILBOX } ') AS m1 `
+ ` LEFT JOIN (SELECT * FROM mailboxes WHERE type=' ${ TYPE _ALIAS } ') AS m2 `
2021-08-17 15:45:57 -07:00
+ ' ON m1.name=m2.aliasName AND m1.domain=m2.aliasDomain AND m1.ownerId=m2.ownerId'
+ ' WHERE m1.domain = ?'
+ ' GROUP BY m1.name, m1.domain, m1.ownerId'
2025-12-05 12:54:48 +01:00
// + searchQuery
2021-08-17 15:45:57 -07:00
+ ' ORDER BY name LIMIT ?,?' ;
const results = await database . query ( query , [ domain , ( page - 1 ) * perPage , perPage ] ) ;
results . forEach ( postProcessMailbox ) ;
results . forEach ( postProcessAliases ) ;
return results ;
2018-01-24 13:11:35 +01:00
}
2025-12-05 12:54:48 +01:00
async function listMailboxes ( page , perPage ) {
2021-08-17 15:45:57 -07:00
assert . strictEqual ( typeof page , 'number' ) ;
assert . strictEqual ( typeof perPage , 'number' ) ;
2020-07-15 15:33:53 -07:00
2022-08-17 23:18:38 +02:00
const query = 'SELECT m1.name AS name, m1.domain AS domain, m1.ownerId AS ownerId, m1.ownerType as ownerType, m1.active as active, JSON_ARRAYAGG(m2.name) AS aliasNames, JSON_ARRAYAGG(m2.domain) AS aliasDomains, m1.enablePop3 AS enablePop3, m1.storageQuota AS storageQuota, m1.messagesQuota AS messagesQuota '
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
+ ` FROM (SELECT * FROM mailboxes WHERE type=' ${ TYPE _MAILBOX } ') AS m1 `
+ ` LEFT JOIN (SELECT * FROM mailboxes WHERE type=' ${ TYPE _ALIAS } ') AS m2 `
2021-08-17 15:45:57 -07:00
+ ' ON m1.name=m2.aliasName AND m1.domain=m2.aliasDomain AND m1.ownerId=m2.ownerId'
+ ' GROUP BY m1.name, m1.domain, m1.ownerId'
+ ' ORDER BY name LIMIT ?,?' ;
2020-07-15 15:33:53 -07:00
2021-08-17 15:45:57 -07:00
const results = await database . query ( query , [ ( page - 1 ) * perPage , perPage ] ) ;
results . forEach ( postProcessMailbox ) ;
results . forEach ( postProcessAliases ) ;
return results ;
2020-07-15 15:33:53 -07:00
}
2026-02-17 14:06:40 +01:00
async function listMailboxesByUserId ( userId ) {
assert . strictEqual ( typeof userId , 'string' ) ;
const groupIds = await groups . _getMembership ( userId ) ;
const baseQuery = 'SELECT m1.name AS name, m1.domain AS domain, m1.ownerId AS ownerId, m1.ownerType as ownerType, m1.active as active, JSON_ARRAYAGG(m2.name) AS aliasNames, JSON_ARRAYAGG(m2.domain) AS aliasDomains, m1.enablePop3 AS enablePop3, m1.storageQuota AS storageQuota, m1.messagesQuota AS messagesQuota '
+ ` FROM (SELECT * FROM mailboxes WHERE type=' ${ TYPE _MAILBOX } ') AS m1 `
+ ` LEFT JOIN (SELECT * FROM mailboxes WHERE type=' ${ TYPE _ALIAS } ') AS m2 `
+ ' ON m1.name=m2.aliasName AND m1.domain=m2.aliasDomain AND m1.ownerId=m2.ownerId' ;
let whereClause = " WHERE (m1.ownerType = 'user' AND m1.ownerId = ?)" ;
const args = [ userId ] ;
if ( groupIds . length > 0 ) {
const placeholders = groupIds . map ( ( ) => '?' ) . join ( ',' ) ;
whereClause += ` OR (m1.ownerType = ' ${ OWNERTYPE _GROUP } ' AND m1.ownerId IN ( ${ placeholders } )) ` ;
args . push ( ... groupIds ) ;
}
const query = baseQuery + whereClause + ' GROUP BY m1.name, m1.domain, m1.ownerId ORDER BY name' ;
const results = await database . query ( query , args ) ;
results . forEach ( postProcessMailbox ) ;
results . forEach ( postProcessAliases ) ;
return results ;
}
2021-08-17 15:45:57 -07:00
async function delByDomain ( domain ) {
assert . strictEqual ( typeof domain , 'string' ) ;
await database . query ( 'DELETE FROM mailboxes WHERE domain = ?' , [ domain ] ) ;
2018-02-11 01:18:29 -08:00
}
2021-08-17 15:45:57 -07:00
async function get ( name , domain ) {
2018-04-03 12:18:26 -07:00
assert . strictEqual ( typeof name , 'string' ) ;
2018-01-24 13:11:35 +01:00
assert . strictEqual ( typeof domain , 'string' ) ;
2025-12-05 12:54:48 +01:00
const results = await database . query ( ` SELECT ${ MAILBOX _FIELDS } FROM mailboxes WHERE name = ? AND domain = ? ` , [ name , domain ] ) ;
2021-08-17 15:45:57 -07:00
if ( results . length === 0 ) return null ;
2018-01-24 13:11:35 +01:00
2021-08-17 15:45:57 -07:00
return postProcessMailbox ( results [ 0 ] ) ;
2018-01-24 13:11:35 +01:00
}
2021-08-17 15:45:57 -07:00
async function getMailbox ( name , domain ) {
assert . strictEqual ( typeof name , 'string' ) ;
assert . strictEqual ( typeof domain , 'string' ) ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
const results = await database . query ( ` SELECT ${ MAILBOX _FIELDS } FROM mailboxes WHERE name = ? AND type = ? AND domain = ? ` , [ name , TYPE _MAILBOX , domain ] ) ;
2021-08-17 15:45:57 -07:00
if ( results . length === 0 ) return null ;
return postProcessMailbox ( results [ 0 ] ) ;
}
async function addMailbox ( name , domain , data , auditSource ) {
2018-04-03 12:18:26 -07:00
assert . strictEqual ( typeof name , 'string' ) ;
2018-01-24 13:11:35 +01:00
assert . strictEqual ( typeof domain , 'string' ) ;
2021-04-14 22:37:01 -07:00
assert . strictEqual ( typeof data , 'object' ) ;
2018-11-09 18:45:44 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2018-01-24 13:11:35 +01:00
2025-06-13 17:38:55 +02:00
const { ownerId , ownerType , active , storageQuota , messagesQuota , enablePop3 } = data ;
2021-04-14 22:37:01 -07:00
assert . strictEqual ( typeof ownerId , 'string' ) ;
assert . strictEqual ( typeof ownerType , 'string' ) ;
assert . strictEqual ( typeof active , 'boolean' ) ;
2025-06-13 17:38:55 +02:00
assert . strictEqual ( typeof enablePop3 , 'boolean' ) ;
2022-08-17 23:18:38 +02:00
assert ( Number . isInteger ( storageQuota ) && storageQuota >= 0 ) ;
assert ( Number . isInteger ( messagesQuota ) && messagesQuota >= 0 ) ;
2021-04-14 22:37:01 -07:00
2018-04-05 16:07:51 -07:00
name = name . toLowerCase ( ) ;
2018-01-24 13:11:35 +01:00
2021-08-17 15:45:57 -07:00
let error = validateName ( name ) ;
if ( error ) throw error ;
2020-11-12 23:25:33 -08:00
2025-10-08 20:11:55 +02:00
if ( ! validateOwnerType ( ownerType ) ) throw new BoxError ( BoxError . BAD _FIELD , 'bad owner type' ) ;
2018-04-05 16:07:51 -07:00
2025-06-13 17:38:55 +02:00
[ error ] = await safe ( database . query ( 'INSERT INTO mailboxes (name, type, domain, ownerId, ownerType, active, storageQuota, messagesQuota, enablePop3) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)' ,
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
[ name , TYPE _MAILBOX , domain , ownerId , ownerType , active , storageQuota , messagesQuota , enablePop3 ] ) ) ;
2025-09-29 11:55:15 +02:00
if ( error && error . sqlCode === 'ER_DUP_ENTRY' ) throw new BoxError ( BoxError . ALREADY _EXISTS , 'mailbox already exists' ) ;
if ( error && error . sqlCode === 'ER_NO_REFERENCED_ROW_2' && error . sqlMessage . includes ( 'mailboxes_domain_constraint' ) ) throw new BoxError ( BoxError . NOT _FOUND , ` no such domain ' ${ domain } ' ` ) ;
2021-08-17 15:45:57 -07:00
if ( error ) throw error ;
2018-11-09 18:45:44 -08:00
2025-06-13 17:38:55 +02:00
await eventlog . add ( eventlog . ACTION _MAIL _MAILBOX _ADD , auditSource , { name , domain , ownerId , ownerType , active , storageQuota , enablePop3 , messageQuota : messagesQuota } ) ;
2018-01-24 13:11:35 +01:00
}
2021-08-17 15:45:57 -07:00
async function updateMailbox ( name , domain , data , auditSource ) {
2018-04-03 14:12:43 -07:00
assert . strictEqual ( typeof name , 'string' ) ;
assert . strictEqual ( typeof domain , 'string' ) ;
2021-04-14 22:37:01 -07:00
assert . strictEqual ( typeof data , 'object' ) ;
2020-01-24 16:55:41 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2018-04-03 14:12:43 -07:00
2022-08-18 11:56:01 +02:00
const args = [ ] ;
const fields = [ ] ;
for ( const k in data ) {
if ( k === 'enablePop3' || k === 'active' ) {
fields . push ( k + ' = ?' ) ;
args . push ( data [ k ] ? 1 : 0 ) ;
continue ;
}
2021-04-14 22:37:01 -07:00
2025-10-08 20:11:55 +02:00
if ( k === 'ownerType' && ! validateOwnerType ( data [ k ] ) ) throw new BoxError ( BoxError . BAD _FIELD , 'bad owner type' ) ;
2018-04-05 16:07:51 -07:00
2022-08-18 11:56:01 +02:00
fields . push ( k + ' = ?' ) ;
args . push ( data [ k ] ) ;
}
args . push ( name . toLowerCase ( ) ) ;
args . push ( domain ) ;
2020-11-12 23:25:33 -08:00
2021-08-17 15:45:57 -07:00
const mailbox = await getMailbox ( name , domain ) ;
if ( ! mailbox ) throw new BoxError ( BoxError . NOT _FOUND , 'No such mailbox' ) ;
2020-01-24 16:55:41 -08:00
2022-08-18 11:56:01 +02:00
const result = await safe ( database . query ( 'UPDATE mailboxes SET ' + fields . join ( ', ' ) + ' WHERE name = ? AND domain = ?' , args ) ) ;
2021-08-17 15:45:57 -07:00
if ( result . affectedRows === 0 ) throw new BoxError ( BoxError . NOT _FOUND , 'Mailbox not found' ) ;
2020-01-24 16:55:41 -08:00
2022-08-18 11:56:01 +02:00
await eventlog . add ( eventlog . ACTION _MAIL _MAILBOX _UPDATE , auditSource , Object . assign ( data , { name , domain , oldUserId : mailbox . userId } ) ) ;
2018-04-03 14:12:43 -07:00
}
2021-08-25 19:41:46 -07:00
async function removeSolrIndex ( mailbox ) {
2020-12-02 00:24:15 -08:00
assert . strictEqual ( typeof mailbox , 'string' ) ;
2021-08-25 19:41:46 -07:00
const addonDetails = await services . getContainerDetails ( 'mail' , 'CLOUDRON_MAIL_TOKEN' ) ;
2020-12-02 00:24:15 -08:00
2021-12-19 00:30:22 -08:00
const [ error , response ] = await safe ( superagent . post ( ` http:// ${ addonDetails . ip } :3000/solr_delete_index?access_token= ${ addonDetails . token } ` )
2021-08-25 19:41:46 -07:00
. timeout ( 2000 )
. send ( { mailbox } )
. ok ( ( ) => true ) ) ;
2020-12-02 00:24:15 -08:00
2021-08-25 19:41:46 -07:00
if ( error ) throw new BoxError ( BoxError . MAIL _ERROR , ` Could not remove solr index: ${ error . message } ` ) ;
2020-12-02 00:24:15 -08:00
2025-02-14 17:26:54 +01:00
if ( response . status !== 200 ) throw new BoxError ( BoxError . MAIL _ERROR , ` Error removing solr index - ${ response . status } ${ response . text } ` ) ;
2020-12-02 00:24:15 -08:00
}
2021-08-17 15:45:57 -07:00
async function delMailbox ( name , domain , options , auditSource ) {
2018-01-24 13:11:35 +01:00
assert . strictEqual ( typeof domain , 'string' ) ;
2018-04-03 12:18:26 -07:00
assert . strictEqual ( typeof name , 'string' ) ;
2020-07-27 22:26:10 -07:00
assert . strictEqual ( typeof options , 'object' ) ;
2018-11-09 18:45:44 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2018-01-24 13:11:35 +01:00
2020-12-02 00:24:15 -08:00
const mailbox = ` ${ name } @ ${ domain } ` ;
2021-08-17 15:45:57 -07:00
if ( options . deleteMails ) {
2025-07-16 21:53:22 +02:00
const [ error ] = await safe ( shell . sudo ( [ REMOVE _MAILBOX _CMD , mailbox ] , { } ) ) ;
2021-08-17 15:45:57 -07:00
if ( error ) throw new BoxError ( BoxError . FS _ERROR , ` Error removing mailbox: ${ error . message } ` ) ;
}
2020-07-27 22:26:10 -07:00
2021-08-17 15:45:57 -07:00
// deletes aliases as well
const result = await database . query ( 'DELETE FROM mailboxes WHERE ((name=? AND domain=?) OR (aliasName = ? AND aliasDomain=?))' , [ name , domain , name , domain ] ) ;
if ( result . affectedRows === 0 ) throw new BoxError ( BoxError . NOT _FOUND , 'Mailbox not found' ) ;
2020-07-27 22:26:10 -07:00
2021-08-25 19:41:46 -07:00
const [ error ] = await safe ( removeSolrIndex ( mailbox ) ) ;
2026-03-12 22:55:28 +05:30
if ( error ) log ( ` delMailbox: failed to remove solr index: ${ error . message } ` ) ;
2021-08-25 19:41:46 -07:00
2022-02-24 20:04:46 -08:00
await eventlog . add ( eventlog . ACTION _MAIL _MAILBOX _REMOVE , auditSource , { name , domain } ) ;
2018-01-24 13:11:35 +01:00
}
2018-01-25 18:03:02 +01:00
2021-08-17 15:45:57 -07:00
async function getAlias ( name , domain ) {
2018-04-03 12:18:26 -07:00
assert . strictEqual ( typeof name , 'string' ) ;
2018-01-25 18:03:02 +01:00
assert . strictEqual ( typeof domain , 'string' ) ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
const results = await database . query ( ` SELECT ${ MAILBOX _FIELDS } FROM mailboxes WHERE name = ? AND type = ? AND domain = ? ` , [ name , TYPE _ALIAS , domain ] ) ;
2021-08-17 15:45:57 -07:00
if ( results . length === 0 ) return null ;
2018-01-25 18:03:02 +01:00
2021-08-17 15:45:57 -07:00
results . forEach ( function ( result ) { postProcessMailbox ( result ) ; } ) ;
2018-01-25 18:03:02 +01:00
2021-08-17 15:45:57 -07:00
return results [ 0 ] ;
}
2022-08-18 13:21:24 +02:00
async function searchAlias ( name , domain ) {
assert . strictEqual ( typeof name , 'string' ) ;
assert . strictEqual ( typeof domain , 'string' ) ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
const results = await database . query ( ` SELECT ${ MAILBOX _FIELDS } FROM mailboxes WHERE ? LIKE REPLACE(REPLACE(name, '*', '%'), '_', ' \\ _') AND type = ? AND domain = ? ` , [ name , TYPE _ALIAS , domain ] ) ;
2022-08-18 13:21:24 +02:00
if ( results . length === 0 ) return null ;
results . forEach ( function ( result ) { postProcessMailbox ( result ) ; } ) ;
return results [ 0 ] ;
}
2021-08-17 15:45:57 -07:00
async function getAliases ( name , domain ) {
assert . strictEqual ( typeof name , 'string' ) ;
assert . strictEqual ( typeof domain , 'string' ) ;
const result = await getMailbox ( name , domain ) ; // check if mailbox exists
if ( result === null ) throw new BoxError ( BoxError . NOT _FOUND , 'No such mailbox' ) ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
return await database . query ( 'SELECT name, domain FROM mailboxes WHERE type = ? AND aliasName = ? AND aliasDomain = ? ORDER BY name' , [ TYPE _ALIAS , name , domain ] ) ;
2018-01-25 18:03:02 +01:00
}
2022-02-24 20:30:13 -08:00
async function setAliases ( name , domain , aliases , auditSource ) {
2018-04-03 12:18:26 -07:00
assert . strictEqual ( typeof name , 'string' ) ;
2018-01-25 18:03:02 +01:00
assert . strictEqual ( typeof domain , 'string' ) ;
assert ( Array . isArray ( aliases ) ) ;
2022-02-24 20:30:13 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2018-01-25 18:03:02 +01:00
2021-08-17 15:45:57 -07:00
for ( let i = 0 ; i < aliases . length ; i ++ ) {
2026-02-18 08:18:37 +01:00
const aliasName = aliases [ i ] . name . toLowerCase ( ) ;
const aliasDomain = aliases [ i ] . domain . toLowerCase ( ) ;
2018-01-25 18:03:02 +01:00
2026-02-18 08:18:37 +01:00
const error = validateAlias ( aliasName ) ;
2021-08-17 15:45:57 -07:00
if ( error ) throw error ;
2020-04-19 18:44:16 -07:00
2026-02-18 08:18:37 +01:00
const mailDomain = await getDomain ( aliasDomain ) ;
if ( ! mailDomain ) throw new BoxError ( BoxError . NOT _FOUND , ` mail domain ${ aliasDomain } not found ` ) ;
2022-01-10 22:06:37 -08:00
2026-02-18 08:18:37 +01:00
aliases [ i ] = { name : aliasName , domain : aliasDomain } ;
2018-01-25 18:03:02 +01:00
}
2021-08-17 15:45:57 -07:00
const results = await database . query ( 'SELECT ' + MAILBOX _FIELDS + ' FROM mailboxes WHERE name = ? AND domain = ?' , [ name , domain ] ) ;
if ( results . length === 0 ) throw new BoxError ( BoxError . NOT _FOUND , 'Mailbox not found' ) ;
2022-02-24 20:30:13 -08:00
const queries = [ ] ;
2021-08-17 15:45:57 -07:00
// clear existing aliases
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
queries . push ( { query : 'DELETE FROM mailboxes WHERE aliasName = ? AND aliasDomain = ? AND type = ?' , args : [ name , domain , TYPE _ALIAS ] } ) ;
2022-02-24 20:30:13 -08:00
for ( const alias of aliases ) {
2021-08-17 15:45:57 -07:00
queries . push ( { query : 'INSERT INTO mailboxes (name, domain, type, aliasName, aliasDomain, ownerId, ownerType) VALUES (?, ?, ?, ?, ?, ?, ?)' ,
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
args : [ alias . name , alias . domain , TYPE _ALIAS , name , domain , results [ 0 ] . ownerId , results [ 0 ] . ownerType ] } ) ;
2022-02-24 20:30:13 -08:00
}
2021-08-17 15:45:57 -07:00
const [ error ] = await safe ( database . transaction ( queries ) ) ;
2025-09-29 11:55:15 +02:00
if ( error && error . sqlCode === 'ER_DUP_ENTRY' && error . message . indexOf ( 'mailboxes_name_domain_unique_index' ) !== - 1 ) {
2025-09-24 10:50:25 +02:00
const aliasMatch = error . message . match ( new RegExp ( ` Duplicate entry '(.*)- ${ domain } ' for key 'mailboxes_name_domain_unique_index' ` ) ) ;
2021-08-17 15:45:57 -07:00
if ( ! aliasMatch ) throw new BoxError ( BoxError . ALREADY _EXISTS , error . message ) ;
throw new BoxError ( BoxError . ALREADY _EXISTS , ` Mailbox, mailinglist or alias for ${ aliasMatch [ 1 ] } already exists ` ) ;
}
if ( error ) throw error ;
2022-02-24 20:30:13 -08:00
await eventlog . add ( eventlog . ACTION _MAIL _MAILBOX _UPDATE , auditSource , { name , domain , aliases } ) ;
2018-01-25 18:03:02 +01:00
}
2018-01-26 10:22:50 +01:00
2025-12-05 12:54:48 +01:00
async function listMailingListsByDomain ( domain , page , perPage ) {
2018-01-26 10:22:50 +01:00
assert . strictEqual ( typeof domain , 'string' ) ;
2020-07-05 10:36:17 -07:00
assert . strictEqual ( typeof page , 'number' ) ;
assert . strictEqual ( typeof perPage , 'number' ) ;
2018-01-26 10:22:50 +01:00
2021-08-17 15:45:57 -07:00
let query = ` SELECT ${ MAILBOX _FIELDS } FROM mailboxes WHERE type = ? AND domain = ? ` ;
2025-12-05 12:54:48 +01:00
// if (search) query += ' AND (name LIKE ' + mysql.escape('%' + search + '%') + ' OR membersJson LIKE ' + mysql.escape('%' + search + '%') + ')';
2018-01-26 10:22:50 +01:00
2021-08-17 15:45:57 -07:00
query += 'ORDER BY name LIMIT ?,?' ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
const results = await database . query ( query , [ TYPE _LIST , domain , ( page - 1 ) * perPage , perPage ] ) ;
2021-08-17 15:45:57 -07:00
results . forEach ( function ( result ) { postProcessMailbox ( result ) ; } ) ;
return results ;
2018-01-26 10:22:50 +01:00
}
2026-02-14 16:34:34 +01:00
async function getStats ( domain ) {
assert . strictEqual ( typeof domain , 'string' ) ;
const mailboxes = await listMailboxesByDomain ( domain , 1 , 10000 ) ;
const mailingLists = await listMailingListsByDomain ( domain , 1 , 10000 ) ;
return {
mailboxCount : mailboxes . length ,
pop3Count : mailboxes . filter ( mb => mb . enablePop3 ) . length ,
aliasCount : mailboxes . map ( mb => mb . aliases . length ) . reduce ( ( a , b ) => a + b , 0 ) ,
mailingListCount : mailingLists . length
} ;
}
2025-12-05 12:54:48 +01:00
async function getMailingList ( name , domain ) {
2020-01-24 16:54:14 -08:00
assert . strictEqual ( typeof name , 'string' ) ;
2018-01-26 10:22:50 +01:00
assert . strictEqual ( typeof domain , 'string' ) ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
const results = await database . query ( 'SELECT ' + MAILBOX _FIELDS + ' FROM mailboxes WHERE type = ? AND name = ? AND domain = ?' , [ TYPE _LIST , name , domain ] ) ;
2021-08-17 15:45:57 -07:00
if ( results . length === 0 ) return null ;
2018-01-26 10:22:50 +01:00
2021-08-17 15:45:57 -07:00
return postProcessMailbox ( results [ 0 ] ) ;
2018-01-26 10:22:50 +01:00
}
2025-12-05 12:54:48 +01:00
async function addMailingList ( name , domain , data , auditSource ) {
2018-01-26 10:22:50 +01:00
assert . strictEqual ( typeof domain , 'string' ) ;
2018-04-05 16:07:51 -07:00
assert . strictEqual ( typeof name , 'string' ) ;
2021-04-14 22:37:01 -07:00
assert . strictEqual ( typeof data , 'object' ) ;
2018-11-09 18:49:55 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2018-01-26 10:22:50 +01:00
2021-04-14 22:37:01 -07:00
const { members , membersOnly , active } = data ;
assert ( Array . isArray ( members ) ) ;
assert . strictEqual ( typeof membersOnly , 'boolean' ) ;
assert . strictEqual ( typeof active , 'boolean' ) ;
2018-04-05 16:07:51 -07:00
name = name . toLowerCase ( ) ;
2021-08-17 15:45:57 -07:00
let error = validateName ( name ) ;
if ( error ) throw error ;
2018-04-05 16:07:51 -07:00
2021-08-17 15:45:57 -07:00
for ( let i = 0 ; i < members . length ; i ++ ) {
if ( ! validator . isEmail ( members [ i ] ) ) throw new BoxError ( BoxError . BAD _FIELD , 'Invalid mail member: ' + members [ i ] ) ;
2018-04-05 16:07:51 -07:00
}
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
[ error ] = await safe ( database . query ( 'INSERT INTO mailboxes (name, type, domain, ownerId, ownerType, membersJson, membersOnly, active) VALUES (?, ?, ?, ?, ?, ?, ?, ?)' , [ name , TYPE _LIST , domain , 'admin' , 'user' , JSON . stringify ( members ) , membersOnly , active ] ) ) ;
2025-09-29 11:55:15 +02:00
if ( error && error . sqlCode === 'ER_DUP_ENTRY' ) throw new BoxError ( BoxError . ALREADY _EXISTS , 'mailbox already exists' ) ;
2021-08-17 15:45:57 -07:00
if ( error ) throw error ;
2018-11-09 18:49:55 -08:00
2022-02-24 20:04:46 -08:00
await eventlog . add ( eventlog . ACTION _MAIL _LIST _ADD , auditSource , { name , domain , members , membersOnly , active } ) ;
2018-01-26 10:22:50 +01:00
}
2025-12-05 12:54:48 +01:00
async function updateMailingList ( name , domain , data , auditSource ) {
2018-04-03 14:12:43 -07:00
assert . strictEqual ( typeof name , 'string' ) ;
assert . strictEqual ( typeof domain , 'string' ) ;
2021-04-14 22:37:01 -07:00
assert . strictEqual ( typeof data , 'object' ) ;
2020-01-24 16:55:41 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2018-04-03 14:12:43 -07:00
2021-04-14 22:37:01 -07:00
const { members , membersOnly , active } = data ;
assert ( Array . isArray ( members ) ) ;
assert . strictEqual ( typeof membersOnly , 'boolean' ) ;
assert . strictEqual ( typeof active , 'boolean' ) ;
2018-04-05 16:07:51 -07:00
name = name . toLowerCase ( ) ;
2024-05-23 10:58:59 +02:00
const error = validateName ( name ) ;
2021-08-17 15:45:57 -07:00
if ( error ) throw error ;
2018-04-05 16:07:51 -07:00
2021-08-17 15:45:57 -07:00
for ( let i = 0 ; i < members . length ; i ++ ) {
if ( ! validator . isEmail ( members [ i ] ) ) throw new BoxError ( BoxError . BAD _FIELD , 'Invalid email: ' + members [ i ] ) ;
2018-04-05 16:07:51 -07:00
}
2021-08-17 15:45:57 -07:00
const result = await database . query ( 'UPDATE mailboxes SET membersJson = ?, membersOnly = ?, active = ? WHERE name = ? AND domain = ?' ,
[ JSON . stringify ( members ) , membersOnly , active , name , domain ] ) ;
if ( result . affectedRows === 0 ) throw new BoxError ( BoxError . NOT _FOUND , 'Mailbox not found' ) ;
2020-01-24 16:55:41 -08:00
2022-02-24 20:04:46 -08:00
await eventlog . add ( eventlog . ACTION _MAIL _LIST _UPDATE , auditSource , { name , domain , oldMembers : result . members , members , membersOnly , active } ) ;
2018-04-03 14:12:43 -07:00
}
2025-12-05 12:54:48 +01:00
async function delMailingList ( name , domain , auditSource ) {
2018-11-09 18:49:55 -08:00
assert . strictEqual ( typeof name , 'string' ) ;
2018-01-26 10:22:50 +01:00
assert . strictEqual ( typeof domain , 'string' ) ;
2018-11-09 18:49:55 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2018-01-26 10:22:50 +01:00
2021-08-17 15:45:57 -07:00
// deletes aliases as well
const result = await database . query ( 'DELETE FROM mailboxes WHERE ((name=? AND domain=?) OR (aliasName = ? AND aliasDomain=?))' , [ name , domain , name , domain ] ) ;
if ( result . affectedRows === 0 ) throw new BoxError ( BoxError . NOT _FOUND , 'Mailbox not found' ) ;
2018-11-09 18:49:55 -08:00
2022-02-24 20:04:46 -08:00
await eventlog . add ( eventlog . ACTION _MAIL _LIST _REMOVE , auditSource , { name , domain } ) ;
2018-01-26 10:22:50 +01:00
}
2019-11-06 16:45:44 -08:00
2020-04-19 18:44:16 -07:00
// resolves the members of a list. i.e the lists and aliases
2025-12-05 12:54:48 +01:00
async function resolveMailingList ( listName , listDomain ) {
2019-11-06 16:45:44 -08:00
assert . strictEqual ( typeof listName , 'string' ) ;
assert . strictEqual ( typeof listDomain , 'string' ) ;
2021-06-29 14:26:34 -07:00
2021-08-17 15:45:57 -07:00
const mailDomains = await listDomains ( ) ;
const mailInDomains = mailDomains . filter ( function ( d ) { return d . enabled ; } ) . map ( function ( d ) { return d . domain ; } ) . join ( ',' ) ;
2019-11-06 16:45:44 -08:00
2025-12-05 12:54:48 +01:00
const list = await getMailingList ( listName , listDomain ) ;
2021-08-17 15:45:57 -07:00
if ( ! list ) throw new BoxError ( BoxError . NOT _FOUND , 'List not found' ) ;
2019-11-06 16:45:44 -08:00
2024-05-23 10:58:59 +02:00
const resolvedMembers = [ ] , visited = [ ] ; // slice creates a copy of array
let toResolve = list . members . slice ( ) ;
2019-11-06 16:45:44 -08:00
2021-08-17 15:45:57 -07:00
while ( toResolve . length != 0 ) {
const toProcess = toResolve . shift ( ) ;
const parts = toProcess . split ( '@' ) ;
const memberName = parts [ 0 ] . split ( '+' ) [ 0 ] , memberDomain = parts [ 1 ] ;
2019-11-06 16:45:44 -08:00
2021-08-17 15:45:57 -07:00
if ( ! mailInDomains . includes ( memberDomain ) ) { // external domain
resolvedMembers . push ( toProcess ) ;
continue ;
}
2019-11-06 16:45:44 -08:00
2021-08-17 15:45:57 -07:00
const member = ` ${ memberName } @ ${ memberDomain } ` ; // cleaned up without any '+' subaddress
if ( visited . includes ( member ) ) {
2026-03-12 22:55:28 +05:30
log ( ` resolveMailingList: list ${ listName } @ ${ listDomain } has a recursion at member ${ member } ` ) ;
2021-08-17 15:45:57 -07:00
continue ;
}
visited . push ( member ) ;
2019-11-06 16:45:44 -08:00
2021-08-17 15:45:57 -07:00
const entry = await get ( memberName , memberDomain ) ;
if ( ! entry ) { // let it bounce
resolvedMembers . push ( member ) ;
continue ;
}
2019-11-06 16:45:44 -08:00
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
if ( entry . type === TYPE _MAILBOX ) { // concrete mailbox
2021-08-17 15:45:57 -07:00
resolvedMembers . push ( member ) ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
} else if ( entry . type === TYPE _ALIAS ) { // resolve aliases
2021-08-17 15:45:57 -07:00
toResolve = toResolve . concat ( ` ${ entry . aliasName } @ ${ entry . aliasDomain } ` ) ;
} else { // resolve list members
toResolve = toResolve . concat ( entry . members ) ;
}
}
2019-11-06 16:45:44 -08:00
2021-08-17 15:45:57 -07:00
return { resolvedMembers , list } ;
2019-11-20 14:11:52 -08:00
}
2023-08-04 13:41:13 +05:30
async function checkStatus ( ) {
const result = await checkConfiguration ( ) ;
if ( result . status ) {
2024-12-11 15:47:41 +01:00
await notifications . unpin ( notifications . TYPE _MAIL _STATUS , { } ) ;
2023-08-04 13:41:13 +05:30
} else {
2024-12-11 15:47:41 +01:00
await notifications . pin ( notifications . TYPE _MAIL _STATUS , 'Email is not configured properly' , result . message , { } ) ;
2023-08-04 13:41:13 +05:30
}
}
2025-10-08 20:01:18 +02:00
2026-02-14 16:34:34 +01:00
const _delByDomain = delByDomain ;
2026-02-14 15:43:24 +01:00
export default {
getStatus ,
checkConfiguration ,
listDomains ,
getDomain ,
clearDomains ,
removePrivateFields ,
setDnsRecords ,
upsertDnsRecords ,
validateName ,
validateDisplayName ,
setMailFromValidation ,
setCatchAllAddress ,
setMailRelay ,
setMailEnabled ,
setBanner ,
sendTestMail ,
listMailboxesByDomain ,
listMailboxes ,
2026-02-17 14:06:40 +01:00
listMailboxesByUserId ,
2026-02-14 15:43:24 +01:00
getMailbox ,
addMailbox ,
updateMailbox ,
delMailbox ,
getAlias ,
getAliases ,
setAliases ,
searchAlias ,
listMailingListsByDomain ,
getMailingList ,
addMailingList ,
updateMailingList ,
delMailingList ,
resolveMailingList ,
getStats ,
checkStatus ,
OWNERTYPE _USER ,
OWNERTYPE _GROUP ,
OWNERTYPE _APP ,
TYPE _MAILBOX ,
TYPE _LIST ,
TYPE _ALIAS ,
_delByDomain ,
2026-02-14 16:34:34 +01:00
_updateDomain : updateDomain ,
2026-02-14 15:43:24 +01:00
} ;