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>
This commit is contained in:
Girish Ramakrishnan
2026-02-14 09:53:14 +01:00
parent e0e9f14a5e
commit 96dc79cfe6
277 changed files with 4789 additions and 3811 deletions

View File

@@ -1,90 +1,85 @@
'use strict';
import assert from 'node:assert';
import BoxError from './boxerror.js';
import constants from './constants.js';
import * as database from './database.js';
import debugModule from 'debug';
import * as dig from './dig.js';
import * as dns from './dns.js';
import eventlog from './eventlog.js';
import * as mailer from './mailer.js';
import * as mailServer from './mailserver.js';
import net from 'node:net';
import * as network from './network.js';
import nodemailer from 'nodemailer';
import * as notifications from './notifications.js';
import path from 'node:path';
import * as platform from './platform.js';
import safe from 'safetydance';
import services from './services.js';
import shellModule from './shell.js';
import superagent from '@cloudron/superagent';
import * as validator from './validator.js';
import * as _ from './underscore.js';
exports = module.exports = {
const debug = debugModule('box:mail');
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';
const _delByDomain = delByDomain;
const _updateDomain = updateDomain;
export {
getStatus,
checkConfiguration,
listDomains,
getDomain,
clearDomains,
removePrivateFields,
setDnsRecords,
upsertDnsRecords,
validateName,
validateDisplayName,
setMailFromValidation,
setCatchAllAddress,
setMailRelay,
setMailEnabled,
setBanner,
sendTestMail,
listMailboxesByDomain,
listMailboxes,
getMailbox,
addMailbox,
updateMailbox,
delMailbox,
getAlias,
getAliases,
setAliases,
searchAlias,
listMailingListsByDomain,
getMailingList,
addMailingList,
updateMailingList,
delMailingList,
resolveMailingList,
getStats,
checkStatus,
OWNERTYPE_USER: 'user',
OWNERTYPE_GROUP: 'group',
OWNERTYPE_APP: 'app',
TYPE_MAILBOX: 'mailbox',
TYPE_LIST: 'list',
TYPE_ALIAS: 'alias',
_delByDomain: delByDomain,
_updateDomain: updateDomain
OWNERTYPE_USER,
OWNERTYPE_GROUP,
OWNERTYPE_APP,
TYPE_MAILBOX,
TYPE_LIST,
TYPE_ALIAS,
_delByDomain,
_updateDomain,
};
const assert = require('node:assert'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
database = require('./database.js'),
debug = require('debug')('box:mail'),
dig = require('./dig.js'),
dns = require('./dns.js'),
eventlog = require('./eventlog.js'),
mailer = require('./mailer.js'),
mailServer = require('./mailserver.js'),
net = require('node:net'),
network = require('./network.js'),
nodemailer = require('nodemailer'),
notifications = require('./notifications.js'),
path = require('node:path'),
platform = require('./platform.js'),
safe = require('safetydance'),
services = require('./services.js'),
shell = require('./shell.js')('mail'),
superagent = require('@cloudron/superagent'),
validator = require('./validator.js'),
_ = require('./underscore.js');
const DNS_OPTIONS = { timeout: 20000, tries: 4 };
const REMOVE_MAILBOX_CMD = path.join(__dirname, 'scripts/rmmailbox.sh');
const REMOVE_MAILBOX_CMD = path.join(import.meta.dirname, 'scripts/rmmailbox.sh');
// if you add a field here, listMailboxes* has to be updated
const MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'ownerType', 'aliasName', 'aliasDomain', 'creationTime', 'membersJson', 'membersOnly', 'domain', 'active', 'enablePop3', 'storageQuota', 'messagesQuota' ].join(',');
@@ -169,7 +164,7 @@ function validateDisplayName(name) {
}
function validateOwnerType(type) {
const OWNERTYPES = [ exports.OWNERTYPE_USER, exports.OWNERTYPE_GROUP, exports.OWNERTYPE_APP ];
const OWNERTYPES = [ OWNERTYPE_USER, OWNERTYPE_GROUP, OWNERTYPE_APP ];
return OWNERTYPES.includes(type);
}
@@ -845,8 +840,8 @@ async function listMailboxesByDomain(domain, page, perPage) {
// const searchQuery = search ? ` HAVING (name LIKE ${escapedSearch} OR aliasNames LIKE ${escapedSearch} OR aliasDomains LIKE ${escapedSearch})` : ''; // having instead of where because of aggregated columns use
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 '
+ ` FROM (SELECT * FROM mailboxes WHERE type='${exports.TYPE_MAILBOX}') AS m1`
+ ` LEFT JOIN (SELECT * FROM mailboxes WHERE type='${exports.TYPE_ALIAS}') AS m2`
+ ` 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'
+ ' WHERE m1.domain = ?'
+ ' GROUP BY m1.name, m1.domain, m1.ownerId'
@@ -866,8 +861,8 @@ async function listMailboxes(page, perPage) {
assert.strictEqual(typeof perPage, 'number');
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 '
+ ` FROM (SELECT * FROM mailboxes WHERE type='${exports.TYPE_MAILBOX}') AS m1`
+ ` LEFT JOIN (SELECT * FROM mailboxes WHERE type='${exports.TYPE_ALIAS}') AS m2`
+ ` 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'
+ ' GROUP BY m1.name, m1.domain, m1.ownerId'
+ ' ORDER BY name LIMIT ?,?';
@@ -914,7 +909,7 @@ async function getMailbox(name, domain) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
const results = await database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE name = ? AND type = ? AND domain = ?`, [ name, exports.TYPE_MAILBOX, domain ]);
const results = await database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE name = ? AND type = ? AND domain = ?`, [ name, TYPE_MAILBOX, domain ]);
if (results.length === 0) return null;
return postProcessMailbox(results[0]);
}
@@ -941,7 +936,7 @@ async function addMailbox(name, domain, data, auditSource) {
if (!validateOwnerType(ownerType)) throw new BoxError(BoxError.BAD_FIELD, 'bad owner type');
[error] = await safe(database.query('INSERT INTO mailboxes (name, type, domain, ownerId, ownerType, active, storageQuota, messagesQuota, enablePop3) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
[ name, exports.TYPE_MAILBOX, domain, ownerId, ownerType, active, storageQuota, messagesQuota, enablePop3 ]));
[ name, TYPE_MAILBOX, domain, ownerId, ownerType, active, storageQuota, messagesQuota, enablePop3 ]));
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}'`);
if (error) throw error;
@@ -1022,7 +1017,7 @@ async function getAlias(name, domain) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
const results = await database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE name = ? AND type = ? AND domain = ?`, [ name, exports.TYPE_ALIAS, domain ]);
const results = await database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE name = ? AND type = ? AND domain = ?`, [ name, TYPE_ALIAS, domain ]);
if (results.length === 0) return null;
results.forEach(function (result) { postProcessMailbox(result); });
@@ -1034,7 +1029,7 @@ async function searchAlias(name, domain) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
const results = await database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE ? LIKE REPLACE(REPLACE(name, '*', '%'), '_', '\\_') AND type = ? AND domain = ?`, [ name, exports.TYPE_ALIAS, domain ]);
const results = await database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE ? LIKE REPLACE(REPLACE(name, '*', '%'), '_', '\\_') AND type = ? AND domain = ?`, [ name, TYPE_ALIAS, domain ]);
if (results.length === 0) return null;
results.forEach(function (result) { postProcessMailbox(result); });
@@ -1048,7 +1043,7 @@ async function getAliases(name, domain) {
const result = await getMailbox(name, domain); // check if mailbox exists
if (result === null) throw new BoxError(BoxError.NOT_FOUND, 'No such mailbox');
return await database.query('SELECT name, domain FROM mailboxes WHERE type = ? AND aliasName = ? AND aliasDomain = ? ORDER BY name', [ exports.TYPE_ALIAS, name, domain ]);
return await database.query('SELECT name, domain FROM mailboxes WHERE type = ? AND aliasName = ? AND aliasDomain = ? ORDER BY name', [ TYPE_ALIAS, name, domain ]);
}
async function setAliases(name, domain, aliases, auditSource) {
@@ -1075,10 +1070,10 @@ async function setAliases(name, domain, aliases, auditSource) {
const queries = [];
// clear existing aliases
queries.push({ query: 'DELETE FROM mailboxes WHERE aliasName = ? AND aliasDomain = ? AND type = ?', args: [ name, domain, exports.TYPE_ALIAS ] });
queries.push({ query: 'DELETE FROM mailboxes WHERE aliasName = ? AND aliasDomain = ? AND type = ?', args: [ name, domain, TYPE_ALIAS ] });
for (const alias of aliases) {
queries.push({ query: 'INSERT INTO mailboxes (name, domain, type, aliasName, aliasDomain, ownerId, ownerType) VALUES (?, ?, ?, ?, ?, ?, ?)',
args: [ alias.name, alias.domain, exports.TYPE_ALIAS, name, domain, results[0].ownerId, results[0].ownerType ] });
args: [ alias.name, alias.domain, TYPE_ALIAS, name, domain, results[0].ownerId, results[0].ownerType ] });
}
const [error] = await safe(database.transaction(queries));
@@ -1103,7 +1098,7 @@ async function listMailingListsByDomain(domain, page, perPage) {
query += 'ORDER BY name LIMIT ?,?';
const results = await database.query(query, [ exports.TYPE_LIST, domain, (page-1)*perPage, perPage ]);
const results = await database.query(query, [ TYPE_LIST, domain, (page-1)*perPage, perPage ]);
results.forEach(function (result) { postProcessMailbox(result); });
@@ -1114,7 +1109,7 @@ async function getMailingList(name, domain) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
const results = await database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE type = ? AND name = ? AND domain = ?', [ exports.TYPE_LIST, name, domain ]);
const results = await database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE type = ? AND name = ? AND domain = ?', [ TYPE_LIST, name, domain ]);
if (results.length === 0) return null;
return postProcessMailbox(results[0]);
@@ -1140,7 +1135,7 @@ async function addMailingList(name, domain, data, auditSource) {
if (!validator.isEmail(members[i])) throw new BoxError(BoxError.BAD_FIELD, 'Invalid mail member: ' + members[i]);
}
[error] = await safe(database.query('INSERT INTO mailboxes (name, type, domain, ownerId, ownerType, membersJson, membersOnly, active) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [ name, exports.TYPE_LIST, domain, 'admin', 'user', JSON.stringify(members), membersOnly, active ]));
[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 ]));
if (error && error.sqlCode === 'ER_DUP_ENTRY') throw new BoxError(BoxError.ALREADY_EXISTS, 'mailbox already exists');
if (error) throw error;
@@ -1223,9 +1218,9 @@ async function resolveMailingList(listName, listDomain) {
continue;
}
if (entry.type === exports.TYPE_MAILBOX) { // concrete mailbox
if (entry.type === TYPE_MAILBOX) { // concrete mailbox
resolvedMembers.push(member);
} else if (entry.type === exports.TYPE_ALIAS) { // resolve aliases
} else if (entry.type === TYPE_ALIAS) { // resolve aliases
toResolve = toResolve.concat(`${entry.aliasName}@${entry.aliasDomain}`);
} else { // resolve list members
toResolve = toResolve.concat(entry.members);