mailboxdb: merge into mail.js

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