Skip to content

Commit

Permalink
fix(api&imap-mailboxes): Added mailbox subpath and whole path max len…
Browse files Browse the repository at this point in the history
…gth limits to API and IMAP ZMS-169 (#732)

* Added submission api endpoint to api docs generation

* mailboxes.js, check for max subpath length and max count on create and rename of a mailbox

* add mailboxes tests

* IMAP rename err code fix. IMAP mailbox create and rename tests added

* IMAP tests, remove magic numbers

* mailboxes tests, remove magic numbers

* mailboxes tests fix broken tests

* remove repetitions
  • Loading branch information
NickOvt authored Sep 23, 2024
1 parent 971a0f1 commit ee870b9
Show file tree
Hide file tree
Showing 7 changed files with 309 additions and 6 deletions.
2 changes: 1 addition & 1 deletion imap-core/lib/commands/rename.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ module.exports = {
this._server.loggelf(logdata);
return callback(null, {
response: 'NO',
code: 'TEMPFAIL'
code: err.code || 'TEMPFAIL'
});
}

Expand Down
113 changes: 113 additions & 0 deletions imap-core/test/protocol-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ let chunks = require('./fixtures/chunks');
let expect = chai.expect;
chai.config.includeStack = true;

const { MAX_SUB_MAILBOXES, MAX_MAILBOX_NAME_LENGTH } = require('../../lib/consts.js');

describe('IMAP Protocol integration tests', function () {
this.timeout(100000); // eslint-disable-line no-invalid-this
let port = 9993;
Expand Down Expand Up @@ -472,6 +474,66 @@ describe('IMAP Protocol integration tests', function () {
}
);
});

it(`cannot create a mailbox with subpath length bigger than ${MAX_MAILBOX_NAME_LENGTH} chars`, function (done) {
let cmds = [
'T1 LOGIN testuser pass',
`T2 CREATE parent/child/${'a'.repeat(MAX_MAILBOX_NAME_LENGTH + 1)}`,
'T3 CREATE parent/child',
'T4 CREATE testfolder',
'T5 LIST "" "*"',
'T6 LOGOUT'
];

testClient(
{
commands: cmds,
secure: true,
port
},
function (resp) {
resp = resp.toString();
expect(/^T2 NO \[CANNOT\]/m.test(resp)).to.be.true;
expect(/^T3 OK/m.test(resp)).to.be.true;
expect(/^T4 OK/m.test(resp)).to.be.true;
expect(resp.indexOf('\r\n* LIST (\\HasNoChildren) "/" "testfolder"\r\n') >= 0).to.be.true;
expect(resp.indexOf('\r\n* LIST (\\Noselect \\HasChildren) "/" "parent"\r\n') >= 0).to.be.true;
expect(resp.indexOf('\r\n* LIST (\\HasNoChildren) "/" "parent/child"\r\n') >= 0).to.be.true;
done();
}
);
});

it(`cannot create a mailbox with more than ${MAX_SUB_MAILBOXES} subpaths`, function (done) {
let cmds = ['T1 LOGIN testuser pass', `T2 CREATE tobechanged`, 'T3 CREATE parent/child', 'T4 CREATE testfolder', 'T5 LIST "" "*"', 'T6 LOGOUT'];

let path = '';

for (let i = 0; i < MAX_SUB_MAILBOXES + 1; i++) {
path += `subpath${i}/`;
}
path = path.substring(0, path.length - 1);

cmds[1] = `T2 CREATE ${path}`;

testClient(
{
commands: cmds,
secure: true,
port
},
function (resp) {
resp = resp.toString();
expect(/^T2 NO \[CANNOT\]/m.test(resp)).to.be.true;
expect(/^T3 OK/m.test(resp)).to.be.true;
expect(/^T4 OK/m.test(resp)).to.be.true;
expect(resp.indexOf('\r\n* LIST (\\HasNoChildren) "/" "testfolder"\r\n') >= 0).to.be.true;
expect(resp.indexOf('\r\n* LIST (\\Noselect \\HasChildren) "/" "parent"\r\n') >= 0).to.be.true;
expect(resp.indexOf('\r\n* LIST (\\HasNoChildren) "/" "parent/child"\r\n') >= 0).to.be.true;
done();
}
);
});
});

