mail: quota support

This commit is contained in:
Girish Ramakrishnan
2022-08-17 23:18:38 +02:00
parent 1c18c16e38
commit b23189b45c
7 changed files with 70 additions and 17 deletions

View File

@@ -286,7 +286,9 @@ async function mailboxSearch(req, res, next) {
objectcategory: 'mailbox',
cn: `${mailbox.name}@${mailbox.domain}`,
uid: `${mailbox.name}@${mailbox.domain}`,
mail: `${mailbox.name}@${mailbox.domain}`
mail: `${mailbox.name}@${mailbox.domain}`,
storagequota: mailbox.storageQuota,
messagesquota: mailbox.messagesQuota,
}
};
@@ -323,7 +325,9 @@ async function mailboxSearch(req, res, next) {
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}`
mail: `${mailbox.name}@${mailbox.domain}`,
storagequota: mailbox.storageQuota,
messagesquota: mailbox.messagesQuota,
}
};

View File

@@ -112,7 +112,7 @@ const REMOVE_MAILBOX_CMD = path.join(__dirname, 'scripts/rmmailbox.sh');
const OWNERTYPES = [ exports.OWNERTYPE_USER, exports.OWNERTYPE_GROUP, exports.OWNERTYPE_APP ];
// if you add a field here, listMailboxes has to be updated
const MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'ownerType', 'aliasName', 'aliasDomain', 'creationTime', 'membersJson', 'membersOnly', 'domain', 'active', 'enablePop3' ].join(',');
const MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'ownerType', 'aliasName', 'aliasDomain', 'creationTime', 'membersJson', 'membersOnly', 'domain', 'active', 'enablePop3', 'storageQuota', 'messagesQuota' ].join(',');
const MAILDB_FIELDS = [ 'domain', 'enabled', 'mailFromValidation', 'catchAllJson', 'relayJson', 'dkimKeyJson', 'dkimSelector', 'bannerJson' ].join(',');
function postProcessMailbox(data) {
@@ -1089,7 +1089,7 @@ async function listMailboxes(domain, search, page, perPage) {
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, m1.enablePop3 AS enablePop3 '
const query = 'SELECT m1.name AS name, m1.domain AS domain, m1.ownerId AS ownerId, m1.ownerType as ownerType, m1.active as active, JSON_ARRAYAGG(m2.name) AS aliasNames, JSON_ARRAYAGG(m2.domain) AS aliasDomains, m1.enablePop3 AS enablePop3, m1.storageQuota AS storageQuota, m1.messagesQuota AS messagesQuota '
+ ` FROM (SELECT * FROM mailboxes WHERE type='${exports.TYPE_MAILBOX}') AS m1`
+ ` LEFT JOIN (SELECT * FROM mailboxes WHERE type='${exports.TYPE_ALIAS}') AS m2`
+ ' ON m1.name=m2.aliasName AND m1.domain=m2.aliasDomain AND m1.ownerId=m2.ownerId'
@@ -1110,7 +1110,7 @@ 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, m1.enablePop3 AS enablePop3 '
const query = 'SELECT m1.name AS name, m1.domain AS domain, m1.ownerId AS ownerId, m1.ownerType as ownerType, m1.active as active, JSON_ARRAYAGG(m2.name) AS aliasNames, JSON_ARRAYAGG(m2.domain) AS aliasDomains, m1.enablePop3 AS enablePop3, m1.storageQuota AS storageQuota, m1.messagesQuota AS messagesQuota '
+ ` FROM (SELECT * FROM mailboxes WHERE type='${exports.TYPE_MAILBOX}') AS m1`
+ ` LEFT JOIN (SELECT * FROM mailboxes WHERE type='${exports.TYPE_ALIAS}') AS m2`
+ ' ON m1.name=m2.aliasName AND m1.domain=m2.aliasDomain AND m1.ownerId=m2.ownerId'
@@ -1164,10 +1164,12 @@ async function addMailbox(name, domain, data, auditSource) {
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof auditSource, 'object');
const { ownerId, ownerType, active } = data;
const { ownerId, ownerType, active, storageQuota, messagesQuota } = data;
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof ownerType, 'string');
assert.strictEqual(typeof active, 'boolean');
assert(Number.isInteger(storageQuota) && storageQuota >= 0);
assert(Number.isInteger(messagesQuota) && messagesQuota >= 0);
name = name.toLowerCase();
@@ -1176,12 +1178,13 @@ async function addMailbox(name, domain, data, auditSource) {
if (!OWNERTYPES.includes(ownerType)) throw new BoxError(BoxError.BAD_FIELD, 'bad owner type');
[error] = await safe(database.query('INSERT INTO mailboxes (name, type, domain, ownerId, ownerType, active) VALUES (?, ?, ?, ?, ?, ?)', [ name, exports.TYPE_MAILBOX, domain, ownerId, ownerType, active ]));
[error] = await safe(database.query('INSERT INTO mailboxes (name, type, domain, ownerId, ownerType, active, storageQuota, messagesQuota) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[ name, exports.TYPE_MAILBOX, domain, ownerId, ownerType, active, storageQuota, messagesQuota ]));
if (error && error.code === 'ER_DUP_ENTRY') throw new BoxError(BoxError.ALREADY_EXISTS, 'mailbox already exists');
if (error && error.code === 'ER_NO_REFERENCED_ROW_2' && error.sqlMessage.includes('mailboxes_domain_constraint')) throw new BoxError(BoxError.NOT_FOUND, `no such domain '${domain}'`);
if (error) throw error;
await eventlog.add(eventlog.ACTION_MAIL_MAILBOX_ADD, auditSource, { name, domain, ownerId, ownerType, active });
await eventlog.add(eventlog.ACTION_MAIL_MAILBOX_ADD, auditSource, { name, domain, ownerId, ownerType, active, storageQuota, messageQuota: messagesQuota });
}
async function updateMailbox(name, domain, data, auditSource) {
@@ -1190,11 +1193,13 @@ async function updateMailbox(name, domain, data, auditSource) {
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof auditSource, 'object');
const { ownerId, ownerType, active, enablePop3 } = data;
const { ownerId, ownerType, active, enablePop3, storageQuota, messagesQuota } = data;
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof ownerType, 'string');
assert.strictEqual(typeof active, 'boolean');
assert.strictEqual(typeof enablePop3, 'boolean');
assert(Number.isInteger(storageQuota) && storageQuota >= 0);
assert(Number.isInteger(messagesQuota) && messagesQuota >= 0);
name = name.toLowerCase();
@@ -1203,10 +1208,11 @@ async function updateMailbox(name, domain, data, auditSource) {
const mailbox = await getMailbox(name, domain);
if (!mailbox) throw new BoxError(BoxError.NOT_FOUND, 'No such mailbox');
const result = await database.query('UPDATE mailboxes SET ownerId = ?, ownerType = ?, active = ?, enablePop3 = ? WHERE name = ? AND domain = ?', [ ownerId, ownerType, active, enablePop3, name, domain ]);
const result = await database.query('UPDATE mailboxes SET ownerId = ?, ownerType = ?, active = ?, enablePop3 = ?, storageQuota = ?, messagesQuota = ? WHERE name = ? AND domain = ?',
[ ownerId, ownerType, active, enablePop3, storageQuota, messagesQuota, name, domain ]);
if (result.affectedRows === 0) throw new BoxError(BoxError.NOT_FOUND, 'Mailbox not found');
await eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, oldUserId: mailbox.userId, ownerId, ownerType, active });
await eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, oldUserId: mailbox.userId, ownerId, ownerType, active, storageQuota, messagesQuota });
}
async function removeSolrIndex(mailbox) {

View File

@@ -177,6 +177,11 @@ async 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'));
if (!Number.isInteger(req.body.storageQuota)) return next(new HttpError(400, 'storageQuota must be an integer'));
if (req.body.storageQuota < 0) return next(new HttpError(400, 'storageQuota must be a postive integer or zero'));
if (!Number.isInteger(req.body.messagesQuota)) return next(new HttpError(400, 'messagesQuota must be an integer'));
if (req.body.messagesQuota < 0) return next(new HttpError(400, 'messagesQuota must be a positive integer or zero'));
const [error] = await safe(mail.addMailbox(req.body.name, req.params.domain, req.body, AuditSource.fromRequest(req)));
if (error) return next(BoxError.toHttpError(error));
@@ -192,6 +197,11 @@ async function updateMailbox(req, res, next) {
if (typeof req.body.active !== 'boolean') return next(new HttpError(400, 'active must be a boolean'));
if (typeof req.body.enablePop3 !== 'boolean') return next(new HttpError(400, 'enablePop3 must be a boolean'));
if (!Number.isInteger(req.body.storageQuota)) return next(new HttpError(400, 'storageQuota must be an integer'));
if (req.body.storageQuota < 0) return next(new HttpError(400, 'storageQuota must be a postive integer or zero'));
if (!Number.isInteger(req.body.messagesQuota)) return next(new HttpError(400, 'messagesQuota must be an integer'));
if (req.body.messagesQuota < 0) return next(new HttpError(400, 'messagesQuota must be a positive integer or zero'));
const [error] = await safe(mail.updateMailbox(req.params.name, req.params.domain, req.body, AuditSource.fromRequest(req)));
if (error) return next(BoxError.toHttpError(error));

View File

@@ -422,7 +422,7 @@ describe('Mail API', function () {
it('add succeeds', async function () {
const response = await superagent.post(`${serverUrl}/api/v1/mail/${dashboardDomain}/mailboxes`)
.send({ name: MAILBOX_NAME, ownerId: owner.id, ownerType: 'user', active: true })
.send({ name: MAILBOX_NAME, ownerId: owner.id, ownerType: 'user', active: true, storageQuota: 10, messagesQuota: 20 })
.query({ access_token: owner.token });
expect(response.statusCode).to.equal(201);
@@ -430,7 +430,7 @@ describe('Mail API', function () {
it('cannot add again', async function () {
const response = await superagent.post(`${serverUrl}/api/v1/mail/${dashboardDomain}/mailboxes`)
.send({ name: MAILBOX_NAME, ownerId: owner.id, ownerType: 'user', active: true })
.send({ name: MAILBOX_NAME, ownerId: owner.id, ownerType: 'user', active: true, storageQuota: 10, messagesQuota: 20 })
.query({ access_token: owner.token })
.ok(() => true);
@@ -457,6 +457,8 @@ describe('Mail API', function () {
expect(response.body.mailbox.aliasName).to.equal(null);
expect(response.body.mailbox.aliasDomain).to.equal(null);
expect(response.body.mailbox.domain).to.equal(dashboardDomain);
expect(response.body.mailbox.storageQuota).to.equal(10);
expect(response.body.mailbox.messagesQuota).to.equal(20);
});
it('listing succeeds', async function () {
@@ -471,6 +473,8 @@ describe('Mail API', function () {
expect(response.body.mailboxes[0].ownerType).to.equal('user');
expect(response.body.mailboxes[0].aliases).to.eql([]);
expect(response.body.mailboxes[0].domain).to.equal(dashboardDomain);
expect(response.body.mailboxes[0].storageQuota).to.equal(10);
expect(response.body.mailboxes[0].messagesQuota).to.equal(20);
});
it('disable fails even if not exist', async function () {
@@ -505,7 +509,7 @@ describe('Mail API', function () {
it('add the mailbox', async function () {
const response = await superagent.post(`${serverUrl}/api/v1/mail/${dashboardDomain}/mailboxes`)
.send({ name: MAILBOX_NAME, ownerId: owner.id, ownerType: 'user', active: true })
.send({ name: MAILBOX_NAME, ownerId: owner.id, ownerType: 'user', active: true, storageQuota: 10, messagesQuota: 20 })
.query({ access_token: owner.token });
expect(response.statusCode).to.equal(201);

View File

@@ -114,16 +114,16 @@ describe('Mail', function () {
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);
await mail.addMailbox('girish', domain.domain, { ownerId: 'uid-0', ownerType: mail.OWNERTYPE_USER, active: true, storageQuota: 0, messagesQuota: 0 }, 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));
const [error] = await safe(mail.addMailbox('girish', domain.domain, { ownerId: 'uid-1', ownerType: mail.OWNERTYPE_GROUP, active: true, storageQuota: 0, messagesQuota: 0 }, 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);
await mail.addMailbox('support', domain.domain, { ownerId: 'osticket', ownerType: 'user', active: true, storageQuota: 10, messagesQuota: 20}, auditSource);
});
it('get succeeds', async function () {
@@ -132,6 +132,8 @@ describe('Mail', function () {
expect(mailbox.ownerId).to.equal('osticket');
expect(mailbox.domain).to.equal(domain.domain);
expect(mailbox.creationTime).to.be.a(Date);
expect(mailbox.storageQuota).to.be(10);
expect(mailbox.messagesQuota).to.be(20);
});
it('get non-existent mailbox', async function () {
@@ -139,6 +141,14 @@ describe('Mail', function () {
expect(mailbox).to.be(null);
});
it('update app mailbox succeeds', async function () {
await mail.updateMailbox('support', domain.domain, { ownerId: 'osticket', ownerType: 'user', active: true, storageQuota: 20, messagesQuota: 30}, auditSource);
const mailbox = await mail.getMailbox('support', domain.domain);
expect(mailbox.storageQuota).to.be(20);
expect(mailbox.messagesQuota).to.be(30);
});
it('list mailboxes succeeds', async function () {
const mailboxes = await mail.listMailboxes(domain.domain, null /* search */, 1, 10);
expect(mailboxes.length).to.be(2);