mailboxdb: merge into mail.js

This commit is contained in:
Girish Ramakrishnan
2021-08-17 15:45:57 -07:00
parent 98ef6dfae9
commit fa9938f50a
12 changed files with 727 additions and 1233 deletions

View File

@@ -37,11 +37,13 @@ exports = module.exports = {
getMailboxCount,
listMailboxes,
listAllMailboxes,
getMailbox,
addMailbox,
updateMailbox,
removeMailbox,
delMailbox,
getAlias,
getAliases,
setAliases,
@@ -49,7 +51,7 @@ exports = module.exports = {
getList,
addList,
updateList,
removeList,
delList,
resolveList,
OWNERTYPE_USER: 'user',
@@ -57,7 +59,11 @@ exports = module.exports = {
DEFAULT_MEMORY_LIMIT: 512 * 1024 * 1024,
_removeMailboxes: removeMailboxes,
TYPE_MAILBOX: 'mailbox',
TYPE_LIST: 'list',
TYPE_ALIAS: 'alias',
_delByDomain: delByDomain,
_readDkimPublicKeySync: readDkimPublicKeySync,
_updateDomain: updateDomain
};
@@ -75,8 +81,8 @@ const assert = require('assert'),
eventlog = require('./eventlog.js'),
hat = require('./hat.js'),
infra = require('./infra_version.js'),
mailboxdb = require('./mailboxdb.js'),
mailer = require('./mailer.js'),
mysql = require('mysql'),
net = require('net'),
nodemailer = require('nodemailer'),
path = require('path'),
@@ -97,15 +103,38 @@ const assert = require('assert'),
_ = require('underscore');
const DNS_OPTIONS = { timeout: 5000 };
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
const REMOVE_MAILBOX = path.join(__dirname, 'scripts/rmmailbox.sh');
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
const REMOVE_MAILBOX_CMD = path.join(__dirname, 'scripts/rmmailbox.sh');
const MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'ownerType', 'aliasName', 'aliasDomain', 'creationTime', 'membersJson', 'membersOnly', 'domain', 'active' ].join(',');
const MAILDB_FIELDS = [ 'domain', 'enabled', 'mailFromValidation', 'catchAllJson', 'relayJson', 'dkimSelector', 'bannerJson' ].join(',');
const domainsGet = util.callbackify(domains.get),
domainsList = util.callbackify(domains.list);
function postProcess(data) {
function postProcessMailbox(data) {
data.members = safe.JSON.parse(data.membersJson) || [ ];
delete data.membersJson;
data.membersOnly = !!data.membersOnly;
data.active = !!data.active;
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) {
data.enabled = !!data.enabled; // int to boolean
data.mailFromValidation = !!data.mailFromValidation; // int to boolean
@@ -542,121 +571,101 @@ function getStatus(domain, callback) {
});
}
function checkConfiguration(callback) {
assert.strictEqual(typeof callback, 'function');
async function checkConfiguration() {
let messages = {};
domainsList(function (error, allDomains) {
if (error) return callback(error);
const allDomains = await listDomains();
async.eachSeries(allDomains, function (domainObject, iteratorCallback) {
getStatus(domainObject.domain, function (error, result) {
if (error) return iteratorCallback(error);
for (const domainObject of allDomains) {
const result = await util.promisify(getStatus)(domainObject.domain);
let message = [];
let message = [];
Object.keys(result.dns).forEach((type) => {
const record = result.dns[type];
if (!record.status) message.push(`${type.toUpperCase()} DNS record (${record.type}) did not match.\n * Hostname: \`${record.name}\`\n * Expected: \`${record.expected}\`\n * Actual: \`${record.value}\``);
});
if (result.relay && result.relay.status === false) message.push(`Relay error: ${result.relay.value}`);
if (result.rbl && result.rbl.status === false) { // rbl field contents is optional
const servers = result.rbl.servers.map((bs) => `[${bs.name}](${bs.site})`); // in markdown
message.push(`This server's IP \`${result.rbl.ip}\` is blacklisted in the following servers - ${servers.join(', ')}`);
}
if (message.length) messages[domainObject.domain] = message;
iteratorCallback(null);
});
}, function (error) {
if (error) return callback(error);
// 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';
});
if (markdownMessage) markdownMessage += 'Email Status is checked every 30 minutes.\n See the [troubleshooting docs](https://docs.cloudron.io/troubleshooting/#mail-dns) for more information.\n';
callback(null, markdownMessage); // empty message means all status checks succeeded
Object.keys(result.dns).forEach((type) => {
const record = result.dns[type];
if (!record.status) message.push(`${type.toUpperCase()} DNS record (${record.type}) did not match.\n * Hostname: \`${record.name}\`\n * Expected: \`${record.expected}\`\n * Actual: \`${record.value}\``);
});
if (result.relay && result.relay.status === false) message.push(`Relay error: ${result.relay.value}`);
if (result.rbl && result.rbl.status === false) { // rbl field contents is optional
const servers = result.rbl.servers.map((bs) => `[${bs.name}](${bs.site})`); // in markdown
message.push(`This server's IP \`${result.rbl.ip}\` is blacklisted in the following servers - ${servers.join(', ')}`);
}
if (message.length) messages[domainObject.domain] = message;
}
// 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';
});
if (markdownMessage) markdownMessage += 'Email Status is checked every 30 minutes.\n See the [troubleshooting docs](https://docs.cloudron.io/troubleshooting/#mail-dns) for more information.\n';
return markdownMessage; // empty message means all status checks succeeded
}
function createMailConfig(mailFqdn, mailDomain, callback) {
async function createMailConfig(mailFqdn, mailDomain) {
assert.strictEqual(typeof mailFqdn, 'string');
assert.strictEqual(typeof mailDomain, 'string');
assert.strictEqual(typeof callback, 'function');
debug('createMailConfig: generating mail config');
const listDomainsFunc = util.callbackify(listDomains);
const mailDomains = await listDomains();
listDomainsFunc(function (error, mailDomains) {
if (error) return callback(error);
const mailOutDomains = mailDomains.filter(d => d.relay.provider !== 'noop').map(d => d.domain).join(',');
const mailInDomains = mailDomains.filter(function (d) { return d.enabled; }).map(function (d) { return d.domain; }).join(',');
const mailOutDomains = mailDomains.filter(d => d.relay.provider !== 'noop').map(d => d.domain).join(',');
const mailInDomains = mailDomains.filter(function (d) { return d.enabled; }).map(function (d) { return d.domain; }).join(',');
// mail_domain is used for SRS
if (!safe.fs.writeFileSync(path.join(paths.ADDON_CONFIG_DIR, 'mail/mail.ini'),
`mail_in_domains=${mailInDomains}\nmail_out_domains=${mailOutDomains}\nmail_server_name=${mailFqdn}\nmail_domain=${mailDomain}\n\n`, 'utf8')) {
throw new BoxError(BoxError.FS_ERROR, `Could not create mail var file: ${safe.error.message}`);
}
// mail_domain is used for SRS
if (!safe.fs.writeFileSync(path.join(paths.ADDON_CONFIG_DIR, 'mail/mail.ini'),
`mail_in_domains=${mailInDomains}\nmail_out_domains=${mailOutDomains}\nmail_server_name=${mailFqdn}\nmail_domain=${mailDomain}\n\n`, 'utf8')) {
return callback(new BoxError(BoxError.FS_ERROR, 'Could not create mail var file:' + safe.error.message));
// enable_outbound makes plugin forward email for relayed mail. non-relayed mail always hits LMTP plugin first
if (!safe.fs.writeFileSync(path.join(paths.ADDON_CONFIG_DIR, 'mail/smtp_forward.ini'), 'enable_outbound=false\ndomain_selector=mail_from\n', 'utf8')) {
throw new BoxError(BoxError.FS_ERROR, `Could not create smtp forward file: ${safe.error.message}`);
}
// create sections for per-domain configuration
for (const domain of mailDomains) {
const catchAll = domain.catchAll.map(function (c) { return `${c}@${domain.domain}`; }).join(',');
const mailFromValidation = domain.mailFromValidation;
if (!safe.fs.appendFileSync(path.join(paths.ADDON_CONFIG_DIR, 'mail/mail.ini'),
`[${domain.domain}]\ncatch_all=${catchAll}\nmail_from_validation=${mailFromValidation}\n\n`, 'utf8')) {
throw new BoxError(BoxError.FS_ERROR, `Could not create mail var file: ${safe.error.message}`);
}
// enable_outbound makes plugin forward email for relayed mail. non-relayed mail always hits LMTP plugin first
if (!safe.fs.writeFileSync(path.join(paths.ADDON_CONFIG_DIR, 'mail/smtp_forward.ini'), 'enable_outbound=false\ndomain_selector=mail_from\n', 'utf8')) {
return callback(new BoxError(BoxError.FS_ERROR, 'Could not create smtp forward file:' + safe.error.message));
if (!safe.fs.writeFileSync(`${paths.ADDON_CONFIG_DIR}/mail/banner/${domain.domain}.text`, domain.banner.text || '')) throw new BoxError(BoxError.FS_ERROR, `Could not create text banner file: ${safe.error.message}`);
if (!safe.fs.writeFileSync(`${paths.ADDON_CONFIG_DIR}/mail/banner/${domain.domain}.html`, domain.banner.html || '')) throw new BoxError(BoxError.FS_ERROR, `Could not create html banner file: ${safe.error.message}`);
const relay = domain.relay;
const enableRelay = relay.provider !== 'cloudron-smtp' && relay.provider !== 'noop',
host = relay.host || '',
port = relay.port || 25,
authType = relay.username ? 'plain' : '',
username = relay.username || '',
password = relay.password || '';
if (!enableRelay) continue;
if (!safe.fs.appendFileSync(paths.ADDON_CONFIG_DIR + '/mail/smtp_forward.ini',
`[${domain.domain}]\nenable_outbound=true\nhost=${host}\nport=${port}\nenable_tls=true\nauth_type=${authType}\nauth_user=${username}\nauth_pass=${password}\n\n`, 'utf8')) {
throw new BoxError(BoxError.FS_ERROR, `Could not create mail var file: ${safe.error.message}`);
}
}
// create sections for per-domain configuration
async.eachSeries(mailDomains, function (domain, iteratorDone) {
const catchAll = domain.catchAll.map(function (c) { return `${c}@${domain.domain}`; }).join(',');
const mailFromValidation = domain.mailFromValidation;
if (!safe.fs.appendFileSync(path.join(paths.ADDON_CONFIG_DIR, 'mail/mail.ini'),
`[${domain.domain}]\ncatch_all=${catchAll}\nmail_from_validation=${mailFromValidation}\n\n`, 'utf8')) {
return iteratorDone(new BoxError(BoxError.FS_ERROR, 'Could not create mail var file:' + safe.error.message));
}
if (!safe.fs.writeFileSync(`${paths.ADDON_CONFIG_DIR}/mail/banner/${domain.domain}.text`, domain.banner.text || '')) return iteratorDone(new BoxError(BoxError.FS_ERROR, 'Could not create text banner file:' + safe.error.message));
if (!safe.fs.writeFileSync(`${paths.ADDON_CONFIG_DIR}/mail/banner/${domain.domain}.html`, domain.banner.html || '')) return iteratorDone(new BoxError(BoxError.FS_ERROR, 'Could not create html banner file:' + safe.error.message));
const relay = domain.relay;
const enableRelay = relay.provider !== 'cloudron-smtp' && relay.provider !== 'noop',
host = relay.host || '',
port = relay.port || 25,
authType = relay.username ? 'plain' : '',
username = relay.username || '',
password = relay.password || '';
if (!enableRelay) return iteratorDone();
if (!safe.fs.appendFileSync(paths.ADDON_CONFIG_DIR + '/mail/smtp_forward.ini',
`[${domain.domain}]\nenable_outbound=true\nhost=${host}\nport=${port}\nenable_tls=true\nauth_type=${authType}\nauth_user=${username}\nauth_pass=${password}\n\n`, 'utf8')) {
return iteratorDone(new BoxError(BoxError.FS_ERROR, 'Could not create mail var file:' + safe.error.message));
}
iteratorDone();
}, function (error) {
if (error) return callback(error);
callback(null, mailInDomains.length !== 0 /* allowInbound */);
});
});
return mailInDomains.length !== 0 /* allowInbound */;
}
function configureMail(mailFqdn, mailDomain, serviceConfig, callback) {
async function configureMail(mailFqdn, mailDomain, serviceConfig) {
assert.strictEqual(typeof mailFqdn, 'string');
assert.strictEqual(typeof mailDomain, 'string');
assert.strictEqual(typeof serviceConfig, 'object');
assert.strictEqual(typeof callback, 'function');
// mail (note: 2587 is hardcoded in mail container and app use this port)
// MAIL_SERVER_NAME is the hostname of the mailserver i.e server uses these certs
@@ -668,52 +677,43 @@ function configureMail(mailFqdn, mailDomain, serviceConfig, callback) {
const memory = system.getMemoryAllocation(memoryLimit);
const cloudronToken = hat(8 * 128), relayToken = hat(8 * 128);
const getCertificatePath = util.callbackify(reverseProxy.getCertificatePath);
getCertificatePath(mailFqdn, mailDomain, function (error, bundle) {
if (error) return callback(error);
const bundle = await reverseProxy.getCertificatePath(mailFqdn, mailDomain);
const dhparamsFilePath = path.join(paths.ADDON_CONFIG_DIR, 'mail/dhparams.pem');
const mailCertFilePath = path.join(paths.ADDON_CONFIG_DIR, 'mail/tls_cert.pem');
const mailKeyFilePath = path.join(paths.ADDON_CONFIG_DIR, 'mail/tls_key.pem');
const dhparamsFilePath = path.join(paths.ADDON_CONFIG_DIR, 'mail/dhparams.pem');
const mailCertFilePath = path.join(paths.ADDON_CONFIG_DIR, 'mail/tls_cert.pem');
const mailKeyFilePath = path.join(paths.ADDON_CONFIG_DIR, 'mail/tls_key.pem');
if (!safe.child_process.execSync(`cp ${paths.DHPARAMS_FILE} ${dhparamsFilePath}`)) return callback(new BoxError(BoxError.FS_ERROR, 'Could not copy dhparams:' + safe.error.message));
if (!safe.child_process.execSync(`cp ${bundle.certFilePath} ${mailCertFilePath}`)) return callback(new BoxError(BoxError.FS_ERROR, 'Could not create cert file:' + safe.error.message));
if (!safe.child_process.execSync(`cp ${bundle.keyFilePath} ${mailKeyFilePath}`)) return callback(new BoxError(BoxError.FS_ERROR, 'Could not create key file:' + safe.error.message));
if (!safe.child_process.execSync(`cp ${paths.DHPARAMS_FILE} ${dhparamsFilePath}`)) throw new BoxError(BoxError.FS_ERROR, 'Could not copy dhparams:' + safe.error.message);
if (!safe.child_process.execSync(`cp ${bundle.certFilePath} ${mailCertFilePath}`)) throw new BoxError(BoxError.FS_ERROR, 'Could not create cert file:' + safe.error.message);
if (!safe.child_process.execSync(`cp ${bundle.keyFilePath} ${mailKeyFilePath}`)) throw new BoxError(BoxError.FS_ERROR, 'Could not create key file:' + safe.error.message);
async.series([
shell.exec.bind(null, 'stopMail', 'docker stop mail || true'),
shell.exec.bind(null, 'removeMail', 'docker rm -f mail || true'),
], function (error) {
if (error) return callback(error);
await shell.promises.exec('stopMail', 'docker stop mail || true');
await shell.promises.exec('removeMail', 'docker rm -f mail || true');
createMailConfig(mailFqdn, mailDomain, function (error, allowInbound) {
if (error) return callback(error);
const allowInbound = await createMailConfig(mailFqdn, mailDomain);
var ports = allowInbound ? '-p 587:2587 -p 993:9993 -p 4190:4190 -p 25:2587' : '';
const ports = allowInbound ? '-p 587:2587 -p 993:9993 -p 4190:4190 -p 25:2587' : '';
const cmd = `docker run --restart=always -d --name="mail" \
--net cloudron \
--net-alias mail \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=mail \
-m ${memory} \
--memory-swap ${memoryLimit} \
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_MAIL_TOKEN="${cloudronToken}" \
-e CLOUDRON_RELAY_TOKEN="${relayToken}" \
-v "${paths.MAIL_DATA_DIR}:/app/data" \
-v "${paths.PLATFORM_DATA_DIR}/addons/mail:/etc/mail" \
${ports} \
--label isCloudronManaged=true \
--read-only -v /run -v /tmp ${tag}`;
const cmd = `docker run --restart=always -d --name="mail" \
--net cloudron \
--net-alias mail \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=mail \
-m ${memory} \
--memory-swap ${memoryLimit} \
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_MAIL_TOKEN="${cloudronToken}" \
-e CLOUDRON_RELAY_TOKEN="${relayToken}" \
-v "${paths.MAIL_DATA_DIR}:/app/data" \
-v "${paths.PLATFORM_DATA_DIR}/addons/mail:/etc/mail" \
${ports} \
--label isCloudronManaged=true \
--read-only -v /run -v /tmp ${tag}`;
shell.exec('startMail', cmd, callback);
});
});
});
await shell.promises.exec('startMail', cmd);
}
function getMailAuth(callback) {
@@ -746,11 +746,12 @@ function restartMail(callback) {
if (process.env.BOX_ENV === 'test' && !process.env.TEST_CREATE_INFRA) return callback();
services.getServiceConfig('mail', function (error, serviceConfig) {
services.getServiceConfig('mail', async function (error, serviceConfig) {
if (error) return callback(error);
debug(`restartMail: restarting mail container with mailFqdn:${settings.mailFqdn()} dashboardDomain:${settings.dashboardDomain()}`);
configureMail(settings.mailFqdn(), settings.dashboardDomain(), serviceConfig, callback);
[error] = await safe(configureMail(settings.mailFqdn(), settings.dashboardDomain(), serviceConfig));
callback(error);
});
}
@@ -775,7 +776,7 @@ async function getDomain(domain) {
const result = await database.query(`SELECT ${MAILDB_FIELDS} FROM mail WHERE domain = ?`, [ domain ]);
if (result.length === 0) return null;
return postProcess(result[0]);
return postProcessDomain(result[0]);
}
async function updateDomain(domain, data) {
@@ -804,7 +805,7 @@ async function updateDomain(domain, data) {
async function listDomains() {
const results = await database.query(`SELECT ${MAILDB_FIELDS} FROM mail ORDER BY domain`);
results.forEach(function (result) { postProcess(result); });
results.forEach(function (result) { postProcessDomain(result); });
return results;
}
@@ -1126,79 +1127,99 @@ async function setMailEnabled(domain, enabled, auditSource) {
await eventlog.add(enabled ? eventlog.ACTION_MAIL_ENABLED : eventlog.ACTION_MAIL_DISABLED, auditSource, { domain });
}
function sendTestMail(domain, to, callback) {
async function sendTestMail(domain, to) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof to, 'string');
assert.strictEqual(typeof callback, 'function');
const getDomainFunc = util.callbackify(getDomain);
const result = await getDomain(domain);
if (!result) throw new BoxError(BoxError.NOT_FOUND, 'mail domain not found');
getDomainFunc(domain, function (error, result) {
if (error) return callback(error);
if (!result) return callback(new BoxError(BoxError.NOT_FOUND, 'mail domain not found'));
mailer.sendTestMail(result.domain, to, function (error) {
if (error) return callback(error);
callback();
});
});
await util.promisify(mailer.sendTestMail)(result.domain);
}
function listMailboxes(domain, search, page, perPage, callback) {
async function listMailboxes(domain, search, page, perPage) {
assert.strictEqual(typeof domain, 'string');
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
mailboxdb.listMailboxes(domain, search, page, perPage, function (error, result) {
if (error) return callback(error);
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
callback(null, result);
});
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 '
+ ` FROM (SELECT * FROM mailboxes WHERE type='${exports.TYPE_MAILBOX}') AS m1`
+ ` LEFT JOIN (SELECT * FROM mailboxes WHERE type='${exports.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'
+ searchQuery
+ ' ORDER BY name LIMIT ?,?';
const results = await database.query(query, [ domain, (page-1)*perPage, perPage ]);
results.forEach(postProcessMailbox);
results.forEach(postProcessAliases);
return results;
}
function getMailboxCount(domain, callback) {
async function listAllMailboxes(page, perPage) {
assert.strictEqual(typeof page, 'number');
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 '
+ ` FROM (SELECT * FROM mailboxes WHERE type='${exports.TYPE_MAILBOX}') AS m1`
+ ` LEFT JOIN (SELECT * FROM mailboxes WHERE type='${exports.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 ?,?';
const results = await database.query(query, [ (page-1)*perPage, perPage ]);
results.forEach(postProcessMailbox);
results.forEach(postProcessAliases);
return results;
}
async function getMailboxCount(domain) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.getMailboxCount(domain, function (error, result) {
if (error) return callback(error);
const results = await database.query('SELECT COUNT(*) AS total FROM mailboxes WHERE type = ? AND domain = ?', [ exports.TYPE_MAILBOX, domain ]);
callback(null, result);
});
return results[0].total;
}
function removeMailboxes(domain, callback) {
async function delByDomain(domain) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.delByDomain(domain, function (error) {
if (error) return callback(error);
callback();
});
await database.query('DELETE FROM mailboxes WHERE domain = ?', [ domain ]);
}
function getMailbox(name, domain, callback) {
async function get(name, domain) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.getMailbox(name, domain, function (error, result) {
if (error) return callback(error);
const results = await database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND domain = ?', [ name, domain ]);
if (results.length === 0) return null;
callback(null, result);
});
return postProcessMailbox(results[0]);
}
function addMailbox(name, domain, data, auditSource, callback) {
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 ]);
if (results.length === 0) return null;
return postProcessMailbox(results[0]);
}
async function addMailbox(name, domain, data, auditSource) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
const { ownerId, ownerType, active } = data;
assert.strictEqual(typeof ownerId, 'string');
@@ -1207,26 +1228,23 @@ function addMailbox(name, domain, data, auditSource, callback) {
name = name.toLowerCase();
var error = validateName(name);
if (error) return callback(error);
let error = validateName(name);
if (error) throw error;
if (ownerType !== exports.OWNERTYPE_USER && ownerType !== exports.OWNERTYPE_GROUP) return callback(new BoxError(BoxError.BAD_FIELD, 'bad owner type'));
if (ownerType !== exports.OWNERTYPE_USER && ownerType !== exports.OWNERTYPE_GROUP) throw new BoxError(BoxError.BAD_FIELD, 'bad owner type');
mailboxdb.addMailbox(name, domain, data, function (error) {
if (error) return callback(error);
[error] = await safe(database.query('INSERT INTO mailboxes (name, type, domain, ownerId, ownerType, active) VALUES (?, ?, ?, ?, ?, ?)', [ name, exports.TYPE_MAILBOX, domain, ownerId, ownerType, active ]));
if (error && error.code === 'ER_DUP_ENTRY') throw new BoxError(BoxError.ALREADY_EXISTS, 'mailbox already exists');
if (error) throw error;
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_ADD, auditSource, { name, domain, ownerId, ownerType, active });
callback(null);
});
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_ADD, auditSource, { name, domain, ownerId, ownerType, active });
}
function updateMailbox(name, domain, data, auditSource, callback) {
async function updateMailbox(name, domain, data, auditSource) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
const { ownerId, ownerType, active } = data;
assert.strictEqual(typeof ownerId, 'string');
@@ -1235,19 +1253,15 @@ function updateMailbox(name, domain, data, auditSource, callback) {
name = name.toLowerCase();
if (ownerType !== exports.OWNERTYPE_USER && ownerType !== exports.OWNERTYPE_GROUP) return callback(new BoxError(BoxError.BAD_FIELD, 'bad owner type'));
if (ownerType !== exports.OWNERTYPE_USER && ownerType !== exports.OWNERTYPE_GROUP) throw new BoxError(BoxError.BAD_FIELD, 'bad owner type');
getMailbox(name, domain, function (error, result) {
if (error) return callback(error);
const mailbox = await getMailbox(name, domain);
if (!mailbox) throw new BoxError(BoxError.NOT_FOUND, 'No such mailbox');
mailboxdb.updateMailbox(name, domain, data, function (error) {
if (error) return callback(error);
const result = await database.query('UPDATE mailboxes SET ownerId = ?, ownerType = ?, active = ? WHERE name = ? AND domain = ?', [ ownerId, ownerType, active, name, domain ]);
if (result.affectedRows === 0) throw new BoxError(BoxError.NOT_FOUND, 'Mailbox not found');
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, oldUserId: result.userId, ownerId, ownerType, active });
callback(null);
});
});
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, oldUserId: mailbox.userId, ownerId, ownerType, active });
}
function removeSolrIndex(mailbox, callback) {
@@ -1267,101 +1281,117 @@ function removeSolrIndex(mailbox, callback) {
});
}
function removeMailbox(name, domain, options, auditSource, callback) {
async function delMailbox(name, domain, options, auditSource) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
const mailbox =`${name}@${domain}`;
const deleteMailFunc = options.deleteMails ? shell.sudo.bind(null, 'removeMailbox', [ REMOVE_MAILBOX, mailbox ], {}) : (next) => next();
if (options.deleteMails) {
const [error] = await safe(shell.promises.sudo('removeMailbox', [ REMOVE_MAILBOX_CMD, mailbox ], {}));
if (error) throw new BoxError(BoxError.FS_ERROR, `Error removing mailbox: ${error.message}`);
}
deleteMailFunc(function (error) {
if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error removing mailbox: ${error.message}`));
// 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');
mailboxdb.del(name, domain, function (error) {
if (error) return callback(error);
removeSolrIndex(mailbox, NOOP_CALLBACK);
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_REMOVE, auditSource, { name, domain });
callback();
});
});
removeSolrIndex(mailbox, NOOP_CALLBACK);
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_REMOVE, auditSource, { name, domain });
}
function getAliases(name, domain, callback) {
async function getAlias(name, domain) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
getMailbox(name, domain, function (error) {
if (error) return callback(error);
const results = await database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE name = ? AND type = ? AND domain = ?`, [ name, exports.TYPE_ALIAS, domain ]);
if (results.length === 0) return null;
mailboxdb.getAliasesForName(name, domain, function (error, aliases) {
if (error) return callback(error);
results.forEach(function (result) { postProcessMailbox(result); });
callback(null, aliases);
});
});
return results[0];
}
function setAliases(name, domain, aliases, callback) {
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');
return await database.query('SELECT name, domain FROM mailboxes WHERE type = ? AND aliasName = ? AND aliasDomain = ? ORDER BY name', [ exports.TYPE_ALIAS, name, domain ]);
}
async function setAliases(name, domain, aliases) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(aliases));
assert.strictEqual(typeof callback, 'function');
for (var i = 0; i < aliases.length; i++) {
for (let i = 0; i < aliases.length; i++) {
let name = aliases[i].name.toLowerCase();
let domain = aliases[i].domain.toLowerCase();
let error = validateName(name);
if (error) return callback(error);
if (error) throw error;
if (!validator.isEmail(`${name}@${domain}`)) return callback(new BoxError(BoxError.BAD_FIELD, `Invalid email: ${name}@${domain}`));
if (!validator.isEmail(`${name}@${domain}`)) throw new BoxError(BoxError.BAD_FIELD, `Invalid email: ${name}@${domain}`);
aliases[i] = { name, domain };
}
mailboxdb.setAliasesForName(name, domain, aliases, function (error) {
if (error) return callback(error);
callback(null);
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');
let queries = [];
// clear existing aliases
queries.push({ query: 'DELETE FROM mailboxes WHERE aliasName = ? AND aliasDomain = ? AND type = ?', args: [ name, domain, exports.TYPE_ALIAS ] });
aliases.forEach(function (alias) {
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 ] });
});
const [error] = await safe(database.transaction(queries));
if (error && error.code === 'ER_DUP_ENTRY' && error.message.indexOf('mailboxes_name_domain_unique_index') !== -1) {
const aliasMatch = error.message.match(new RegExp(`^ER_DUP_ENTRY: Duplicate entry '(.*)-${domain}' for key 'mailboxes_name_domain_unique_index'$`));
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;
}
function getLists(domain, search, page, perPage, callback) {
async function getLists(domain, search, page, perPage) {
assert.strictEqual(typeof domain, 'string');
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
mailboxdb.getLists(domain, search, page, perPage, function (error, result) {
if (error) return callback(error);
let query = `SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? AND domain = ?`;
if (search) query += ' AND (name LIKE ' + mysql.escape('%' + search + '%') + ' OR membersJson LIKE ' + mysql.escape('%' + search + '%') + ')';
callback(null, result);
});
query += 'ORDER BY name LIMIT ?,?';
const results = await database.query(query, [ exports.TYPE_LIST, domain, (page-1)*perPage, perPage ]);
results.forEach(function (result) { postProcessMailbox(result); });
return results;
}
function getList(name, domain, callback) {
async function getList(name, domain) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.getList(name, domain, function (error, result) {
if (error) return callback(error);
const results = await database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE type = ? AND name = ? AND domain = ?', [ exports.TYPE_LIST, name, domain ]);
if (results.length === 0) return null;
callback(null, result);
});
return postProcessMailbox(results[0]);
}
function addList(name, domain, data, auditSource, callback) {
async function addList(name, domain, data, auditSource) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
const { members, membersOnly, active } = data;
assert(Array.isArray(members));
@@ -1370,28 +1400,25 @@ function addList(name, domain, data, auditSource, callback) {
name = name.toLowerCase();
var error = validateName(name);
if (error) return callback(error);
let error = validateName(name);
if (error) throw error;
for (var i = 0; i < members.length; i++) {
if (!validator.isEmail(members[i])) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid mail member: ' + members[i]));
for (let i = 0; i < members.length; i++) {
if (!validator.isEmail(members[i])) throw new BoxError(BoxError.BAD_FIELD, 'Invalid mail member: ' + members[i]);
}
mailboxdb.addList(name, domain, data, function (error) {
if (error) return callback(error);
[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 ]));
if (error && error.code === 'ER_DUP_ENTRY') throw new BoxError(BoxError.ALREADY_EXISTS, 'mailbox already exists');
if (error) throw error;
eventlog.add(eventlog.ACTION_MAIL_LIST_ADD, auditSource, { name, domain, members, membersOnly, active });
callback();
});
eventlog.add(eventlog.ACTION_MAIL_LIST_ADD, auditSource, { name, domain, members, membersOnly, active });
}
function updateList(name, domain, data, auditSource, callback) {
async function updateList(name, domain, data, auditSource) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
const { members, membersOnly, active } = data;
assert(Array.isArray(members));
@@ -1400,90 +1427,76 @@ function updateList(name, domain, data, auditSource, callback) {
name = name.toLowerCase();
var error = validateName(name);
if (error) return callback(error);
let error = validateName(name);
if (error) throw error;
for (var i = 0; i < members.length; i++) {
if (!validator.isEmail(members[i])) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid email: ' + members[i]));
for (let i = 0; i < members.length; i++) {
if (!validator.isEmail(members[i])) throw new BoxError(BoxError.BAD_FIELD, 'Invalid email: ' + members[i]);
}
getList(name, domain, function (error, result) {
if (error) return callback(error);
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');
mailboxdb.updateList(name, domain, data, function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, oldMembers: result.members, members, membersOnly, active });
callback(null);
});
});
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, oldMembers: result.members, members, membersOnly, active });
}
function removeList(name, domain, auditSource, callback) {
async function delList(name, domain, auditSource) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
mailboxdb.del(name, domain, function (error) {
if (error) return callback(error);
// 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');
eventlog.add(eventlog.ACTION_MAIL_LIST_REMOVE, auditSource, { name, domain });
callback();
});
eventlog.add(eventlog.ACTION_MAIL_LIST_REMOVE, auditSource, { name, domain });
}
// resolves the members of a list. i.e the lists and aliases
function resolveList(listName, listDomain, callback) {
async function resolveList(listName, listDomain) {
assert.strictEqual(typeof listName, 'string');
assert.strictEqual(typeof listDomain, 'string');
assert.strictEqual(typeof callback, 'function');
const listDomainsFunc = util.callbackify(listDomains);
const mailDomains = await listDomains();
const mailInDomains = mailDomains.filter(function (d) { return d.enabled; }).map(function (d) { return d.domain; }).join(',');
listDomainsFunc(function (error, mailDomains) {
if (error) return callback(error);
const list = await getList(listName, listDomain);
if (!list) throw new BoxError(BoxError.NOT_FOUND, 'List not found');
const mailInDomains = mailDomains.filter(function (d) { return d.enabled; }).map(function (d) { return d.domain; }).join(',');
let resolvedMembers = [], toResolve = list.members.slice(), visited = []; // slice creates a copy of array
mailboxdb.getList(listName, listDomain, function (error, list) {
if (error) return callback(error);
while (toResolve.length != 0) {
const toProcess = toResolve.shift();
const parts = toProcess.split('@');
const memberName = parts[0].split('+')[0], memberDomain = parts[1];
let result = [], toResolve = list.members.slice(), visited = []; // slice creates a copy of array
if (!mailInDomains.includes(memberDomain)) { // external domain
resolvedMembers.push(toProcess);
continue;
}
async.whilst((testDone) => testDone(null, toResolve.length != 0), function (iteratorCallback) {
const toProcess = toResolve.shift();
const parts = toProcess.split('@');
const memberName = parts[0].split('+')[0], memberDomain = parts[1];
const member =`${memberName}@${memberDomain}`; // cleaned up without any '+' subaddress
if (visited.includes(member)) {
debug(`resolveList: list ${listName}@${listDomain} has a recursion at member ${member}`);
continue;
}
visited.push(member);
if (!mailInDomains.includes(memberDomain)) { result.push(toProcess); return iteratorCallback(); } // external domain
const entry = await get(memberName, memberDomain);
if (!entry) { // let it bounce
resolvedMembers.push(member);
continue;
}
const member =`${memberName}@${memberDomain}`; // cleaned up without any '+' subaddress
if (visited.includes(member)) {
debug(`resolveList: list ${listName}@${listDomain} has a recursion at member ${member}`);
return iteratorCallback();
}
visited.push(member);
if (entry.type === exports.TYPE_MAILBOX) { // concrete mailbox
resolvedMembers.push(member);
} else if (entry.type === exports.TYPE_ALIAS) { // resolve aliases
toResolve = toResolve.concat(`${entry.aliasName}@${entry.aliasDomain}`);
} else { // resolve list members
toResolve = toResolve.concat(entry.members);
}
}
mailboxdb.get(memberName, memberDomain, function (error, entry) {
if (error && error.reason == BoxError.NOT_FOUND) { result.push(member); return iteratorCallback(); } // let it bounce
if (error) return iteratorCallback(error);
if (entry.type === mailboxdb.TYPE_MAILBOX) { // concrete mailbox
result.push(member);
} else if (entry.type === mailboxdb.TYPE_ALIAS) { // resolve aliases
toResolve = toResolve.concat(`${entry.aliasName}@${entry.aliasDomain}`);
} else { // resolve list members
toResolve = toResolve.concat(entry.members);
}
iteratorCallback();
});
}, function (error) {
callback(error, result, list);
});
});
});
return { resolvedMembers, list };
}