diff --git a/migrations/20160527010012-mailboxes-add-table.js b/migrations/20160527010012-mailboxes-add-table.js index 5b7f00bdb..0a7bfd481 100644 --- a/migrations/20160527010012-mailboxes-add-table.js +++ b/migrations/20160527010012-mailboxes-add-table.js @@ -5,11 +5,10 @@ var type = dbm.dataType; exports.up = function(db, callback) { var cmd = 'CREATE TABLE mailboxes(' + - 'id VARCHAR(128) NOT NULL,' + - 'name VARCHAR(128) NOT NULL UNIQUE,' + + 'name VARCHAR(128) NOT NULL,' + 'aliasTarget VARCHAR(128),' + 'creationTime TIMESTAMP,' + - 'PRIMARY KEY (id))'; + 'PRIMARY KEY (name))'; db.runSql(cmd, function (error) { if (error) console.error(error); diff --git a/migrations/schema.sql b/migrations/schema.sql index 93b602e16..a74ade74c 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -120,9 +120,8 @@ CREATE TABLE IF NOT EXISTS eventlog( PRIMARY KEY (id)); CREATE TABLE IF NOT EXISTS mailboxes( - id VARCHAR(128) NOT NULL, - name VARCHAR(128) NOT NULL UNIQUE, - aliasTarget VARCHAR(128), /* the target if type is an alias */ + name VARCHAR(128) NOT NULL, + aliasTarget VARCHAR(128), /* the target name type is an alias */ creationTime TIMESTAMP, PRIMARY KEY (id)); diff --git a/src/mailboxdb.js b/src/mailboxdb.js index 0ce7ed133..7f1f6a4c1 100644 --- a/src/mailboxdb.js +++ b/src/mailboxdb.js @@ -5,22 +5,24 @@ exports = module.exports = { del: del, get: get, getAll: getAll, + getAliases: getAliases, + setAliases: setAliases, _clear: clear }; var assert = require('assert'), database = require('./database.js'), - DatabaseError = require('./databaseerror.js'); + DatabaseError = require('./databaseerror.js'), + util = require('util'); -var MAILBOX_FIELDS = [ 'id', 'name', 'aliasTarget', 'creationTime' ].join(','); +var MAILBOX_FIELDS = [ 'name', 'aliasTarget', 'creationTime' ].join(','); -function add(id, name, callback) { - assert.strictEqual(typeof id, 'string'); +function add(name, callback) { assert.strictEqual(typeof name, 'string'); assert.strictEqual(typeof callback, 'function'); - database.query('INSERT INTO mailboxes (id, name) VALUES (?, ?)', [ id, name ], function (error) { + database.query('INSERT INTO mailboxes (name) VALUES (?)', [ name ], function (error) { if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS)); if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error)); @@ -37,11 +39,12 @@ function clear(callback) { }); } -function del(id, callback) { - assert.strictEqual(typeof id, 'string'); +function del(name, callback) { + assert.strictEqual(typeof name, 'string'); assert.strictEqual(typeof callback, 'function'); - database.query('DELETE FROM mailboxes WHERE id=?', [ id ], function (error, result) { + // deletes aliases as well + database.query('DELETE FROM mailboxes WHERE name=? OR aliasTarget = ?', [ name, name ], function (error, result) { if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error)); if (result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND)); @@ -49,11 +52,11 @@ function del(id, callback) { }); } -function get(id, callback) { - assert.strictEqual(typeof id, 'string'); +function get(name, callback) { + assert.strictEqual(typeof name, 'string'); assert.strictEqual(typeof callback, 'function'); - database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE id=?', [ id ], function (error, results) { + database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name=? AND aliasTarget IS NULL', [ name ], function (error, results) { if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error)); if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND)); @@ -61,13 +64,44 @@ function get(id, callback) { }); } - function getAll(callback) { assert.strictEqual(typeof callback, 'function'); - database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes', function (error, results) { + database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE aliasTarget IS NULL', function (error, results) { if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error)); callback(null, results); }); } + +function setAliases(name, aliases, callback) { + assert.strictEqual(typeof name, 'string'); + assert(util.isArray(aliases)); + assert.strictEqual(typeof callback, 'function'); + + // also cleanup the groupMembers table + var queries = []; + queries.push({ query: 'DELETE FROM mailboxes WHERE aliasTarget = ?', args: [ name ] }); + aliases.forEach(function (alias) { + queries.push({ query: 'INSERT INTO mailboxes (name, aliasTarget) VALUES (?, ?)', args: [ alias, name ] }); + }); + + database.transaction(queries, function (error) { + if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error.message)); + if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error)); + + callback(null); + }); +} + +function getAliases(name, callback) { + assert.strictEqual(typeof name, 'string'); + assert.strictEqual(typeof callback, 'function'); + + database.query('SELECT name FROM mailboxes WHERE aliasTarget=? ORDER BY name', [ name ], function (error, results) { + if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error)); + + results = results.map(function (r) { return r.name; }); + callback(null, results); + }); +} diff --git a/src/mailboxes.js b/src/mailboxes.js index bca1129af..73af51f33 100644 --- a/src/mailboxes.js +++ b/src/mailboxes.js @@ -5,6 +5,8 @@ exports = module.exports = { del: del, get: get, getAll: getAll, + setAliases: setAliases, + getAliases: getAliases, MailboxError: MailboxError }; @@ -59,12 +61,11 @@ function add(name, callback) { var error = validateName(name); if (error) return callback(error); - mailboxdb.add(name /* id */, name, function (error) { + mailboxdb.add(name, function (error) { if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailboxError(MailboxError.ALREADY_EXISTS)); if (error) return callback(new MailboxError(MailboxError.INTERNAL_ERROR, error)); var mailbox = { - id: name, name: name }; @@ -72,11 +73,11 @@ function add(name, callback) { }); } -function del(id, callback) { - assert.strictEqual(typeof id, 'string'); +function del(name, callback) { + assert.strictEqual(typeof name, 'string'); assert.strictEqual(typeof callback, 'function'); - mailboxdb.del(id, function (error) { + mailboxdb.del(name, function (error) { if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailboxError(MailboxError.NOT_FOUND)); if (error) return callback(new MailboxError(MailboxError.INTERNAL_ERROR, error)); @@ -84,11 +85,11 @@ function del(id, callback) { }); } -function get(id, callback) { - assert.strictEqual(typeof id, 'string'); +function get(name, callback) { + assert.strictEqual(typeof name, 'string'); assert.strictEqual(typeof callback, 'function'); - mailboxdb.get(id, function (error, mailbox) { + mailboxdb.get(name, function (error, mailbox) { if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailboxError(MailboxError.NOT_FOUND)); if (error) return callback(new MailboxError(MailboxError.INTERNAL_ERROR, error)); @@ -105,3 +106,36 @@ function getAll(callback) { callback(null, results); }); } + +function setAliases(name, aliases, callback) { + assert.strictEqual(typeof name, 'string'); + assert(util.isArray(aliases)); + assert.strictEqual(typeof callback, 'function'); + + for (var i = 0; i < aliases.length; i++) { + aliases[i] = aliases[i].toLowerCase(); + + var error = validateName(aliases[i]); + if (error) return callback(error); + } + + mailboxdb.setAliases(name, aliases, function (error) { + if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailboxError(MailboxError.ALREADY_EXISTS, error.message)) + if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailboxError(MailboxError.NOT_FOUND)); + if (error) return callback(new MailboxError(MailboxError.INTERNAL_ERROR, error)); + + callback(null); + }); +} + +function getAliases(name, callback) { + assert.strictEqual(typeof name, 'string'); + assert.strictEqual(typeof callback, 'function'); + + mailboxdb.getAliases(name, function (error, aliases) { + if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailboxError(MailboxError.NOT_FOUND)); + if (error) return callback(new MailboxError(MailboxError.INTERNAL_ERROR, error)); + + callback(null, aliases); + }); +} diff --git a/src/routes/mailboxes.js b/src/routes/mailboxes.js index e7f0750d7..2c979da04 100644 --- a/src/routes/mailboxes.js +++ b/src/routes/mailboxes.js @@ -4,14 +4,17 @@ exports = module.exports = { list: list, get: get, remove: remove, - create: create + create: create, + setAliases: setAliases, + getAliases: getAliases }; var assert = require('assert'), HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess, mailboxes = require('../mailboxes.js'), - MailboxError = mailboxes.MailboxError; + MailboxError = mailboxes.MailboxError, + util = require('util'); function create(req, res, next) { assert.strictEqual(typeof req.body, 'object'); @@ -31,7 +34,7 @@ function get(req, res, next) { assert.strictEqual(typeof req.params.mailboxId, 'string'); mailboxes.get(req.params.mailboxId, function (error, result) { - if (error && error.reason === MailboxError.NOT_FOUND) return next(new HttpError(404, 'No such group')); + if (error && error.reason === MailboxError.NOT_FOUND) return next(new HttpError(404, 'No such mailbox')); if (error) return next(new HttpError(500, error)); next(new HttpSuccess(200, result)); @@ -56,3 +59,33 @@ function remove(req, res, next) { next(new HttpSuccess(204)); }); } + +function setAliases(req, res, next) { + assert.strictEqual(typeof req.params.mailboxId, 'string'); + + if (!util.isArray(req.body.aliases)) return next(new HttpError(400, 'aliases must be an array')); + + for (var i = 0; i < req.body.aliases.length; i++) { + if (typeof req.body.aliases[i] !== 'string') return next(new HttpError(400, 'alias must be a string')); + } + + mailboxes.setAliases(req.params.mailboxId, req.body.aliases, function (error) { + if (error && error.reason === MailboxError.NOT_FOUND) return next(new HttpError(404, 'No such mailbox')); + if (error && error.reason === MailboxError.BAD_NAME) return next(new HttpError(400, error.reason)); + if (error && error.reason === MailboxError.ALREADY_EXISTS) return next(new HttpError(409, 'One or more alias already exist')); + if (error) return next(new HttpError(500, error)); + + next(new HttpSuccess(200)); + }); +} + +function getAliases(req, res, next) { + assert.strictEqual(typeof req.params.mailboxId, 'string'); + + mailboxes.getAliases(req.params.mailboxId, function (error, aliases) { + if (error && error.reason === MailboxError.NOT_FOUND) return next(new HttpError(404, 'No such mailbox')); + if (error) return next(new HttpError(500, error)); + + next(new HttpSuccess(200, { aliases: aliases })); + }); +} diff --git a/src/routes/test/mailboxes-test.js b/src/routes/test/mailboxes-test.js index 276d2dc98..e364e61d5 100644 --- a/src/routes/test/mailboxes-test.js +++ b/src/routes/test/mailboxes-test.js @@ -116,6 +116,36 @@ describe('Mailbox API', function () { }); }); + it('cannot set with invalid alias', function (done) { + superagent.put(SERVER_URL + '/api/v1/mailboxes/' + MAILBOX_ID + '/aliases') + .query({ access_token: token }) + .send({ aliases: [ 'a' ]}) + .end(function (err, res) { + expect(res.statusCode).to.equal(400); + done(); + }); + }); + + it('cannot set with invalid type', function (done) { + superagent.put(SERVER_URL + '/api/v1/mailboxes/' + MAILBOX_ID + '/aliases') + .query({ access_token: token }) + .send({ aliases: [ 'apple', 34 ]}) + .end(function (err, res) { + expect(res.statusCode).to.equal(400); + done(); + }); + }); + + it('can set aliases of mailbox', function (done) { + superagent.put(SERVER_URL + '/api/v1/mailboxes/' + MAILBOX_ID + '/aliases') + .query({ access_token: token }) + .send({ aliases: [ 'alias1', 'alias2' ]}) + .end(function (err, res) { + expect(res.statusCode).to.equal(200); + done(); + }); + }); + it('can list mailboxes', function (done) { superagent.get(SERVER_URL + '/api/v1/mailboxes') .query({ access_token: token }) @@ -127,6 +157,38 @@ describe('Mailbox API', function () { }); }); + it('can get aliases', function (done) { + superagent.get(SERVER_URL + '/api/v1/mailboxes/' + MAILBOX_ID + '/aliases') + .query({ access_token: token }) + .end(function (err, res) { + expect(res.statusCode).to.equal(200); + expect(res.body.aliases).to.be.an(Array); + expect(res.body.aliases[0]).to.be('alias1'); + expect(res.body.aliases[1]).to.be('alias2'); + done(); + }); + }); + + it('can add another mailbox', function (done) { + superagent.post(SERVER_URL + '/api/v1/mailboxes') + .query({ access_token: token }) + .send({ name: MAILBOX_ID + '2' }) + .end(function (err, res) { + expect(res.statusCode).to.equal(201); + done(); + }); + }); + + it('cannot alias existing mailbox', function (done) { + superagent.put(SERVER_URL + '/api/v1/mailboxes/' + MAILBOX_ID + '/aliases') + .query({ access_token: token }) + .send({ aliases: [ MAILBOX_ID + '2' ]}) + .end(function (err, res) { + expect(res.statusCode).to.equal(409); + done(); + }); + }); + it('can delete mailbox', function (done) { superagent.del(SERVER_URL + '/api/v1/mailboxes/' + MAILBOX_ID) .query({ access_token: token }) diff --git a/src/server.js b/src/server.js index afae33699..b7f9c6130 100644 --- a/src/server.js +++ b/src/server.js @@ -125,6 +125,8 @@ function initializeExpressSync() { router.post('/api/v1/mailboxes', usersScope, routes.user.requireAdmin, routes.mailboxes.create); router.get ('/api/v1/mailboxes/:mailboxId', usersScope, routes.user.requireAdmin, routes.mailboxes.get); router.del ('/api/v1/mailboxes/:mailboxId', usersScope, routes.user.requireAdmin, routes.mailboxes.remove); + router.put ('/api/v1/mailboxes/:mailboxId/aliases', usersScope, routes.user.requireAdmin, routes.mailboxes.setAliases); + router.get ('/api/v1/mailboxes/:mailboxId/aliases', usersScope, routes.user.requireAdmin, routes.mailboxes.getAliases); // form based login routes used by oauth2 frame router.get ('/api/v1/session/login', csrf, routes.oauth2.loginForm); diff --git a/src/test/database-test.js b/src/test/database-test.js index 1508160d3..c471fddf1 100644 --- a/src/test/database-test.js +++ b/src/test/database-test.js @@ -1127,23 +1127,22 @@ describe('database', function () { describe('mailboxes', function () { it('add succeeds', function (done) { - mailboxdb.add('supportid', 'support', function (error, mailbox) { + mailboxdb.add('support', function (error, mailbox) { expect(error).to.be(null); done(); }); }); it('cannot add dup entry', function (done) { - mailboxdb.add('supportid2', 'support', function (error, mailbox) { + mailboxdb.add('support', function (error, mailbox) { expect(error.reason).to.be(DatabaseError.ALREADY_EXISTS); done(); }); }); it('get succeeds', function (done) { - mailboxdb.get('supportid', function (error, mailbox) { + mailboxdb.get('support', function (error, mailbox) { expect(error).to.be(null); - expect(mailbox.id).to.be('supportid'); expect(mailbox.name).to.be('support'); expect(mailbox.creationTime).to.be.a(Date); @@ -1156,14 +1155,43 @@ describe('database', function () { expect(error).to.be(null); expect(results).to.be.an(Array); expect(results.length).to.be(1); - expect(results[0].id).to.be('supportid'); + expect(results[0].name).to.be('support'); done(); }); }); + it('can set alias', function (done) { + mailboxdb.setAliases('support2', [ 'support2', 'help' ], function (error) { + expect(error).to.be(null); + done(); + }); + }); + + it('can get alias', function (done) { + mailboxdb.getAliases('support2', function (error, results) { + expect(error).to.be(null); + expect(results.length).to.be(2); + expect(results[0]).to.be('help'); + expect(results[1]).to.be('support2') + done(); + }); + }); + + it('unset aliases', function (done) { + mailboxdb.setAliases('support2', [ ], function (error) { + expect(error).to.be(null); + + mailboxdb.getAliases('support2', function (error, results) { + expect(error).to.be(null); + expect(results.length).to.be(0); + done(); + }); + }); + }); + it('del succeeds', function (done) { - mailboxdb.del('supportid', function (error) { + mailboxdb.del('support', function (error) { expect(error).to.be(null); done(); }); diff --git a/src/test/mailboxes-test.js b/src/test/mailboxes-test.js index 8822c4bdc..f08c818a9 100644 --- a/src/test/mailboxes-test.js +++ b/src/test/mailboxes-test.js @@ -38,7 +38,7 @@ function cleanup(done) { database._clear(done); } -var MAILBOX_ID = 'test'; +var MAILBOX_NAME = 'test'; describe('Mailboxes', function () { before(setup); @@ -73,14 +73,14 @@ describe('Mailboxes', function () { }); it('can create valid mailbox', function (done) { - mailboxes.add(MAILBOX_ID, function (error) { + mailboxes.add(MAILBOX_NAME, function (error) { expect(error).to.be(null); done(); }); }); it('cannot add existing mailbox', function (done) { - mailboxes.add(MAILBOX_ID, function (error) { + mailboxes.add(MAILBOX_NAME, function (error) { expect(error.reason).to.be(MailboxError.ALREADY_EXISTS); done(); }); @@ -94,9 +94,38 @@ describe('Mailboxes', function () { }); it('can get valid mailbox', function (done) { - mailboxes.get(MAILBOX_ID, function (error, group) { + mailboxes.get(MAILBOX_NAME, function (error, group) { expect(error).to.be(null); - expect(group.name).to.equal(MAILBOX_ID); + expect(group.name).to.equal(MAILBOX_NAME); + done(); + }); + }); + + it('can set aliases', function (done) { + mailboxes.setAliases(MAILBOX_NAME, [ 'alias1', 'alias2' ], function (error) { + expect(error).to.be(null); + done(); + }); + }); + + it('can set subset alias', function (done) { + mailboxes.setAliases(MAILBOX_NAME, [ 'alias1' ], function (error) { + expect(error).to.be(null); + done(); + }); + }); + + it('can get aliases', function (done) { + mailboxes.getAliases(MAILBOX_NAME, function (error, aliases) { + expect(error).to.be(null); + expect(aliases[0]).to.be('alias1'); + done(); + }); + }); + + it('cannot set self-referential alias', function (done) { + mailboxes.setAliases(MAILBOX_NAME, [ MAILBOX_NAME ], function (error) { + expect(error.reason).to.be(MailboxError.ALREADY_EXISTS); done(); }); });