diff --git a/src/cloudron.js b/src/cloudron.js index 31bff1cb4..6122420d3 100644 --- a/src/cloudron.js +++ b/src/cloudron.js @@ -209,15 +209,9 @@ function runSystemChecks(callback) { ], callback); } -function checkMailStatus(callback) { - assert.strictEqual(typeof callback, 'function'); - - mail.checkConfiguration(async function (error, message) { - if (error) return callback(error); - - await safe(notifications.alert(notifications.ALERT_MAIL_STATUS, 'Email is not configured properly', message)); - callback(); - }); +async function checkMailStatus() { + const message = await mail.checkConfiguration(); + await notifications.alert(notifications.ALERT_MAIL_STATUS, 'Email is not configured properly', message); } async function checkRebootRequired() { diff --git a/src/ldap.js b/src/ldap.js index 67e6b1cd4..34b321cdf 100644 --- a/src/ldap.js +++ b/src/ldap.js @@ -18,7 +18,6 @@ const assert = require('assert'), groups = require('./groups.js'), ldap = require('ldapjs'), mail = require('./mail.js'), - mailboxdb = require('./mailboxdb.js'), safe = require('safetydance'), services = require('./services.js'), users = require('./users.js'), @@ -261,151 +260,33 @@ function groupAdminsCompare(req, res, next) { }); } -function mailboxSearch(req, res, next) { +async function mailboxSearch(req, res, next) { debug('mailbox search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id); // if cn is set we only search for one mailbox specifically if (req.dn.rdns[0].attrs.cn) { - var email = req.dn.rdns[0].attrs.cn.value.toLowerCase(); - var parts = email.split('@'); + const email = req.dn.rdns[0].attrs.cn.value.toLowerCase(); + const parts = email.split('@'); if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString())); - mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) { - if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); - if (error) return next(new ldap.OperationsError(error.toString())); - - if (!mailbox.active) return next(new ldap.NoSuchObjectError(req.dn.toString())); - - let obj = { - dn: req.dn.toString(), - attributes: { - objectclass: ['mailbox'], - objectcategory: 'mailbox', - cn: `${mailbox.name}@${mailbox.domain}`, - uid: `${mailbox.name}@${mailbox.domain}`, - mail: `${mailbox.name}@${mailbox.domain}` - } - }; - - // ensure all filter values are also lowercase - var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null); - if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString())); - - if (lowerCaseFilter.matches(obj.attributes)) { - finalSend([ obj ], req, res, next); - } else { - res.end(); - } - }); - } else if (req.dn.rdns[0].attrs.domain) { // legacy ldap mailbox search for old sogo - var domain = req.dn.rdns[0].attrs.domain.value.toLowerCase(); - - mailboxdb.listMailboxes(domain, 1, 1000, function (error, mailboxes) { - if (error) return next(new ldap.OperationsError(error.toString())); - - mailboxes = mailboxes.filter(m => m.active); - - let results = []; - - // send mailbox objects - mailboxes.forEach(function (mailbox) { - var dn = ldap.parseDN(`cn=${mailbox.name}@${domain},domain=${domain},ou=mailboxes,dc=cloudron`); - - var obj = { - dn: dn.toString(), - attributes: { - objectclass: ['mailbox'], - objectcategory: 'mailbox', - cn: `${mailbox.name}@${domain}`, - uid: `${mailbox.name}@${domain}`, - mail: `${mailbox.name}@${domain}` - } - }; - - // ensure all filter values are also lowercase - var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null); - if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString())); - if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) { - results.push(obj); - } - }); - - finalSend(results, req, res, next); - }); - } else { // new sogo - mailboxdb.listAllMailboxes(1, 1000, async function (error, mailboxes) { - if (error) return next(new ldap.OperationsError(error.toString())); - - mailboxes = mailboxes.filter(m => m.active); - - let results = []; - - for (const mailbox of mailboxes) { - const dn = ldap.parseDN(`cn=${mailbox.name}@${mailbox.domain},ou=mailboxes,dc=cloudron`); - - const [error, ownerObject] = await safe(mailbox.ownerType === mail.OWNERTYPE_USER ? users.get(mailbox.ownerId) : groups.get(mailbox.ownerId)); - if (error || !ownerObject) continue; // skip mailboxes with unknown user - - const obj = { - dn: dn.toString(), - attributes: { - objectclass: ['mailbox'], - objectcategory: 'mailbox', - displayname: mailbox.ownerType === mail.OWNERTYPE_USER ? ownerObject.displayName : ownerObject.name, - cn: `${mailbox.name}@${mailbox.domain}`, - uid: `${mailbox.name}@${mailbox.domain}`, - mail: `${mailbox.name}@${mailbox.domain}` - } - }; - - mailbox.aliases.forEach(function (a, idx) { - obj.attributes['mail' + idx] = `${a.name}@${a.domain}`; - }); - - // ensure all filter values are also lowercase - const lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null); - if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString())); - - if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) { - results.push(obj); - } - } - - finalSend(results, req, res, next); - }); - } -} - -function mailAliasSearch(req, res, next) { - debug('mail alias get: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id); - - if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString())); - - var email = req.dn.rdns[0].attrs.cn.value.toLowerCase(); - var parts = email.split('@'); - if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString())); - - mailboxdb.getAlias(parts[0], parts[1], function (error, alias) { - if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); + const [error, mailbox] = await safe(mail.getMailbox(parts[0], parts[1])); if (error) return next(new ldap.OperationsError(error.toString())); + if (!mailbox) return next(new ldap.NoSuchObjectError(req.dn.toString())); + if (!mailbox.active) return next(new ldap.NoSuchObjectError(req.dn.toString())); - if (!alias.active) return next(new ldap.NoSuchObjectError(req.dn.toString())); // there is no way to disable an alias. this is just here for completeness - - // https://wiki.debian.org/LDAP/MigrationTools/Examples - // https://docs.oracle.com/cd/E19455-01/806-5580/6jej518pp/index.html - // member is fully qualified - https://docs.oracle.com/cd/E19957-01/816-6082-10/chap4.doc.html#43314 - let obj = { + const obj = { dn: req.dn.toString(), attributes: { - objectclass: ['nisMailAlias'], - objectcategory: 'nisMailAlias', - cn: `${alias.name}@${alias.domain}`, - rfc822MailMember: `${alias.aliasName}@${alias.aliasDomain}` + objectclass: ['mailbox'], + objectcategory: 'mailbox', + cn: `${mailbox.name}@${mailbox.domain}`, + uid: `${mailbox.name}@${mailbox.domain}`, + mail: `${mailbox.name}@${mailbox.domain}` } }; // ensure all filter values are also lowercase - var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null); + const lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null); if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString())); if (lowerCaseFilter.matches(obj.attributes)) { @@ -413,10 +294,123 @@ function mailAliasSearch(req, res, next) { } else { res.end(); } - }); + } else if (req.dn.rdns[0].attrs.domain) { // legacy ldap mailbox search for old sogo + const domain = req.dn.rdns[0].attrs.domain.value.toLowerCase(); + + let [error, mailboxes] = await safe(mail.listMailboxes(domain, 1, 1000)); + if (error) return next(new ldap.OperationsError(error.toString())); + + mailboxes = mailboxes.filter(m => m.active); + + let results = []; + + // send mailbox objects + mailboxes.forEach(function (mailbox) { + var dn = ldap.parseDN(`cn=${mailbox.name}@${domain},domain=${domain},ou=mailboxes,dc=cloudron`); + + var obj = { + dn: dn.toString(), + attributes: { + objectclass: ['mailbox'], + objectcategory: 'mailbox', + cn: `${mailbox.name}@${domain}`, + uid: `${mailbox.name}@${domain}`, + mail: `${mailbox.name}@${domain}` + } + }; + + // ensure all filter values are also lowercase + var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null); + if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString())); + if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) { + results.push(obj); + } + }); + + finalSend(results, req, res, next); + } else { // new sogo + let [error, mailboxes] = await safe(mail.listAllMailboxes(1, 1000)); + if (error) return next(new ldap.OperationsError(error.toString())); + + mailboxes = mailboxes.filter(m => m.active); + + let results = []; + + for (const mailbox of mailboxes) { + const dn = ldap.parseDN(`cn=${mailbox.name}@${mailbox.domain},ou=mailboxes,dc=cloudron`); + + const [error, ownerObject] = await safe(mailbox.ownerType === mail.OWNERTYPE_USER ? users.get(mailbox.ownerId) : groups.get(mailbox.ownerId)); + if (error || !ownerObject) continue; // skip mailboxes with unknown user + + const obj = { + dn: dn.toString(), + attributes: { + objectclass: ['mailbox'], + objectcategory: 'mailbox', + displayname: mailbox.ownerType === mail.OWNERTYPE_USER ? ownerObject.displayName : ownerObject.name, + cn: `${mailbox.name}@${mailbox.domain}`, + uid: `${mailbox.name}@${mailbox.domain}`, + mail: `${mailbox.name}@${mailbox.domain}` + } + }; + + mailbox.aliases.forEach(function (a, idx) { + obj.attributes['mail' + idx] = `${a.name}@${a.domain}`; + }); + + // ensure all filter values are also lowercase + const lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null); + if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString())); + + if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) { + results.push(obj); + } + } + + finalSend(results, req, res, next); + } } -function mailingListSearch(req, res, next) { +async function mailAliasSearch(req, res, next) { + debug('mail alias get: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id); + + if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString())); + + const email = req.dn.rdns[0].attrs.cn.value.toLowerCase(); + const parts = email.split('@'); + if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString())); + + const [error, alias] = await safe(mail.getAlias(parts[0], parts[1])); + if (error) return next(new ldap.OperationsError(error.toString())); + if (!alias) return next(new ldap.NoSuchObjectError(req.dn.toString())); + + if (!alias.active) return next(new ldap.NoSuchObjectError(req.dn.toString())); // there is no way to disable an alias. this is just here for completeness + + // https://wiki.debian.org/LDAP/MigrationTools/Examples + // https://docs.oracle.com/cd/E19455-01/806-5580/6jej518pp/index.html + // member is fully qualified - https://docs.oracle.com/cd/E19957-01/816-6082-10/chap4.doc.html#43314 + const obj = { + dn: req.dn.toString(), + attributes: { + objectclass: ['nisMailAlias'], + objectcategory: 'nisMailAlias', + cn: `${alias.name}@${alias.domain}`, + rfc822MailMember: `${alias.aliasName}@${alias.aliasDomain}` + } + }; + + // ensure all filter values are also lowercase + const lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null); + if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString())); + + if (lowerCaseFilter.matches(obj.attributes)) { + finalSend([ obj ], req, res, next); + } else { + res.end(); + } +} + +async function mailingListSearch(req, res, next) { debug('mailing list get: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id); if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString())); @@ -426,36 +420,37 @@ function mailingListSearch(req, res, next) { if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString())); const name = parts[0], domain = parts[1]; - mail.resolveList(parts[0], parts[1], function (error, resolvedMembers, list) { - if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); - if (error) return next(new ldap.OperationsError(error.toString())); + const [error, result] = await safe(mail.resolveList(parts[0], parts[1])); + if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); + if (error) return next(new ldap.OperationsError(error.toString())); - if (!list.active) return next(new ldap.NoSuchObjectError(req.dn.toString())); + const { resolvedMembers, list } = result; - // http://ldapwiki.willeke.com/wiki/Original%20Mailgroup%20Schema%20From%20Netscape - // members are fully qualified (https://docs.oracle.com/cd/E19444-01/816-6018-10/groups.htm#13356) - var obj = { - dn: req.dn.toString(), - attributes: { - objectclass: ['mailGroup'], - objectcategory: 'mailGroup', - cn: `${name}@${domain}`, // fully qualified - mail: `${name}@${domain}`, - membersOnly: list.membersOnly, // ldapjs only supports strings and string array. so this is not a bool! - mgrpRFC822MailMember: resolvedMembers // fully qualified - } - }; + if (!list.active) return next(new ldap.NoSuchObjectError(req.dn.toString())); - // ensure all filter values are also lowercase - var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null); - if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString())); - - if (lowerCaseFilter.matches(obj.attributes)) { - finalSend([ obj ], req, res, next); - } else { - res.end(); + // http://ldapwiki.willeke.com/wiki/Original%20Mailgroup%20Schema%20From%20Netscape + // members are fully qualified (https://docs.oracle.com/cd/E19444-01/816-6018-10/groups.htm#13356) + const obj = { + dn: req.dn.toString(), + attributes: { + objectclass: ['mailGroup'], + objectcategory: 'mailGroup', + cn: `${name}@${domain}`, // fully qualified + mail: `${name}@${domain}`, + membersOnly: list.membersOnly, // ldapjs only supports strings and string array. so this is not a bool! + mgrpRFC822MailMember: resolvedMembers // fully qualified } - }); + }; + + // ensure all filter values are also lowercase + const lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null); + if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString())); + + if (lowerCaseFilter.matches(obj.attributes)) { + finalSend([ obj ], req, res, next); + } else { + res.end(); + } } // Will attach req.user if successful @@ -524,39 +519,34 @@ async function verifyMailboxPassword(mailbox, password) { return verifiedUser; } -function authenticateUserMailbox(req, res, next) { +async function authenticateUserMailbox(req, res, next) { debug('user mailbox auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id); if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString())); - var email = req.dn.rdns[0].attrs.cn.value.toLowerCase(); - var parts = email.split('@'); + const email = req.dn.rdns[0].attrs.cn.value.toLowerCase(); + const parts = email.split('@'); if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString())); - const getDomainFunc = util.callbackify(mail.getDomain); + const [error, domain] = await safe(mail.getDomain(parts[1])); + if (error) return next(new ldap.OperationsError(error.message)); + if (!domain) return next(new ldap.NoSuchObjectError(req.dn.toString())); - getDomainFunc(parts[1], function (error, domain) { - if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); - if (error) return next(new ldap.OperationsError(error.message)); + if (!domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString())); - if (!domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString())); + const [getMailboxError, mailbox] = await safe(mail.getMailbox(parts[0], parts[1])); + if (getMailboxError) return next(new ldap.OperationsError(getMailboxError.message)); + if (!mailbox) return next(new ldap.NoSuchObjectError(req.dn.toString())); + if (!mailbox.active) return next(new ldap.NoSuchObjectError(req.dn.toString())); - mailboxdb.getMailbox(parts[0], parts[1], async function (error, mailbox) { - if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); - if (error) return next(new ldap.OperationsError(error.message)); + const [verifyError, result] = await safe(verifyMailboxPassword(mailbox, req.credentials || '')); + if (verifyError && verifyError.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); + if (verifyError && verifyError.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString())); + if (verifyError) return next(new ldap.OperationsError(verifyError.message)); - if (!mailbox.active) return next(new ldap.NoSuchObjectError(req.dn.toString())); + eventlog.upsertLoginEvent(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) }); - const [verifyError, result] = await safe(verifyMailboxPassword(mailbox, req.credentials || '')); - if (verifyError && verifyError.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); - if (verifyError && verifyError.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString())); - if (verifyError) return next(new ldap.OperationsError(verifyError.message)); - - eventlog.upsertLoginEvent(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) }); - - res.end(); - }); - }); + res.end(); } function authenticateSftp(req, res, next) { @@ -658,7 +648,7 @@ function verifyAppMailboxPassword(addonId, username, password, callback) { }); } -function authenticateMailAddon(req, res, next) { +async function authenticateMailAddon(req, res, next) { debug('mail addon auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id); if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString())); @@ -670,37 +660,31 @@ function authenticateMailAddon(req, res, next) { const addonId = req.dn.rdns[1].attrs.ou.value.toLowerCase(); // 'sendmail' or 'recvmail' if (addonId !== 'sendmail' && addonId !== 'recvmail') return next(new ldap.OperationsError('Invalid DN')); - const getDomainFunc = util.callbackify(mail.getDomain); + const [error, domain] = await safe(mail.getDomain(parts[1])); + if (error) return next(new ldap.OperationsError(error.message)); + if (!domain) return next(new ldap.NoSuchObjectError(req.dn.toString())); - getDomainFunc(parts[1], function (error, domain) { - if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); - if (error) return next(new ldap.OperationsError(error.message)); + if (addonId === 'recvmail' && !domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString())); - if (addonId === 'recvmail' && !domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString())); + const [appPasswordError] = await safe(util.promisify(verifyAppMailboxPassword)(addonId, email, req.credentials || '')); + if (!appPasswordError) return res.end(); // validated as app - verifyAppMailboxPassword(addonId, email, req.credentials || '', function (error) { - if (!error) return res.end(); // validated as app + if (appPasswordError && appPasswordError.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString())); + if (appPasswordError && appPasswordError.reason !== BoxError.NOT_FOUND) return next(new ldap.OperationsError(appPasswordError.message)); - if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString())); - if (error && error.reason !== BoxError.NOT_FOUND) return next(new ldap.OperationsError(error.message)); + const [getMailboxError, mailbox] = await safe(mail.getMailbox(parts[0], parts[1])); + if (getMailboxError) return next(new ldap.OperationsError(getMailboxError.message)); + if (!mailbox) return next(new ldap.NoSuchObjectError(req.dn.toString())); + if (!mailbox.active) return next(new ldap.NoSuchObjectError(req.dn.toString())); - mailboxdb.getMailbox(parts[0], parts[1], async function (error, mailbox) { - if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); - if (error) return next(new ldap.OperationsError(error.message)); + const [verifyError, result] = await safe(verifyMailboxPassword(mailbox, req.credentials || '')); + if (verifyError && verifyError.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); + if (verifyError && verifyError.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString())); + if (verifyError) return next(new ldap.OperationsError(verifyError.message)); - if (!mailbox.active) return next(new ldap.NoSuchObjectError(req.dn.toString())); + eventlog.upsertLoginEvent(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) }); - const [verifyError, result] = await safe(verifyMailboxPassword(mailbox, req.credentials || '')); - if (verifyError && verifyError.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); - if (verifyError && verifyError.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString())); - if (verifyError) return next(new ldap.OperationsError(verifyError.message)); - - eventlog.upsertLoginEvent(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) }); - - res.end(); - }); - }); - }); + res.end(); } function start(callback) { diff --git a/src/mail.js b/src/mail.js index dcbdb1d51..11e39296f 100644 --- a/src/mail.js +++ b/src/mail.js @@ -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 }; } diff --git a/src/mailboxdb.js b/src/mailboxdb.js deleted file mode 100644 index 4b2540c30..000000000 --- a/src/mailboxdb.js +++ /dev/null @@ -1,408 +0,0 @@ -'use strict'; - -exports = module.exports = { - addMailbox, - addList, - - updateMailbox, - updateList, - del, - - getMailboxCount, - listMailboxes, - getLists, - - listAllMailboxes, - - get, - getMailbox, - getList, - getAlias, - - getAliasesForName, - setAliasesForName, - - getByOwnerId, - delByOwnerId, - delByDomain, - - updateName, - - _clear: clear, - - TYPE_MAILBOX: 'mailbox', - TYPE_LIST: 'list', - TYPE_ALIAS: 'alias' -}; - -var assert = require('assert'), - BoxError = require('./boxerror.js'), - database = require('./database.js'), - mysql = require('mysql'), - safe = require('safetydance'), - util = require('util'); - -var MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'ownerType', 'aliasName', 'aliasDomain', 'creationTime', 'membersJson', 'membersOnly', 'domain', 'active' ].join(','); - -function postProcess(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] }; - } -} - -function addMailbox(name, domain, data, callback) { - assert.strictEqual(typeof name, 'string'); - assert.strictEqual(typeof domain, 'string'); - assert.strictEqual(typeof data, 'object'); - assert.strictEqual(typeof callback, 'function'); - - const { ownerId, ownerType, active } = data; - assert.strictEqual(typeof ownerId, 'string'); - assert.strictEqual(typeof ownerType, 'string'); - assert.strictEqual(typeof active, 'boolean'); - - database.query('INSERT INTO mailboxes (name, type, domain, ownerId, ownerType, active) VALUES (?, ?, ?, ?, ?, ?)', [ name, exports.TYPE_MAILBOX, domain, ownerId, ownerType, active ], function (error) { - if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, 'mailbox already exists')); - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null); - }); -} - -function updateMailbox(name, domain, data, callback) { - assert.strictEqual(typeof name, 'string'); - assert.strictEqual(typeof domain, 'string'); - assert.strictEqual(typeof data, 'object'); - assert.strictEqual(typeof callback, 'function'); - - const { ownerId, ownerType, active } = data; - assert.strictEqual(typeof ownerId, 'string'); - assert.strictEqual(typeof ownerType, 'string'); - assert.strictEqual(typeof active, 'boolean'); - - database.query('UPDATE mailboxes SET ownerId = ?, ownerType = ?, active = ? WHERE name = ? AND domain = ?', [ ownerId, ownerType, active, name, domain ], function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found')); - - callback(null); - }); -} - -function addList(name, domain, data, callback) { - assert.strictEqual(typeof name, 'string'); - assert.strictEqual(typeof domain, 'string'); - assert.strictEqual(typeof data, 'object'); - assert.strictEqual(typeof callback, 'function'); - - const { members, membersOnly, active } = data; - assert(Array.isArray(members)); - assert.strictEqual(typeof membersOnly, 'boolean'); - assert.strictEqual(typeof active, 'boolean'); - - 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 ], function (error) { - if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, 'mailbox already exists')); - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null); - }); -} - -function updateList(name, domain, data, callback) { - assert.strictEqual(typeof name, 'string'); - assert.strictEqual(typeof domain, 'string'); - assert.strictEqual(typeof data, 'object'); - assert.strictEqual(typeof callback, 'function'); - - const { members, membersOnly, active } = data; - assert(Array.isArray(members)); - assert.strictEqual(typeof membersOnly, 'boolean'); - assert.strictEqual(typeof active, 'boolean'); - - database.query('UPDATE mailboxes SET membersJson = ?, membersOnly = ?, active = ? WHERE name = ? AND domain = ?', - [ JSON.stringify(members), membersOnly, active, name, domain ], function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found')); - - callback(null); - }); -} - -function clear(callback) { - assert.strictEqual(typeof callback, 'function'); - - database.query('TRUNCATE TABLE mailboxes', [], function (error) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - callback(null); - }); -} - -function del(name, domain, callback) { - assert.strictEqual(typeof name, 'string'); - assert.strictEqual(typeof domain, 'string'); - assert.strictEqual(typeof callback, 'function'); - - // deletes aliases as well - database.query('DELETE FROM mailboxes WHERE ((name=? AND domain=?) OR (aliasName = ? AND aliasDomain=?))', [ name, domain, name, domain ], function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found')); - - callback(null); - }); -} - -function delByDomain(domain, callback) { - assert.strictEqual(typeof domain, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('DELETE FROM mailboxes WHERE domain = ?', [ domain ], function (error) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null); - }); -} - -function delByOwnerId(id, callback) { - assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('DELETE FROM mailboxes WHERE ownerId=?', [ id ], function (error) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null); - }); -} - -function updateName(oldName, oldDomain, newName, newDomain, callback) { - assert.strictEqual(typeof oldName, 'string'); - assert.strictEqual(typeof oldDomain, 'string'); - assert.strictEqual(typeof newName, 'string'); - assert.strictEqual(typeof newDomain, 'string'); - assert.strictEqual(typeof callback, 'function'); - - // skip if no changes - if (oldName === newName && oldDomain === newDomain) return callback(null); - - database.query('UPDATE mailboxes SET name=?, domain=? WHERE name=? AND domain = ?', [ newName, newDomain, oldName, oldDomain ], function (error, result) { - if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, 'mailbox already exists')); - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found')); - - callback(null); - }); -} - -function get(name, domain, callback) { - assert.strictEqual(typeof name, 'string'); - assert.strictEqual(typeof domain, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND domain = ?', - [ name, domain ], function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found')); - - callback(null, postProcess(results[0])); - }); -} - -function getMailbox(name, domain, callback) { - assert.strictEqual(typeof name, 'string'); - assert.strictEqual(typeof domain, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND type = ? AND domain = ?', - [ name, exports.TYPE_MAILBOX, domain ], function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found')); - - callback(null, postProcess(results[0])); - }); -} - -function getMailboxCount(domain, callback) { - assert.strictEqual(typeof domain, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT COUNT(*) AS total FROM mailboxes WHERE type = ? AND domain = ?', [ exports.TYPE_MAILBOX, domain ], function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null, results[0].total); - }); -} - -function listMailboxes(domain, search, page, perPage, callback) { - 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'); - - 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 - - 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 ?,?'; - - database.query(query, [ domain, (page-1)*perPage, perPage ], function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - results.forEach(postProcess); - results.forEach(postProcessAliases); - - callback(null, results); - }); -} - -function listAllMailboxes(page, perPage, callback) { - assert.strictEqual(typeof page, 'number'); - assert.strictEqual(typeof perPage, 'number'); - assert.strictEqual(typeof callback, 'function'); - - 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 ?,?'; - - database.query(query, [ (page-1)*perPage, perPage ], function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - results.forEach(postProcess); - results.forEach(postProcessAliases); - - callback(null, results); - }); -} - -function getLists(domain, search, page, perPage, callback) { - 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'); - - 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 + '%') + ')'; - - query += 'ORDER BY name LIMIT ?,?'; - - database.query(query, [ exports.TYPE_LIST, domain, (page-1)*perPage, perPage ], function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - results.forEach(function (result) { postProcess(result); }); - - callback(null, results); - }); -} - -function getList(name, domain, callback) { - assert.strictEqual(typeof name, 'string'); - assert.strictEqual(typeof domain, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE type = ? AND name = ? AND domain = ?', - [ exports.TYPE_LIST, name, domain ], function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found')); - - callback(null, postProcess(results[0])); - }); -} - -function getByOwnerId(ownerId, callback) { - assert.strictEqual(typeof ownerId, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE ownerId = ? ORDER BY name', [ ownerId ], function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found')); - - results.forEach(function (result) { postProcess(result); }); - - callback(null, results); - }); -} - -function setAliasesForName(name, domain, aliases, callback) { - assert.strictEqual(typeof name, 'string'); - assert.strictEqual(typeof domain, 'string'); - assert(Array.isArray(aliases)); - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND domain = ?', [ name, domain ], function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found')); - - var 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 ] }); - }); - - database.transaction(queries, function (error) { - if (error && error.code === 'ER_DUP_ENTRY' && error.message.indexOf('mailboxes_name_domain_unique_index') !== -1) { - var aliasMatch = error.message.match(new RegExp(`^ER_DUP_ENTRY: Duplicate entry '(.*)-${domain}' for key 'mailboxes_name_domain_unique_index'$`)); - if (!aliasMatch) return callback(new BoxError(BoxError.ALREADY_EXISTS, error)); - - return callback(new BoxError(BoxError.ALREADY_EXISTS, `Mailbox, mailinglist or alias for ${aliasMatch[1]} already exists`)); - } - - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null); - }); - }); -} - -function getAliasesForName(name, domain, callback) { - assert.strictEqual(typeof name, 'string'); - assert.strictEqual(typeof domain, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT name, domain FROM mailboxes WHERE type = ? AND aliasName = ? AND aliasDomain = ? ORDER BY name', - [ exports.TYPE_ALIAS, name, domain ], function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null, results); - }); -} - -function getAlias(name, domain, callback) { - assert.strictEqual(typeof name, 'string'); - assert.strictEqual(typeof domain, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND type = ? AND domain = ?', - [ name, exports.TYPE_ALIAS, domain ], function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found')); - - results.forEach(function (result) { postProcess(result); }); - - callback(null, results[0]); - }); -} diff --git a/src/routes/mail.js b/src/routes/mail.js index 49143a5d1..20495a8e3 100644 --- a/src/routes/mail.js +++ b/src/routes/mail.js @@ -17,7 +17,7 @@ exports = module.exports = { getMailbox, addMailbox, updateMailbox, - removeMailbox, + delMailbox, getAliases, setAliases, @@ -26,7 +26,7 @@ exports = module.exports = { getList, addList, updateList, - removeList, + delList, getMailboxCount }; @@ -133,46 +133,44 @@ function sendTestMail(req, res, next) { }); } -function listMailboxes(req, res, next) { +async function listMailboxes(req, res, next) { assert.strictEqual(typeof req.params.domain, 'string'); - var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1; + const page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1; if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a positive number')); - var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25; + const perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25; if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a positive number')); if (req.query.search && typeof req.query.search !== 'string') return next(new HttpError(400, 'search must be a string')); - mail.listMailboxes(req.params.domain, req.query.search || null, page, perPage, function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, mailboxes] = await safe(mail.listMailboxes(req.params.domain, req.query.search || null, page, perPage)); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, { mailboxes: result })); - }); + next(new HttpSuccess(200, { mailboxes })); } -function getMailboxCount(req, res, next) { +async function getMailboxCount(req, res, next) { assert.strictEqual(typeof req.params.domain, 'string'); - mail.getMailboxCount(req.params.domain, function (error, count) { - if (error) return next(BoxError.toHttpError(error)); + const [error, count] = await safe(mail.getMailboxCount(req.params.domain)); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, { count })); - }); + next(new HttpSuccess(200, { count })); } -function getMailbox(req, res, next) { +async function getMailbox(req, res, next) { assert.strictEqual(typeof req.params.domain, 'string'); assert.strictEqual(typeof req.params.name, 'string'); - mail.getMailbox(req.params.name, req.params.domain, function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, result] = await safe(mail.getMailbox(req.params.name, req.params.domain)); + if (error) return next(BoxError.toHttpError(error)); + if (!result) return next(new HttpError(404, 'Mailbox not found')); - next(new HttpSuccess(200, { mailbox: result })); - }); + next(new HttpSuccess(200, { mailbox: result })); } -function addMailbox(req, res, next) { +async function addMailbox(req, res, next) { assert.strictEqual(typeof req.params.domain, 'string'); if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be a string')); @@ -180,14 +178,13 @@ function addMailbox(req, res, next) { if (typeof req.body.ownerType !== 'string') return next(new HttpError(400, 'ownerType must be a string')); if (typeof req.body.active !== 'boolean') return next(new HttpError(400, 'active must be a boolean')); - mail.addMailbox(req.body.name, req.params.domain, req.body, auditSource.fromRequest(req), function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(mail.addMailbox(req.body.name, req.params.domain, req.body, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(201, {})); - }); + next(new HttpSuccess(201, {})); } -function updateMailbox(req, res, next) { +async function updateMailbox(req, res, next) { assert.strictEqual(typeof req.params.domain, 'string'); assert.strictEqual(typeof req.params.name, 'string'); @@ -195,38 +192,35 @@ function updateMailbox(req, res, next) { if (typeof req.body.ownerType !== 'string') return next(new HttpError(400, 'ownerType must be a string')); if (typeof req.body.active !== 'boolean') return next(new HttpError(400, 'active must be a boolean')); - mail.updateMailbox(req.params.name, req.params.domain, req.body, auditSource.fromRequest(req), function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(mail.updateMailbox(req.params.name, req.params.domain, req.body, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(204)); - }); + next(new HttpSuccess(204)); } -function removeMailbox(req, res, next) { +async function delMailbox(req, res, next) { assert.strictEqual(typeof req.params.domain, 'string'); assert.strictEqual(typeof req.params.name, 'string'); if (typeof req.body.deleteMails !== 'boolean') return next(new HttpError(400, 'deleteMails must be a boolean')); - mail.removeMailbox(req.params.name, req.params.domain, req.body, auditSource.fromRequest(req), function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(mail.delMailbox(req.params.name, req.params.domain, req.body, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(201, {})); - }); + next(new HttpSuccess(201, {})); } -function getAliases(req, res, next) { +async function getAliases(req, res, next) { assert.strictEqual(typeof req.params.domain, 'string'); assert.strictEqual(typeof req.params.name, 'string'); - mail.getAliases(req.params.name, req.params.domain, function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, aliases] = await safe(mail.getAliases(req.params.name, req.params.domain)); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, { aliases: result })); - }); + next(new HttpSuccess(200, { aliases })); } -function setAliases(req, res, next) { +async function setAliases(req, res, next) { assert.strictEqual(typeof req.params.domain, 'string'); assert.strictEqual(typeof req.params.name, 'string'); assert.strictEqual(typeof req.body, 'object'); @@ -239,11 +233,10 @@ function setAliases(req, res, next) { if (typeof alias.domain !== 'string') return next(new HttpError(400, 'domain must be a string')); } - mail.setAliases(req.params.name, req.params.domain, req.body.aliases, function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(mail.setAliases(req.params.name, req.params.domain, req.body.aliases)); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(202)); - }); + next(new HttpSuccess(202)); } async function setBanner(req, res, next) { @@ -259,7 +252,7 @@ async function setBanner(req, res, next) { next(new HttpSuccess(202)); } -function getLists(req, res, next) { +async function getLists(req, res, next) { assert.strictEqual(typeof req.params.domain, 'string'); const page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1; @@ -270,25 +263,24 @@ function getLists(req, res, next) { if (req.query.search && typeof req.query.search !== 'string') return next(new HttpError(400, 'search must be a string')); - mail.getLists(req.params.domain, req.query.search || null, page, perPage, function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, lists] = await safe(mail.getLists(req.params.domain, req.query.search || null, page, perPage)); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, { lists: result })); - }); + next(new HttpSuccess(200, { lists })); } -function getList(req, res, next) { +async function getList(req, res, next) { assert.strictEqual(typeof req.params.domain, 'string'); assert.strictEqual(typeof req.params.name, 'string'); - mail.getList(req.params.name, req.params.domain, function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, result] = await safe(mail.getList(req.params.name, req.params.domain)); + if (error) return next(BoxError.toHttpError(error)); + if (!result) return next(new HttpError(404, 'List not found')); - next(new HttpSuccess(200, { list: result })); - }); + next(new HttpSuccess(200, { list: result })); } -function addList(req, res, next) { +async function addList(req, res, next) { assert.strictEqual(typeof req.params.domain, 'string'); assert.strictEqual(typeof req.body, 'object'); @@ -296,20 +288,19 @@ function addList(req, res, next) { if (!Array.isArray(req.body.members)) return next(new HttpError(400, 'members must be a string')); if (req.body.members.length === 0) return next(new HttpError(400, 'list must have atleast one member')); - for (var i = 0; i < req.body.members.length; i++) { + for (let i = 0; i < req.body.members.length; i++) { if (typeof req.body.members[i] !== 'string') return next(new HttpError(400, 'member must be a string')); } if (typeof req.body.membersOnly !== 'boolean') return next(new HttpError(400, 'membersOnly must be a boolean')); if (typeof req.body.active !== 'boolean') return next(new HttpError(400, 'active must be a boolean')); - mail.addList(req.body.name, req.params.domain, req.body, auditSource.fromRequest(req), function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(mail.addList(req.body.name, req.params.domain, req.body, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(201, {})); - }); + next(new HttpSuccess(201, {})); } -function updateList(req, res, next) { +async function updateList(req, res, next) { assert.strictEqual(typeof req.params.domain, 'string'); assert.strictEqual(typeof req.params.name, 'string'); @@ -322,20 +313,18 @@ function updateList(req, res, next) { if (typeof req.body.membersOnly !== 'boolean') return next(new HttpError(400, 'membersOnly must be a boolean')); if (typeof req.body.active !== 'boolean') return next(new HttpError(400, 'active must be a boolean')); - mail.updateList(req.params.name, req.params.domain, req.body, auditSource.fromRequest(req), function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(mail.updateList(req.params.name, req.params.domain, req.body, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(204)); - }); + next(new HttpSuccess(204)); } -function removeList(req, res, next) { +async function delList(req, res, next) { assert.strictEqual(typeof req.params.domain, 'string'); assert.strictEqual(typeof req.params.name, 'string'); - mail.removeList(req.params.name, req.params.domain, auditSource.fromRequest(req), function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(mail.delList(req.params.name, req.params.domain, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(204)); - }); + next(new HttpSuccess(204)); } diff --git a/src/routes/mailserver.js b/src/routes/mailserver.js index 1d9f8c888..d8c7c62bb 100644 --- a/src/routes/mailserver.js +++ b/src/routes/mailserver.js @@ -4,8 +4,8 @@ exports = module.exports = { proxy, restart, - getLocation, - setLocation + setLocation, + getLocation }; const assert = require('assert'), @@ -16,6 +16,7 @@ const assert = require('assert'), HttpSuccess = require('connect-lastmile').HttpSuccess, mail = require('../mail.js'), middleware = require('../middleware/index.js'), + safe = require('safetydance'), services = require('../services.js'), url = require('url'); @@ -55,23 +56,21 @@ function proxy(req, res, next) { }); } -function getLocation(req, res, next) { - mail.getLocation(function (error, result) { - if (error) return next(BoxError.toHttpError(error)); +async function getLocation(req, res, next) { + const [error, result] = await safe(mail.getLocation()); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, { domain: result.domain, subdomain: result.subdomain })); - }); + next(new HttpSuccess(200, { domain: result.domain, subdomain: result.subdomain })); } -function setLocation(req, res, next) { +async function setLocation(req, res, next) { assert.strictEqual(typeof req.body, 'object'); if (typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string')); if (typeof req.body.subdomain !== 'string') return next(new HttpError(400, 'subdomain must be a string')); - mail.setLocation(req.body.subdomain, req.body.domain, auditSource.fromRequest(req), function (error, taskId) { - if (error) return next(BoxError.toHttpError(error)); + const [error, taskId] = await safe(mail.setLocation(req.body.subdomain, req.body.domain, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(202, { taskId })); - }); + next(new HttpSuccess(202, { taskId })); } \ No newline at end of file diff --git a/src/routes/test/mail-test.js b/src/routes/test/mail-test.js index b53be74ed..3b286ff59 100644 --- a/src/routes/test/mail-test.js +++ b/src/routes/test/mail-test.js @@ -496,11 +496,8 @@ describe('Mail API', function () { describe('aliases', function () { const MAILBOX_NAME = 'support'; - after(function (done) { - mail._removeMailboxes(dashboardDomain, function (error) { - if (error) return done(error); - done(); - }); + after(async function () { + await mail._delByDomain(dashboardDomain); }); it('add the mailbox', async function () { @@ -565,12 +562,8 @@ describe('Mail API', function () { describe('mailinglists', function () { const LIST_NAME = 'people'; - after(function (done) { - mail._removeMailboxes(dashboardDomain, function (error) { - if (error) return done(error); - - done(); - }); + after(async function () { + await mail._delByDomain(dashboardDomain); }); it('add fails without groupId', async function () { diff --git a/src/server.js b/src/server.js index 00747b689..4dc07ef2c 100644 --- a/src/server.js +++ b/src/server.js @@ -281,14 +281,14 @@ function initializeExpressSync() { router.get ('/api/v1/mail/:domain/mailboxes/:name', token, authorizeAdmin, routes.mail.getMailbox); router.post('/api/v1/mail/:domain/mailboxes', json, token, authorizeAdmin, routes.mail.addMailbox); router.post('/api/v1/mail/:domain/mailboxes/:name', json, token, authorizeAdmin, routes.mail.updateMailbox); - router.del ('/api/v1/mail/:domain/mailboxes/:name', json, token, authorizeAdmin, routes.mail.removeMailbox); + router.del ('/api/v1/mail/:domain/mailboxes/:name', json, token, authorizeAdmin, routes.mail.delMailbox); router.get ('/api/v1/mail/:domain/mailboxes/:name/aliases', token, authorizeAdmin, routes.mail.getAliases); router.put ('/api/v1/mail/:domain/mailboxes/:name/aliases', json, token, authorizeAdmin, routes.mail.setAliases); router.get ('/api/v1/mail/:domain/lists', token, authorizeAdmin, routes.mail.getLists); router.post('/api/v1/mail/:domain/lists', json, token, authorizeAdmin, routes.mail.addList); router.get ('/api/v1/mail/:domain/lists/:name', token, authorizeAdmin, routes.mail.getList); router.post('/api/v1/mail/:domain/lists/:name', json, token, authorizeAdmin, routes.mail.updateList); - router.del ('/api/v1/mail/:domain/lists/:name', token, authorizeAdmin, routes.mail.removeList); + router.del ('/api/v1/mail/:domain/lists/:name', token, authorizeAdmin, routes.mail.delList); // support routes router.post('/api/v1/support/ticket', json, token, authorizeAdmin, routes.support.canCreateTicket, routes.support.createTicket); diff --git a/src/test/common.js b/src/test/common.js index 79141b842..db9587c40 100644 --- a/src/test/common.js +++ b/src/test/common.js @@ -10,8 +10,6 @@ const appdb = require('../appdb.js'), domains = require('../domains.js'), expect = require('expect.js'), fs = require('fs'), - mail = require('../mail.js'), - mailboxdb = require('../mailboxdb.js'), mailer = require('../mailer.js'), nock = require('nock'), path = require('path'), @@ -133,11 +131,6 @@ exports = module.exports = { manifest, user, appstoreToken: 'atoken', - - mailboxName: 'support', - mailbox: `support@${domain.domain}`, - mailAliasName: 'alsosupport', - mailAlias: `alsosupport@${domain.domain}` }; function createTree(root, obj) { @@ -197,8 +190,6 @@ function setup(done) { const result = await users.add(user.email, user, auditSource); user.id = result; }, - (done) => mailboxdb.addMailbox(exports.mailboxName, domain.domain, { ownerId: user.id, ownerType: mail.OWNERTYPE_USER, active: true }, done), - (done) => mailboxdb.setAliasesForName(exports.mailboxName, domain.domain, [ { name: exports.mailAliasName, domain: domain.domain} ], done), tasks.stopAllTasks, ], done); diff --git a/src/test/database-test.js b/src/test/database-test.js index a8782121b..24f2b4438 100644 --- a/src/test/database-test.js +++ b/src/test/database-test.js @@ -12,7 +12,6 @@ const appdb = require('../appdb.js'), database = require('../database'), domains = require('../domains.js'), expect = require('expect.js'), - mailboxdb = require('../mailboxdb.js'), reverseProxy = require('../reverseproxy.js'), settingsdb = require('../settingsdb.js'), _ = require('underscore'); @@ -472,169 +471,4 @@ describe('database', function () { }); }); - - describe('mailboxes', function () { - before(function (done) { - async.series([ - domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0), - ], done); - }); - - after(function (done) { - database._clear(done); - }); - - it('add user mailbox succeeds', function (done) { - mailboxdb.addMailbox('girish', DOMAIN_0.domain, { ownerId: 'uid-0', ownerType: 'user', active: true }, function (error) { - expect(error).to.be(null); - done(); - }); - }); - - it('cannot add dup entry', function (done) { - mailboxdb.addMailbox('girish', DOMAIN_0.domain, { ownerId: 'uid-1', ownerType: 'group', active: true }, function (error) { - expect(error.reason).to.be(BoxError.ALREADY_EXISTS); - done(); - }); - }); - - it('add app mailbox succeeds', function (done) { - mailboxdb.addMailbox('support', DOMAIN_0.domain, { ownerId: 'osticket', ownerType: 'user', active: true}, function (error) { - expect(error).to.be(null); - done(); - }); - }); - - it('get succeeds', function (done) { - mailboxdb.getMailbox('support', DOMAIN_0.domain, function (error, mailbox) { - expect(error).to.be(null); - expect(mailbox.name).to.equal('support'); - expect(mailbox.ownerId).to.equal('osticket'); - expect(mailbox.domain).to.equal(DOMAIN_0.domain); - expect(mailbox.creationTime).to.be.a(Date); - - done(); - }); - }); - - it('list mailboxes succeeds', function (done) { - mailboxdb.listMailboxes(DOMAIN_0.domain, null /* search */, 1, 10, function (error, mailboxes) { - expect(error).to.be(null); - expect(mailboxes.length).to.be(2); - expect(mailboxes[0].name).to.be('girish'); - expect(mailboxes[1].name).to.be('support'); - - done(); - }); - }); - - it('can set alias', function (done) { - mailboxdb.setAliasesForName('support', DOMAIN_0.domain, [ { name: 'support2', domain: DOMAIN_0.domain }, { name: 'help', domain: DOMAIN_0.domain } ], function (error) { - expect(error).to.be(null); - done(); - }); - }); - - it('list all mailboxes succeeds', function (done) { - mailboxdb.listAllMailboxes(1, 10, function (error, mailboxes) { - expect(error).to.be(null); - expect(mailboxes.length).to.be(2); - expect(mailboxes[0].name).to.be('girish'); - expect(mailboxes[1].name).to.be('support'); - expect(mailboxes[1].domain).to.be(DOMAIN_0.domain); - - done(); - }); - }); - - - it('can get aliases of name', function (done) { - mailboxdb.getAliasesForName('support', DOMAIN_0.domain, function (error, results) { - expect(error).to.be(null); - expect(results.length).to.be(2); - expect(results[0].name).to.be('help'); - expect(results[0].domain).to.be(DOMAIN_0.domain); - expect(results[1].name).to.be('support2'); - expect(results[1].domain).to.be(DOMAIN_0.domain); - done(); - }); - }); - - it('can get alias', function (done) { - mailboxdb.getAlias('support2', DOMAIN_0.domain, function (error, result) { - expect(error).to.be(null); - expect(result.name).to.be('support2'); - expect(result.aliasName).to.be('support'); - expect(result.aliasDomain).to.be(DOMAIN_0.domain); - done(); - }); - }); - - it('can get by owner id', function (done) { - mailboxdb.getByOwnerId('osticket', function (error, results) { - expect(error).to.be(null); - expect(results.length).to.be(3); - expect(results[0].name).to.be('help'); - expect(results[1].name).to.be('support'); - expect(results[2].name).to.be('support2'); - done(); - }); - }); - - it('cannot get non-existing group', function (done) { - mailboxdb.getList('random', DOMAIN_0.domain, function (error) { - expect(error.reason).to.be(BoxError.NOT_FOUND); - done(); - }); - }); - - it('can change name', function (done) { - mailboxdb.updateName('support', DOMAIN_0.domain, 'support3', DOMAIN_0.domain, function (error) { - expect(error).to.be(null); - - mailboxdb.updateName('support3', DOMAIN_0.domain, 'support', DOMAIN_0.domain, done); - }); - }); - - it('cannot change name to existing one', function (done) { - mailboxdb.updateName('support', DOMAIN_0.domain, 'support2', DOMAIN_0.domain, function (error) { - expect(error).to.be.ok(); - expect(error.reason).to.eql(BoxError.ALREADY_EXISTS); - - done(); - }); - }); - - it('unset aliases', function (done) { - mailboxdb.setAliasesForName('support', DOMAIN_0.domain, [], function (error) { - expect(error).to.be(null); - - mailboxdb.getAliasesForName('support', DOMAIN_0.domain, function (error, results) { - expect(error).to.be(null); - expect(results.length).to.be(0); - done(); - }); - }); - }); - - it('del succeeds', function (done) { - mailboxdb.del('girish', DOMAIN_0.domain, function (error) { - expect(error).to.be(null); - done(); - }); - }); - - it('del by ownerId succeeds', function (done) { - mailboxdb.delByOwnerId('osticket', function (error) { - expect(error).to.be(null); - - mailboxdb.getByOwnerId('osticket', function (error) { - expect(error).to.be.ok(); - expect(error.reason).to.be(BoxError.NOT_FOUND); - done(); - }); - }); - }); - }); - }); diff --git a/src/test/ldap-test.js b/src/test/ldap-test.js index 5e117cc84..eb4b210a6 100644 --- a/src/test/ldap-test.js +++ b/src/test/ldap-test.js @@ -15,7 +15,6 @@ const appdb = require('../appdb.js'), ldap = require('ldapjs'), ldapServer = require('../ldap.js'), mail = require('../mail.js'), - mailboxdb = require('../mailboxdb.js'), safe = require('safetydance'), util = require('util'); @@ -61,12 +60,19 @@ async function ldapSearch(dn, opts) { } describe('Ldap', function () { - const { setup, cleanup, admin, user, app, domain, mailbox, mailAlias, mailboxName } = common; + const { setup, cleanup, admin, user, app, domain, auditSource } = common; let group; + const mailboxName = 'support'; + const mailbox = `support@${domain.domain}`; + const mailAliasName = 'alsosupport'; + const mailAlias = `alsosupport@${domain.domain}`; + before(function (done) { async.series([ setup, + async () => await mail.addMailbox(mailboxName, domain.domain, { ownerId: user.id, ownerType: mail.OWNERTYPE_USER, active: true }, auditSource), + async () => await mail.setAliases(mailboxName, domain.domain, [ { name: mailAliasName, domain: domain.domain} ], auditSource), ldapServer.start.bind(null), async () => { group = await groups.add({ name: 'ldap-test' }); @@ -285,13 +291,11 @@ describe('Ldap', function () { }); it('cannot get inactive mailbox', async function () { - const updateMailbox = util.promisify(mailboxdb.updateMailbox); - - await updateMailbox(mailboxName, domain.domain, { ownerId: user.id, ownerType: mail.OWNERTYPE_USER, active: false }); + await mail.updateMailbox(mailboxName, domain.domain, { ownerId: user.id, ownerType: mail.OWNERTYPE_USER, active: false }, auditSource); const [error] = await safe(ldapSearch(`cn=${mailbox},ou=mailboxes,dc=cloudron`, 'objectclass=mailbox')); expect(error).to.be.a(ldap.NoSuchObjectError); - await updateMailbox(mailboxName, domain.domain, { ownerId: user.id, ownerType: mail.OWNERTYPE_USER, active: true }); + await mail.updateMailbox(mailboxName, domain.domain, { ownerId: user.id, ownerType: mail.OWNERTYPE_USER, active: true }, auditSource); }); }); @@ -317,8 +321,8 @@ describe('Ldap', function () { describe('search mailing list', function () { const LIST_NAME = 'devs', LIST = `devs@${domain.domain}`; - before(function (done) { - mailboxdb.addList(LIST_NAME, domain.domain, { members: [ mailbox , 'outsider@external.com' ], membersOnly: false, active: true }, done); + before(async function () { + await mail.addList(LIST_NAME, domain.domain, { members: [ mailbox , 'outsider@external.com' ], membersOnly: false, active: true }, auditSource); }); it('get specific list', async function () { @@ -334,9 +338,7 @@ describe('Ldap', function () { }); it('inactive list', async function () { - const updateList = util.promisify(mailboxdb.updateList); - - await updateList(LIST_NAME, domain.domain, { members: [ mailbox , 'outsider@external.com' ], membersOnly: false, active: false }); + await mail.updateList(LIST_NAME, domain.domain, { members: [ mailbox , 'outsider@external.com' ], membersOnly: false, active: false }, auditSource); const [error] = await safe(ldapSearch('cn=devs@example.com,ou=mailinglists,dc=cloudron', 'objectclass=mailGroup')); expect(error).to.be.a(ldap.NoSuchObjectError); }); @@ -390,13 +392,11 @@ describe('Ldap', function () { }); it('does not allow for inactive mailbox', async function () { - const updateMailbox = util.promisify(mailboxdb.updateMailbox); - await mail._updateDomain(domain.domain, { enabled: true }); - await updateMailbox(mailboxName, domain.domain, { ownerId: user.id, ownerType: mail.OWNERTYPE_USER, active: false }); + await mail.updateMailbox(mailboxName, domain.domain, { ownerId: user.id, ownerType: mail.OWNERTYPE_USER, active: false }, auditSource); const [error] = await safe(ldapBind(`cn=${mailbox},ou=sendmail,dc=cloudron`, 'badpassword')); expect(error).to.be.a(ldap.NoSuchObjectError); - await updateMailbox(mailboxName, domain.domain, { ownerId: user.id, ownerType: mail.OWNERTYPE_USER, active: true }); + await mail.updateMailbox(mailboxName, domain.domain, { ownerId: user.id, ownerType: mail.OWNERTYPE_USER, active: true }, auditSource); }); }); @@ -444,14 +444,12 @@ describe('Ldap', function () { }); it('does not allow for inactive mailbox', async function () { - const updateMailbox = util.promisify(mailboxdb.updateMailbox); - await mail._updateDomain(domain.domain, { enabled: true }); - await updateMailbox(mailboxName, domain.domain, { ownerId: user.id, ownerType: mail.OWNERTYPE_USER, active: false }); + await mail.updateMailbox(mailboxName, domain.domain, { ownerId: user.id, ownerType: mail.OWNERTYPE_USER, active: false }, auditSource); const [error] = await safe(ldapBind(`cn=${mailbox},ou=recvmail,dc=cloudron`, 'badpassword')); expect(error).to.be.a(ldap.NoSuchObjectError); await mail._updateDomain(domain.domain, { enabled: false }); - await updateMailbox(mailboxName, domain.domain, { ownerId: user.id, ownerType: mail.OWNERTYPE_USER, active: true }); + await mail.updateMailbox(mailboxName, domain.domain, { ownerId: user.id, ownerType: mail.OWNERTYPE_USER, active: true }, auditSource); }); }); diff --git a/src/test/mail-test.js b/src/test/mail-test.js index 9a1cdb8b5..5051cec6d 100644 --- a/src/test/mail-test.js +++ b/src/test/mail-test.js @@ -6,8 +6,10 @@ 'use strict'; const common = require('./common.js'), + BoxError = require('../boxerror.js'), expect = require('expect.js'), - mail = require('../mail.js'); + mail = require('../mail.js'), + safe = require('safetydance'); describe('Mail', function () { const { setup, cleanup, domain, auditSource } = common; @@ -15,7 +17,7 @@ describe('Mail', function () { before(setup); after(cleanup); - describe('values', function () { + describe('settings', function () { it('can get default', async function () { const mailConfig = await mail.getDomain(domain.domain); expect(mailConfig.enabled).to.be(false); @@ -70,4 +72,109 @@ describe('Mail', function () { expect(mailConfig.enabled).to.be(true); }); }); + + describe('mailboxes', function () { + it('add user mailbox succeeds', async function () { + await mail.addMailbox('girish', domain.domain, { ownerId: 'uid-0', ownerType: mail.OWNERTYPE_USER, active: true }, auditSource); + }); + + it('cannot add dup entry', async function () { + const [error] = await safe(mail.addMailbox('girish', domain.domain, { ownerId: 'uid-1', ownerType: mail.OWNERTYPE_GROUP, active: true }, auditSource)); + expect(error.reason).to.be(BoxError.ALREADY_EXISTS); + }); + + it('add app mailbox succeeds', async function () { + await mail.addMailbox('support', domain.domain, { ownerId: 'osticket', ownerType: 'user', active: true}, auditSource); + }); + + it('get succeeds', async function () { + const mailbox = await mail.getMailbox('support', domain.domain); + expect(mailbox.name).to.equal('support'); + expect(mailbox.ownerId).to.equal('osticket'); + expect(mailbox.domain).to.equal(domain.domain); + expect(mailbox.creationTime).to.be.a(Date); + }); + + it('get non-existent mailbox', async function () { + const mailbox = await mail.getMailbox('random', domain.domain); + expect(mailbox).to.be(null); + }); + + it('list mailboxes succeeds', async function () { + const mailboxes = await mail.listMailboxes(domain.domain, null /* search */, 1, 10); + expect(mailboxes.length).to.be(2); + expect(mailboxes[0].name).to.be('girish'); + expect(mailboxes[1].name).to.be('support'); + }); + + it('list all mailboxes succeeds', async function () { + const mailboxes = await mail.listAllMailboxes(1, 10); + expect(mailboxes.length).to.be(2); + expect(mailboxes[0].name).to.be('girish'); + expect(mailboxes[0].domain).to.be(domain.domain); + expect(mailboxes[1].name).to.be('support'); + expect(mailboxes[1].domain).to.be(domain.domain); + }); + + it('mailbox count succeeds', async function () { + const count = await mail.getMailboxCount(domain.domain); + expect(count).to.be(2); + }); + + it('can set alias', async function () { + await mail.setAliases('support', domain.domain, [ { name: 'support2', domain: domain.domain }, { name: 'help', domain: domain.domain } ]); + }); + + it('can get aliases of name', async function () { + const results = await mail.getAliases('support', domain.domain); + expect(results.length).to.be(2); + expect(results[0].name).to.be('help'); + expect(results[0].domain).to.be(domain.domain); + expect(results[1].name).to.be('support2'); + expect(results[1].domain).to.be(domain.domain); + }); + + it('unset aliases', async function () { + await mail.setAliases('support', domain.domain, []); + + const results = await mail.getAliases('support', domain.domain); + expect(results.length).to.be(0); + }); + + it('add list succeeds', async function () { + await mail.addList('people', domain.domain, { members: [ 'test@cloudron.io' ], membersOnly: false, active: true }, auditSource); + }); + + it('cannot add dup list', async function () { + const [error] = await safe(mail.addList('people', domain.domain, { members: [ 'admin@cloudron.io' ], membersOnly: false, active: true }, auditSource)); + expect(error.reason).to.be(BoxError.ALREADY_EXISTS); + }); + + it('cannot get non-existing list', async function () { + const result = await mail.getList('random', domain.domain); + expect(result).to.be(null); + }); + + it('del list succeeds', async function () { + await mail.delList('people', domain.domain, auditSource); + const result = await mail.getList('people', domain.domain); + expect(result).to.be(null); + }); + + it('del non-existent list fails', async function () { + const [error] = await safe(mail.delList('people', domain.domain, auditSource)); + expect(error.reason).to.be(BoxError.NOT_FOUND); + }); + + it('del mailbox succeeds', async function () { + await mail.delMailbox('girish', domain.domain, {/*options*/}, auditSource); + const result = await mail.getMailbox('girish', domain.domain); + expect(result).to.be(null); + }); + + it('del non-existent mailbox fails', async function () { + const [error] = await safe(mail.delMailbox('girish', domain.domain, {/*options*/}, auditSource)); + expect(error.reason).to.be(BoxError.NOT_FOUND); + }); + }); });