describe('RENAME', function () {
Expand Down Expand Up @@ -502,6 +564,57 @@ describe('IMAP Protocol integration tests', function () {
}
);
});

it('cannot rename a mailbox to a mailbox path where subpath length is bigger than max allowed', function (done) {
let cmds = [
'T1 LOGIN testuser pass',
'T2 CREATE testfolder',
`T3 RENAME testfolder parent/child/${'a'.repeat(MAX_MAILBOX_NAME_LENGTH + 1)}`,
'T5 LIST "" "*"',
'T6 LOGOUT'
];

testClient(
{
commands: cmds,
secure: true,
port
},
function (resp) {
resp = resp.toString();
expect(/^T3 NO \[CANNOT\]/m.test(resp)).to.be.true;
expect(resp.indexOf('\r\n* LIST (\\HasNoChildren) "/" "testfolder"\r\n') >= 0).to.be.true;
done();
}
);
});

it('cannot rename a mailbox to a mailbox path where there are more than max subpath count', function (done) {
let cmds = ['T1 LOGIN testuser pass', 'T2 CREATE testfolder', `T3 RENAME testfolder parent/child`, 'T5 LIST "" "*"', 'T6 LOGOUT'];

let path = '';

for (let i = 0; i < MAX_SUB_MAILBOXES + 1; i++) {
path += `subpath${i}/`;
}
path = path.substring(0, path.length - 1);

cmds[2] = `T3 RENAME testfolder ${path}`;

testClient(
{
commands: cmds,
secure: true,
port
},
function (resp) {
resp = resp.toString();
expect(/^T3 NO \[CANNOT\]/m.test(resp)).to.be.true;
expect(resp.indexOf('\r\n* LIST (\\HasNoChildren) "/" "testfolder"\r\n') >= 0).to.be.true;
done();
}
);
});
});

