diff --git a/docs/references/api.md b/docs/references/api.md index b942ccbc5..b16ba3d13 100644 --- a/docs/references/api.md +++ b/docs/references/api.md @@ -893,125 +893,6 @@ Response (204): {} ``` -## Mailboxes - -Mailboxes allow users to receive mail using `IMAP`. Every user gets a mailbox with the same name as the username. -Users can receive email using IMAP (TLS) at `my.customdomain.com` and send mail using SMTP (STARTTLS) at -`my.customdomain.com`. - -Apps can also receive email using the [recvmail addon](/references/addons.html#recvmail). App mailboxes are -named as `.app`. For this reason, the `.app` suffix is reserved for apps. - -### Create mailbox - -POST `/api/v1/mailboxes` admin - -Creates a new mailbox. - -`name` specifies the address at which email can be received and does not include the domain name. `name` can contain -only alphanumeric characters and a dot ('.') and must be lower case. The Cloudron mail server support '+' based subaddressing (i.e)name+tag@domain.com will be delivered to this mailbox. - -mailboxes can only be accessed by users with the same username as the mailbox. A mailbox is automatically created -for every user on the Cloudron. - -Request: -``` -{ - name: -} -``` - -`name` must be atleast 2 characters and must be alphanumeric. - -Response (200): -``` -{ - id: , - name: -} -``` - -### Get mailbox - -GET `/api/v1/mailboxes/:mailboxName` admin - -Gets an existing mailbox with name `mailboxName`. - -Response (200): -``` -{ - id: , - name: , - aliases: [ , ...] -} -``` - -### List mailboxes - -GET `/api/v1/mailboxes` admin - -Lists all mailboxes. - -Response (200): -``` -{ - mailboxes: [ - { - id: , - name: - }, - ... - ] -} -``` - -### Set mailbox aliases - -PUT `/api/v1/mailboxes/:mailboxName/aliases` admin - -Sets aliases of an existing mailbox. - -An alias is an alternate email address for a mailbox. Mails received at the alternate address are stored -in the mailbox. The email's `to` header should indicate the address that the original sent it to. - -This also allows users to send emails with `From` address set to an alias. - -Note that this call will replace the current aliases for this mailbox, it does **not** merge the provided ones with the current aliases. - -Aliases have to be unique and cannot conflict with existing aliases or mailbox names. Any conflict with result in a 409. - -Request: -``` -{ - aliases: [ , ... ] -} - -``` - -### Get mailbox aliases - -GET `/api/v1/mailboxes/:mailboxName/aliases` admin - -Gets aliases of an existing mailbox. - -Response (200): -``` -{ - aliases: [ , ... ] -} -``` - -### Delete mailbox - -DELETE `/api/v1/mailboxes/:mailboxName` admin - -Deletes an existing with name `mailboxName`. - -Response (204): -``` -{} -``` - ## Profile ### Get profile diff --git a/src/ldap.js b/src/ldap.js index a574a810a..ab38e5915 100644 --- a/src/ldap.js +++ b/src/ldap.js @@ -8,13 +8,13 @@ exports = module.exports = { var assert = require('assert'), apps = require('./apps.js'), config = require('./config.js'), + DatabaseError = require('./databaseerror.js'), debug = require('debug')('box:ldap'), eventlog = require('./eventlog.js'), user = require('./user.js'), UserError = user.UserError, ldap = require('ldapjs'), - mailboxes = require('./mailboxes.js'), - MailboxError = mailboxes.MailboxError, + mailboxdb = require('./mailboxdb.js'), safe = require('safetydance'); var gServer = null; @@ -232,8 +232,8 @@ function authorizeUserForMailbox(req, res, next) { assert(req.user); // We simply authorize the user to access a mailbox by his own name - mailboxes.get(req.user.username, function (error) { - if (error && error.reason === MailboxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); + mailboxdb.get(req.user.username, function (error) { + if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); if (error) return next(new ldap.OperationsError(error.message)); eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: req.user.username }, { userId: req.user.username }); diff --git a/src/mailboxdb.js b/src/mailboxdb.js index 2cd696d8d..6d1f62f32 100644 --- a/src/mailboxdb.js +++ b/src/mailboxdb.js @@ -3,6 +3,7 @@ exports = module.exports = { add: add, del: del, + upsertByOwner: upsertByOwner, get: get, getAll: getAll, getAliases: getAliases, @@ -29,7 +30,21 @@ function add(name, ownerId, ownerType, callback) { assert.strictEqual(typeof ownerType, 'string'); assert.strictEqual(typeof callback, 'function'); - database.query('INSERT INTO mailboxes (name, ownerId, ownerType) VALUES (?, ?, ?)', [ name, ownerId, ownerType ], function (error) { + database.query('INSERT INTO mailboxes (name, ownerId, ownerType) VALUES (?, ?, ?)', [ name, ownerId, ownerType, 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)); + + callback(null); + }); +} + +function upsertByOwner(ownerId, ownerType, name, callback) { + assert.strictEqual(typeof ownerId, 'string'); + assert.strictEqual(typeof ownerType, 'string'); + assert.strictEqual(typeof name, 'string'); + assert.strictEqual(typeof callback, 'function'); + + database.query('INSERT INTO mailboxes (name, ownerId, ownerType) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE name=?', [ name, ownerId, ownerType, 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)); diff --git a/src/mailboxes.js b/src/mailboxes.js deleted file mode 100644 index e87861bcc..000000000 --- a/src/mailboxes.js +++ /dev/null @@ -1,152 +0,0 @@ -'use strict'; - -exports = module.exports = { - add: add, - del: del, - get: get, - getAll: getAll, - setAliases: setAliases, - getAliases: getAliases, - - MailboxError: MailboxError -}; - -var assert = require('assert'), - DatabaseError = require('./databaseerror.js'), - debug = require('debug')('box:mailboxes'), - mailboxdb = require('./mailboxdb.js'), - util = require('util'); - -function MailboxError(reason, errorOrMessage) { - assert.strictEqual(typeof reason, 'string'); - assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined'); - - Error.call(this); - Error.captureStackTrace(this, this.constructor); - - this.name = this.constructor.name; - this.reason = reason; - if (typeof errorOrMessage === 'undefined') { - this.message = reason; - } else if (typeof errorOrMessage === 'string') { - this.message = errorOrMessage; - } else { - this.message = 'Internal error'; - this.nestedError = errorOrMessage; - } -} -util.inherits(MailboxError, Error); -MailboxError.ALREADY_EXISTS = 'already exists'; -MailboxError.BAD_FIELD = 'Field error'; -MailboxError.NOT_FOUND = 'not found'; -MailboxError.INTERNAL_ERROR = 'internal error'; -MailboxError.EXTERNAL_ERROR = 'external error'; - -function validateName(name) { - var RESERVED_NAMES = [ 'no-reply', 'postmaster', 'mailer-daemon' ]; - - if (!name.length) return new MailboxError(MailboxError.BAD_FIELD, "name cannot be empty"); - - if (name.length < 2) return new MailboxError(MailboxError.BAD_FIELD, 'name too small'); - if (name.length > 127) return new MailboxError(MailboxError.BAD_FIELD, 'name too long'); - if (RESERVED_NAMES.indexOf(name) !== -1) return new MailboxError(MailboxError.BAD_FIELD, 'name is reserved'); - - if (/[^a-zA-Z0-9.]/.test(name)) return new MailboxError(MailboxError.BAD_FIELD, 'name can only contain alphanumerals and dot'); - - if (name.indexOf('.app') !== -1) return new MailboxError(MailboxError.BAD_FIELD, 'alias pattern is reserved for apps'); - - return null; -} - -function add(name, callback) { - assert.strictEqual(typeof name, 'string'); - assert.strictEqual(typeof callback, 'function'); - - name = name.toLowerCase(); - - var error = validateName(name); - if (error) return callback(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)); - - debug('Added mailbox %s', name); - - var mailbox = { - name: name - }; - - callback(null, mailbox); - }); -} - -function del(name, callback) { - assert.strictEqual(typeof name, 'string'); - assert.strictEqual(typeof callback, 'function'); - - 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)); - - debug('deleted mailbox %s', name); - - callback(); - }); -} - -function get(name, callback) { - assert.strictEqual(typeof name, 'string'); - assert.strictEqual(typeof callback, 'function'); - - 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)); - - callback(null, mailbox); - }); -} - -function getAll(callback) { - assert.strictEqual(typeof callback, 'function'); - - mailboxdb.getAll(function (error, results) { - if (error) return callback(new MailboxError(MailboxError.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'); - - 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/index.js b/src/routes/index.js index 4c945de94..2f9626868 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -9,7 +9,6 @@ exports = module.exports = { eventlog: require('./eventlog.js'), graphs: require('./graphs.js'), groups: require('./groups.js'), - mailboxes: require('./mailboxes.js'), oauth2: require('./oauth2.js'), profile: require('./profile.js'), sysadmin: require('./sysadmin.js'), diff --git a/src/routes/mailboxes.js b/src/routes/mailboxes.js deleted file mode 100644 index a2d2bc2c6..000000000 --- a/src/routes/mailboxes.js +++ /dev/null @@ -1,91 +0,0 @@ -'use strict'; - -exports = module.exports = { - list: list, - get: get, - remove: remove, - 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, - util = require('util'); - -function create(req, res, next) { - assert.strictEqual(typeof req.body, 'object'); - - if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be string')); - - mailboxes.add(req.body.name, function (error, mailbox) { - if (error && error.reason === MailboxError.BAD_FIELD) return next(new HttpError(400, error.message)); - if (error && error.reason === MailboxError.ALREADY_EXISTS) return next(new HttpError(409, 'Already exists')); - if (error) return next(new HttpError(500, error)); - - next(new HttpSuccess(201, mailbox)); - }); -} - -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 mailbox')); - if (error) return next(new HttpError(500, error)); - - next(new HttpSuccess(200, result)); - }); -} - -function list(req, res, next) { - mailboxes.getAll(function (error, result) { - if (error) return next(new HttpError(500, error)); - - next(new HttpSuccess(200, { mailboxes: result })); - }); -} - -function remove(req, res, next) { - assert.strictEqual(typeof req.params.mailboxId, 'string'); - - mailboxes.del(req.params.mailboxId, function (error) { - if (error && error.reason === MailboxError.NOT_FOUND) return next(new HttpError(404, 'Mailbox not found')); - if (error) return next(new HttpError(500, error)); - - 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_FIELD) return next(new HttpError(400, error.message)); - 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 deleted file mode 100644 index e364e61d5..000000000 --- a/src/routes/test/mailboxes-test.js +++ /dev/null @@ -1,211 +0,0 @@ -/* jslint node:true */ -/* global it:false */ -/* global describe:false */ -/* global before:false */ -/* global after:false */ - -'use strict'; - -var async = require('async'), - config = require('../../config.js'), - database = require('../../database.js'), - expect = require('expect.js'), - superagent = require('superagent'), - server = require('../../server.js'), - nock = require('nock'), - userdb = require('../../userdb.js'); - -var SERVER_URL = 'http://localhost:' + config.get('port'); - -var MAILBOX_ID = 'mailbox'; - -var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com'; -var token = null; - -var server; -function setup(done) { - config.set('fqdn', 'foobar.com'); - - async.series([ - server.start.bind(server), - - userdb._clear, - - function createAdmin(callback) { - var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {}); - var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {}); - - superagent.post(SERVER_URL + '/api/v1/cloudron/activate') - .query({ setupToken: 'somesetuptoken' }) - .send({ username: USERNAME, password: PASSWORD, email: EMAIL }) - .end(function (error, result) { - expect(result).to.be.ok(); - expect(result.statusCode).to.eql(201); - expect(scope1.isDone()).to.be.ok(); - expect(scope2.isDone()).to.be.ok(); - - // stash token for further use - token = result.body.token; - - callback(); - }); - } - ], done); -} - -function cleanup(done) { - database._clear(function (error) { - expect(!error).to.be.ok(); - - server.stop(done); - }); -} - -describe('Mailbox API', function () { - this.timeout(10000); - - before(setup); - after(cleanup); - - it('cannot create a mailbox without name param', function (done) { - superagent.post(SERVER_URL + '/api/v1/mailboxes') - .query({ access_token: token }) - .end(function (err, res) { - expect(res.statusCode).to.equal(400); - done(); - }); - }); - - it('cannot create a mailbox without token', function (done) { - superagent.post(SERVER_URL + '/api/v1/mailboxes') - .send({ name: MAILBOX_ID }) - .end(function (err, res) { - expect(res.statusCode).to.equal(401); - done(); - }); - }); - - it('cannot create invalid mailbox', function (done) { - superagent.post(SERVER_URL + '/api/v1/mailboxes') - .query({ access_token: token }) - .send({ name: 'no-reply' }) - .end(function (err, res) { - expect(res.statusCode).to.equal(400); - done(); - }); - }); - - it('can create mailbox', function (done) { - superagent.post(SERVER_URL + '/api/v1/mailboxes') - .query({ access_token: token }) - .send({ name: MAILBOX_ID }) - .end(function (err, res) { - expect(res.statusCode).to.equal(201); - done(); - }); - }); - - it('can get mailbox', function (done) { - superagent.get(SERVER_URL + '/api/v1/mailboxes/' + MAILBOX_ID) - .query({ access_token: token }) - .end(function (err, res) { - expect(res.statusCode).to.equal(200); - expect(res.body.name).to.equal(MAILBOX_ID); - expect(res.body.creationTime).to.be.ok(); - done(); - }); - }); - - 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 }) - .end(function (err, res) { - expect(res.statusCode).to.equal(200); - expect(res.body.mailboxes).to.be.an(Array); - expect(res.body.mailboxes[0].name).to.be(MAILBOX_ID); - done(); - }); - }); - - 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 }) - .send({ name: MAILBOX_ID }) - .end(function (err, res) { - expect(res.statusCode).to.equal(204); - done(); - }); - }); - - it('cannot delete random mailbox', function (done) { - superagent.del(SERVER_URL + '/api/v1/mailboxes/' + MAILBOX_ID) - .query({ access_token: token }) - .send({ name: MAILBOX_ID }) - .end(function (err, res) { - expect(res.statusCode).to.equal(404); - done(); - }); - }); -}); diff --git a/src/routes/user.js b/src/routes/user.js index 6680c2f29..6aecf5ddd 100644 --- a/src/routes/user.js +++ b/src/routes/user.js @@ -9,7 +9,9 @@ exports = module.exports = { verifyPassword: verifyPassword, requireAdmin: requireAdmin, sendInvite: sendInvite, - setGroups: setGroups + setGroups: setGroups, + setAliases: setAliases, + getAliases: getAliases }; var assert = require('assert'), @@ -21,6 +23,7 @@ var assert = require('assert'), oauth2 = require('./oauth2.js'), user = require('../user.js'), UserError = user.UserError, + util = require('util'), _ = require('underscore'); function auditSource(req) { @@ -190,3 +193,33 @@ function setGroups(req, res, next) { next(new HttpSuccess(204)); }); } + +function setAliases(req, res, next) { + assert.strictEqual(typeof req.params.userId, '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')); + } + + user.setAliases(req.params.userId, req.body.aliases, function (error) { + if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'No such user')); + if (error && error.reason === UserError.BAD_FIELD) return next(new HttpError(400, error.message)); + if (error && error.reason === UserError.ALREADY_EXISTS) return next(new HttpError(409, 'One or more aliases already exist')); + if (error) return next(new HttpError(500, error)); + + next(new HttpSuccess(200)); + }); +} + +function getAliases(req, res, next) { + assert.strictEqual(typeof req.params.userId, 'string'); + + user.getAliases(req.params.userId, function (error, aliases) { + if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'No such user')); + if (error) return next(new HttpError(500, error)); + + next(new HttpSuccess(200, { aliases: aliases })); + }); +} diff --git a/src/server.js b/src/server.js index 61787b129..01edb3ca9 100644 --- a/src/server.js +++ b/src/server.js @@ -115,6 +115,8 @@ function initializeExpressSync() { router.del ('/api/v1/users/:userId', usersScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.user.remove); router.post('/api/v1/users/:userId', usersScope, routes.user.requireAdmin, routes.user.update); router.put ('/api/v1/users/:userId/groups', usersScope, routes.user.requireAdmin, routes.user.setGroups); + router.get ('/api/v1/users/:userId/aliases', usersScope, routes.user.requireAdmin, routes.user.getAliases); + router.put ('/api/v1/users/:userId/aliases', usersScope, routes.user.requireAdmin, routes.user.setAliases); router.post('/api/v1/users/:userId/invite', usersScope, routes.user.requireAdmin, routes.user.sendInvite); // Group management @@ -123,14 +125,6 @@ function initializeExpressSync() { router.get ('/api/v1/groups/:groupId', usersScope, routes.user.requireAdmin, routes.groups.get); router.del ('/api/v1/groups/:groupId', usersScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.groups.remove); - // Mailbox management - router.get ('/api/v1/mailboxes', usersScope, routes.user.requireAdmin, routes.mailboxes.list); - 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); router.post('/api/v1/session/login', csrf, routes.oauth2.login); diff --git a/src/test/mailboxes-test.js b/src/test/mailboxes-test.js deleted file mode 100644 index f89a2b2eb..000000000 --- a/src/test/mailboxes-test.js +++ /dev/null @@ -1,136 +0,0 @@ -/* global it:false */ -/* global describe:false */ -/* global before:false */ -/* global after:false */ - -'use strict'; - -var database = require('../database.js'), - expect = require('expect.js'), - mailboxes = require('../mailboxes.js'), - MailboxError = mailboxes.MailboxError, - hat = require('hat'); - -function setup(done) { - // ensure data/config/mount paths - database.initialize(function (error) { - expect(error).to.be(null); - - database._clear(done); - }); -} - -function cleanup(done) { - database._clear(done); -} - -var MAILBOX_NAME = 'test'; - -describe('Mailboxes', function () { - before(setup); - after(cleanup); - - it('cannot create mailbox - too small', function (done) { - mailboxes.add('a', function (error) { - expect(error.reason).to.be(MailboxError.BAD_FIELD); - done(); - }); - }); - - it('cannot create mailbox - too big', function (done) { - mailboxes.add(new Array(129).join('a'), function (error) { - expect(error.reason).to.be(MailboxError.BAD_FIELD); - done(); - }); - }); - - it('cannot create mailbox - bad name', function (done) { - mailboxes.add('bad:name', function (error) { - expect(error.reason).to.be(MailboxError.BAD_FIELD); - done(); - }); - }); - - it('cannot create mailbox - reserved', function (done) { - mailboxes.add('no-reply', function (error) { - expect(error.reason).to.be(MailboxError.BAD_FIELD); - done(); - }); - }); - - it('can create valid mailbox', function (done) { - mailboxes.add(MAILBOX_NAME, function (error) { - expect(error).to.be(null); - done(); - }); - }); - - it('cannot add existing mailbox', function (done) { - mailboxes.add(MAILBOX_NAME, function (error) { - expect(error.reason).to.be(MailboxError.ALREADY_EXISTS); - done(); - }); - }); - - it('cannot get invalid mailbox', function (done) { - mailboxes.get('sometrandom', function (error) { - expect(error.reason).to.be(MailboxError.NOT_FOUND); - done(); - }); - }); - - it('can get valid mailbox', function (done) { - mailboxes.get(MAILBOX_NAME, function (error, group) { - expect(error).to.be(null); - 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('can get aliases from mailbox', function (done) { - mailboxes.get(MAILBOX_NAME, function (error, group) { - expect(error).to.be(null); - expect(group.name).to.equal(MAILBOX_NAME); - expect(group.aliases.length).to.be(1); - expect(group.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(); - }); - }); - - it('cannot delete invalid mailbox', function (done) { - mailboxes.del('random', function (error) { - expect(error.reason).to.be(MailboxError.NOT_FOUND); - done(); - }); - }); -}); - diff --git a/src/test/user-test.js b/src/test/user-test.js index 88a3238ae..450dd14ad 100644 --- a/src/test/user-test.js +++ b/src/test/user-test.js @@ -12,6 +12,7 @@ var async = require('async'), fs = require('fs'), groupdb = require('../groupdb.js'), groups = require('../groups.js'), + mailboxdb = require('../mailboxdb.js'), mailer = require('../mailer.js'), user = require('../user.js'), userdb = require('../userdb.js'), @@ -33,6 +34,7 @@ function cleanupUsers(done) { async.series([ groupdb._clear, userdb._clear, + mailboxdb._clear, mailer._clearMailQueue ], done); } diff --git a/src/user.js b/src/user.js index eeb9e2abf..c3845ad79 100644 --- a/src/user.js +++ b/src/user.js @@ -20,6 +20,8 @@ exports = module.exports = { getOwner: getOwner, sendInvite: sendInvite, setGroups: setGroups, + setAliases: setAliases, + getAliases: getAliases, setShowTutorial: setShowTutorial }; @@ -183,8 +185,6 @@ function createUser(username, password, email, displayName, auditSource, options }; asyncIf(!!username, mailboxdb.add.bind(null, username, user.id /* owner */, mailboxdb.TYPE_USER), function (error) { - if (error) return callback(error); - if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new UserError(UserError.ALREADY_EXISTS, error.message)); if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error)); @@ -372,7 +372,7 @@ function updateUser(userId, data, auditSource, callback) { if (error) return callback(error); } - function doUpdate(error, callback) { + asyncIf(!!data.username, mailboxdb.upsertByOwner.bind(null, userId /* owner */, mailboxdb.TYPE_USER, data.username), function (error) { if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new UserError(UserError.ALREADY_EXISTS, error.message)); if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error)); @@ -385,13 +385,7 @@ function updateUser(userId, data, auditSource, callback) { callback(null); }); - } - - if (data.username) { - mailboxdb.add(data.username, userId /* owner */, mailboxdb.TYPE_USER, doUpdate); // TODO: do this only when username actually changes - } else { - doUpdate(null, callback); - } + }); } function setGroups(userId, groupIds, callback) { @@ -571,3 +565,47 @@ function setShowTutorial(userId, showTutorial, callback) { callback(null); }); } + +function setAliases(userId, aliases, callback) { + assert.strictEqual(typeof userId, '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 = validateUsername(aliases[i]); + if (error) return callback(error); + } + + userdb.get(userId, function (error, user) { + if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND)); + if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error)); + + mailboxdb.setAliases(user.username, aliases, userId, mailboxdb.TYPE_USER, function (error) { + if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new UserError(UserError.ALREADY_EXISTS, error.message)); + if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND)); + if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error)); + + + callback(null); + }); + }); +} + +function getAliases(userId, callback) { + assert.strictEqual(typeof userId, 'string'); + assert.strictEqual(typeof callback, 'function'); + + userdb.get(userId, function (error, user) { + if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND)); + if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error)); + + mailboxdb.getAliases(user.username, function (error, aliases) { + if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND)); + if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error)); + + callback(null, aliases); + }); + }); +}