describe('DELETE', function () {
Expand Down
7 changes: 6 additions & 1 deletion lib/api/mailboxes.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ const imapTools = require('../../imap-core/lib/imap-tools');
const tools = require('../tools');
const roles = require('../roles');
const util = require('util');
const { sessSchema, sessIPSchema, booleanSchema } = require('../schemas');
const { sessSchema, sessIPSchema, booleanSchema, mailboxPathValidator } = require('../schemas');
const { userId, mailboxId } = require('../schemas/request/general-schemas');
const { successRes } = require('../schemas/response/general-schemas');
const { GetMailboxesResult } = require('../schemas/response/mailboxes-schemas');
const { MAX_MAILBOX_NAME_LENGTH, MAX_SUB_MAILBOXES } = require('../consts');

module.exports = (db, server, mailboxHandler) => {
const getMailboxCounter = util.promisify(tools.getMailboxCounter);
Expand Down Expand Up @@ -281,6 +282,8 @@ module.exports = (db, server, mailboxHandler) => {
requestBody: {
path: Joi.string()
.regex(/\/{2,}|\/$/, { invert: true })
.max(MAX_MAILBOX_NAME_LENGTH * MAX_SUB_MAILBOXES + 127)
.custom(mailboxPathValidator, 'Mailbox path validation')
.required()
.description('Full path of the mailbox, folders are separated by slashes, ends with the mailbox name (unicode string)'),
hidden: booleanSchema.default(false).description('Is the folder hidden or not. Hidden folders can not be opened in IMAP.'),
Expand Down Expand Up @@ -539,6 +542,8 @@ module.exports = (db, server, mailboxHandler) => {
requestBody: {
path: Joi.string()
.regex(/\/{2,}|\/$/, { invert: true })
.max(MAX_MAILBOX_NAME_LENGTH * MAX_SUB_MAILBOXES + 127)
.custom(mailboxPathValidator, 'Mailbox path validation')
.description('Full path of the mailbox, use this to rename an existing Mailbox'),
retention: Joi.number()
.empty('')
Expand Down
8 changes: 7 additions & 1 deletion lib/consts.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,5 +137,11 @@ module.exports = {
MAX_FILTERS: 400,

// maximum amount of mailboxes per user
MAX_MAILBOXES: 1500
MAX_MAILBOXES: 1500,

// Max length of a mailbox subpath element
MAX_MAILBOX_NAME_LENGTH: 512,

// Number of mailbox subpaths in a single mailbox path
MAX_SUB_MAILBOXES: 128
};
42 changes: 42 additions & 0 deletions lib/mailbox-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const ObjectId = require('mongodb').ObjectId;
const ImapNotifier = require('./imap-notifier');
const { publish, MAILBOX_CREATED, MAILBOX_RENAMED, MAILBOX_DELETED } = require('./events');
const { SettingsHandler } = require('./settings-handler');
const { MAX_MAILBOX_NAME_LENGTH, MAX_SUB_MAILBOXES } = require('./consts');

class MailboxHandler {
constructor(options) {
Expand Down Expand Up @@ -40,6 +41,26 @@ class MailboxHandler {
throw err;
}

const splittedPathArr = path.split('/');

if (splittedPathArr.length > MAX_SUB_MAILBOXES) {
// too many subpaths
const err = new Error(`The mailbox path cannot be more than ${MAX_SUB_MAILBOXES} levels deep`);
err.code = 'CANNOT';
err.responseCode = 400;
throw err;
}

for (const pathPart of splittedPathArr) {
if (pathPart.length > MAX_MAILBOX_NAME_LENGTH) {
// individual path part longer than specified max
const err = new Error(`Any part of the mailbox path cannot be longer than ${MAX_MAILBOX_NAME_LENGTH} chars`);
err.code = 'CANNOT';
err.responseCode = 400;
throw err;
}
}

let mailboxData = await this.database.collection('mailboxes').findOne({ user, path });

if (mailboxData) {
Expand Down Expand Up @@ -118,6 +139,27 @@ class MailboxHandler {
if (err) {
return callback(err);
}

const splittedPathArr = newname.split('/');

if (splittedPathArr.length > MAX_SUB_MAILBOXES) {
// too many subpaths
const err = new Error(`The mailbox path cannot be more than ${MAX_SUB_MAILBOXES} levels deep`);
err.code = 'CANNOT';
err.responseCode = 400;
return callback(err, 'CANNOT');
}

for (const pathPart of splittedPathArr) {
if (pathPart.length > MAX_MAILBOX_NAME_LENGTH) {
// individual path part longer than specified max
const err = new Error(`Any part of the mailbox path cannot be longer than ${MAX_MAILBOX_NAME_LENGTH} chars`);
err.code = 'CANNOT';
err.responseCode = 400;
return callback(err, 'CANNOT');
}
}

if (!mailboxData) {
const err = new Error('Mailbox update failed with code NoSuchMailbox');
err.code = 'NONEXISTENT';
Expand Down
22 changes: 21 additions & 1 deletion lib/schemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const EJSON = require('mongodb-extended-json');
const Joi = require('joi');
const { MAX_SUB_MAILBOXES, MAX_MAILBOX_NAME_LENGTH } = require('./consts');

const sessSchema = Joi.string().max(255).label('Session identifier').description('Session identifier for the logs');
const sessIPSchema = Joi.string()
Expand Down Expand Up @@ -82,6 +83,24 @@ const metaDataValidator = () => (value, helpers) => {
return strValue;
};

const mailboxPathValidator = (value, helpers) => {
const splittedPathArr = value.split('/');

if (splittedPathArr.length > MAX_SUB_MAILBOXES) {
// too many paths
return helpers.message(`The mailbox path cannot be more than ${MAX_SUB_MAILBOXES} levels deep`);
}

for (const pathPart of splittedPathArr) {
if (pathPart.length > MAX_MAILBOX_NAME_LENGTH) {
// part too long error
return helpers.message(`Any part of the mailbox path cannot be longer than ${MAX_MAILBOX_NAME_LENGTH} chars long`);
}
}

return value;
};

const mongoCursorSchema = Joi.string().trim().empty('').custom(mongoCursorValidator({}), 'Cursor validation').max(1024);
const pageLimitSchema = Joi.number().default(20).min(1).max(250).label('Page size');
const pageNrSchema = Joi.number().default(1).label('Page number').description('Current page number. Informational only, page numbers start from 1');
Expand Down Expand Up @@ -111,5 +130,6 @@ module.exports = {
pageLimitSchema,
booleanSchema,
metaDataSchema,
usernameSchema
usernameSchema,
mailboxPathValidator
};
Loading

0 comments on commit ee870b9

Please sign in to comment.