diff --git a/migrations/20190303024631-apps-default-mailboxName.js b/migrations/20190303024631-apps-default-mailboxName.js index a63b4570e..9d2685d19 100644 --- a/migrations/20190303024631-apps-default-mailboxName.js +++ b/migrations/20190303024631-apps-default-mailboxName.js @@ -1,12 +1,6 @@ 'use strict'; -var async = require('async'), - crypto = require('crypto'), - fs = require('fs'), - os = require('os'), - path = require('path'), - safe = require('safetydance'), - tldjs = require('tldjs'); +var async = require('async'); exports.up = function(db, callback) { db.all('SELECT * FROM apps, subdomains WHERE apps.id=subdomains.appId AND type="primary"', function (error, apps) { diff --git a/migrations/20191115053638-appdb-add-mailboxDomain.js b/migrations/20191115053638-appdb-add-mailboxDomain.js new file mode 100644 index 000000000..cdfe1bfba --- /dev/null +++ b/migrations/20191115053638-appdb-add-mailboxDomain.js @@ -0,0 +1,27 @@ +'use strict'; + +var async = require('async'); + +exports.up = function(db, callback) { + async.series([ + db.runSql.bind(db, 'ALTER TABLE apps ADD COLUMN mailboxDomain VARCHAR(128)'), + function setDefaultMailboxDomain(done) { + db.all('SELECT * FROM apps, subdomains WHERE apps.id=subdomains.appId AND type="primary"', function (error, apps) { + if (error) return done(error); + + async.eachSeries(apps, function (app, iteratorDone) { + db.runSql('UPDATE apps SET mailboxDomain=? WHERE id=?', [ app.subdomain, app.id ], iteratorDone); + }, done); + }); + }, + db.runSql.bind(db, 'ALTER TABLE apps MODIFY COLUMN mailboxDomain VARCHAR(128) NOT NULL'), + db.runSql.bind(db, 'ALTER TABLE apps ADD CONSTRAINT apps_mailDomain_constraint FOREIGN KEY(mailboxDomain) REFERENCES domains(domain)'), + ], callback); +}; + +exports.down = function(db, callback) { + async.series([ + db.runSql.bind(db, 'ALTER TABLE app DROP FOREIGN KEY apps_mailDomain_constraint'), + db.runSql.bind(db, 'ALTER TABLE apps DROP COLUMN mailboxDomain'), + ], callback); +}; diff --git a/migrations/schema.sql b/migrations/schema.sql index 22e646a58..fad8991f3 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -85,12 +85,14 @@ CREATE TABLE IF NOT EXISTS apps( enableBackup BOOLEAN DEFAULT 1, // misnomer: controls automatic daily backups enableAutomaticUpdate BOOLEAN DEFAULT 1, mailboxName VARCHAR(128), // mailbox of this app. default allocated as '.app' + mailboxDomain VARCHAR(128) NOT NULL, // mailbox domain of this apps label VARCHAR(128), // display name tagsJson VARCHAR(2048), // array of tags dataDir VARCHAR(256) UNIQUE, taskId INTEGER, // current task errorJson TEXT, + FOREIGN KEY(mailboxDomain) REFERENCES domains(domain), FOREIGN KEY(taskId) REFERENCES tasks(id), PRIMARY KEY(id)); diff --git a/src/addons.js b/src/addons.js index c898bc98d..d639c1db4 100644 --- a/src/addons.js +++ b/src/addons.js @@ -913,10 +913,10 @@ function setupSendMail(app, options, callback) { { name: `${envPrefix}MAIL_SMTP_SERVER`, value: 'mail' }, { name: `${envPrefix}MAIL_SMTP_PORT`, value: '2525' }, { name: `${envPrefix}MAIL_SMTPS_PORT`, value: '2465' }, - { name: `${envPrefix}MAIL_SMTP_USERNAME`, value: app.mailboxName + '@' + app.domain }, + { name: `${envPrefix}MAIL_SMTP_USERNAME`, value: app.mailboxName + '@' + app.mailboxDomain }, { name: `${envPrefix}MAIL_SMTP_PASSWORD`, value: password }, - { name: `${envPrefix}MAIL_FROM`, value: app.mailboxName + '@' + app.domain }, - { name: `${envPrefix}MAIL_DOMAIN`, value: app.domain } + { name: `${envPrefix}MAIL_FROM`, value: app.mailboxName + '@' + app.mailboxDomain }, + { name: `${envPrefix}MAIL_DOMAIN`, value: app.mailboxDomain } ]; debugApp(app, 'Setting sendmail addon config to %j', env); appdb.setAddonConfig(app.id, 'sendmail', env, callback); @@ -950,10 +950,10 @@ function setupRecvMail(app, options, callback) { var env = [ { name: `${envPrefix}MAIL_IMAP_SERVER`, value: 'mail' }, { name: `${envPrefix}MAIL_IMAP_PORT`, value: '9993' }, - { name: `${envPrefix}MAIL_IMAP_USERNAME`, value: app.mailboxName + '@' + app.domain }, + { name: `${envPrefix}MAIL_IMAP_USERNAME`, value: app.mailboxName + '@' + app.mailboxDomain }, { name: `${envPrefix}MAIL_IMAP_PASSWORD`, value: password }, - { name: `${envPrefix}MAIL_TO`, value: app.mailboxName + '@' + app.domain }, - { name: `${envPrefix}MAIL_DOMAIN`, value: app.domain } + { name: `${envPrefix}MAIL_TO`, value: app.mailboxName + '@' + app.mailboxDomain }, + { name: `${envPrefix}MAIL_DOMAIN`, value: app.mailboxDomain } ]; debugApp(app, 'Setting sendmail addon config to %j', env); diff --git a/src/appdb.js b/src/appdb.js index 3f6254d46..67a4bcac1 100644 --- a/src/appdb.js +++ b/src/appdb.js @@ -43,7 +43,7 @@ var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationSta 'apps.accessRestrictionJson', 'apps.memoryLimit', 'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.sso', 'apps.debugModeJson', 'apps.enableBackup', - 'apps.creationTime', 'apps.updateTime', 'apps.mailboxName', 'apps.enableAutomaticUpdate', + 'apps.creationTime', 'apps.updateTime', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableAutomaticUpdate', 'apps.dataDir', 'apps.ts', 'apps.healthTime' ].join(','); var PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(','); @@ -250,16 +250,17 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal const label = data.label || null; const tagsJson = data.tags ? JSON.stringify(data.tags) : null; const mailboxName = data.mailboxName || null; + const mailboxDomain = data.mailboxDomain || null; const reverseProxyConfigJson = data.reverseProxyConfig ? JSON.stringify(data.reverseProxyConfig) : null; var queries = []; queries.push({ query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, ' - + 'sso, debugModeJson, mailboxName, label, tagsJson, reverseProxyConfigJson) ' - + ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', + + 'sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson) ' + + ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', args: [ id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, - sso, debugModeJson, mailboxName, label, tagsJson, reverseProxyConfigJson ] + sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson ] }); queries.push({ diff --git a/src/apps.js b/src/apps.js index 95e1a64b4..890749e30 100644 --- a/src/apps.js +++ b/src/apps.js @@ -380,7 +380,7 @@ function getDataDir(app, dataDir) { function removeInternalFields(app) { return _.pick(app, 'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId', - 'location', 'domain', 'fqdn', 'mailboxName', + 'location', 'domain', 'fqdn', 'mailboxName', 'mailboxDomain', 'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'sso', 'debugMode', 'reverseProxyConfig', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags', 'label', 'alternateDomains', 'env', 'enableAutomaticUpdate', 'dataDir'); @@ -587,7 +587,9 @@ function downloadManifest(appStoreId, manifest, callback) { } function mailboxNameForLocation(location, manifest) { - return (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app'; + if (location) return `${location}.app`; + if (manifest.title) return manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '') + '.app'; + return 'noreply.app'; } function scheduleTask(appId, installationState, taskId, callback) { @@ -690,7 +692,6 @@ function install(data, user, auditSource, callback) { enableAutomaticUpdate = 'enableAutomaticUpdate' in data ? data.enableAutomaticUpdate : true, alternateDomains = data.alternateDomains || [], env = data.env || {}, - mailboxName = data.mailboxName || '', label = data.label || null, tags = data.tags || [], overwriteDns = 'overwriteDns' in data ? data.overwriteDns : false; @@ -731,14 +732,8 @@ function install(data, user, auditSource, callback) { error = validateEnv(env); if (error) return callback(error); - if (mailboxName) { - error = mail.validateName(mailboxName); - if (error) return callback(new BoxError(BoxError.BAD_FIELD, error.message, { field: 'mailboxName' })); - } else { - mailboxName = mailboxNameForLocation(location, manifest); - } - - var appId = uuid.v4(); + const mailboxName = mailboxNameForLocation(location, manifest); + const appId = uuid.v4(); if (icon) { if (!validator.isBase64(icon)) return callback(new BoxError(BoxError.BAD_FIELD, 'icon is not base64', { field: 'icon' })); @@ -765,6 +760,7 @@ function install(data, user, auditSource, callback) { sso: sso, debugMode: debugMode, mailboxName: mailboxName, + mailboxDomain: domain, enableBackup: enableBackup, enableAutomaticUpdate: enableAutomaticUpdate, alternateDomains: alternateDomains, @@ -989,9 +985,10 @@ function setDebugMode(appId, debugMode, auditSource, callback) { }); } -function setMailbox(appId, mailboxName, auditSource, callback) { +function setMailbox(appId, mailboxName, mailboxDomain, auditSource, callback) { assert.strictEqual(typeof appId, 'string'); assert(mailboxName === null || typeof mailboxName === 'string'); + assert.strictEqual(typeof mailboxDomain, 'string'); assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof callback, 'function'); @@ -1001,23 +998,27 @@ function setMailbox(appId, mailboxName, auditSource, callback) { error = checkAppState(app, exports.ISTATE_PENDING_RECREATE_CONTAINER); if (error) return callback(error); - if (mailboxName) { - error = mail.validateName(mailboxName); - if (error) return callback(new BoxError(BoxError.BAD_FIELD, error.message, { field: 'mailboxName' })); - } else { - mailboxName = mailboxNameForLocation(app.location, app.manifest); - } - - const task = { - args: {}, - values: { mailboxName } - }; - addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task, function (error, result) { + mail.getDomain(mailboxDomain, function (error) { if (error) return callback(error); - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: app, mailboxName: mailboxName, taskId: result.taskId }); + if (mailboxName) { + error = mail.validateName(mailboxName); + if (error) return callback(new BoxError(BoxError.BAD_FIELD, error.message, { field: 'mailboxName' })); + } else { + mailboxName = mailboxNameForLocation(app.location, app.manifest); + } - callback(null, { taskId: result.taskId }); + const task = { + args: {}, + values: { mailboxName, mailboxDomain } + }; + addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task, function (error, result) { + if (error) return callback(error); + + eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: app, mailboxName: mailboxName, taskId: result.taskId }); + + callback(null, { taskId: result.taskId }); + }); }); }); } @@ -1501,7 +1502,6 @@ function clone(appId, data, user, auditSource, callback) { domain = data.domain.toLowerCase(), portBindings = data.portBindings || null, backupId = data.backupId, - mailboxName = data.mailboxName || '', overwriteDns = 'overwriteDns' in data ? data.overwriteDns : false; assert.strictEqual(typeof backupId, 'string'); @@ -1526,13 +1526,7 @@ function clone(appId, data, user, auditSource, callback) { error = validatePortBindings(portBindings, manifest); if (error) return callback(error); - if (mailboxName) { - error = mail.validateName(mailboxName); - if (error) return callback(new BoxError(BoxError.BAD_FIELD, error.message, { field: 'mailboxName' })); - } else { - mailboxName = mailboxNameForLocation(location, manifest); - } - + const mailboxName = mailboxNameForLocation(location, manifest); const locations = [{subdomain: location, domain}]; validateLocations(locations, function (error, domainObjectMap) { if (error) return callback(error); @@ -1546,6 +1540,7 @@ function clone(appId, data, user, auditSource, callback) { accessRestriction: app.accessRestriction, sso: !!app.sso, mailboxName: mailboxName, + mailboxDomain: domain, enableBackup: app.enableBackup, reverseProxyConfig: app.reverseProxyConfig, env: app.env, diff --git a/src/routes/apps.js b/src/routes/apps.js index beae0b699..8270e5f46 100644 --- a/src/routes/apps.js +++ b/src/routes/apps.js @@ -295,8 +295,9 @@ function setMailbox(req, res, next) { assert.strictEqual(typeof req.params.id, 'string'); if (req.body.mailboxName !== null && typeof req.body.mailboxName !== 'string') return next(new HttpError(400, 'mailboxName must be a string')); + if (typeof req.body.mailboxDomain !== 'string') return next(new HttpError(400, 'mailboxDomain must be a string')); - apps.setMailbox(req.params.id, req.body.mailboxName, auditSource.fromRequest(req), function (error, result) { + apps.setMailbox(req.params.id, req.body.mailboxName, req.body.mailboxDomain, auditSource.fromRequest(req), function (error, result) { if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(202, { taskId: result.taskId })); diff --git a/src/routes/test/apps-test.js b/src/routes/test/apps-test.js index a5d70a1e6..09e7e96c0 100644 --- a/src/routes/test/apps-test.js +++ b/src/routes/test/apps-test.js @@ -480,6 +480,7 @@ describe('App API', function () { expect(res.body.id).to.eql(APP_ID); expect(res.body.installationState).to.be.ok(); expect(res.body.mailboxName).to.be(APP_LOCATION + '.app'); + expect(res.body.mailboxDomain).to.be(DOMAIN_0.domain); done(); }); }); @@ -1153,6 +1154,7 @@ describe('App API', function () { if (error) return done(error); expect(app.mailboxName).to.be(APP_LOCATION_NEW + '.app'); // must follow location change + expect(app.mailboxDomain).to.be(DOMAIN_0.domain); docker.getContainer(app.containerId).inspect(function (error, data) { expect(error).to.not.be.ok(); @@ -1364,7 +1366,7 @@ describe('App API', function () { it('can set mailbox', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/mailbox') .query({ access_token: token }) - .send({ mailboxName: 'genos' }) + .send({ mailboxName: 'genos', mailboxDomain: DOMAIN_0.domain }) .end(function (err, res) { expect(res.statusCode).to.equal(202); taskId = res.body.taskId; @@ -1392,7 +1394,7 @@ describe('App API', function () { it('can reset mailbox', function (done) { superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/mailbox') .query({ access_token: token }) - .send({ mailboxName: null }) + .send({ mailboxName: null, mailboxDomain: DOMAIN_0.domain }) .end(function (err, res) { expect(res.statusCode).to.equal(202); taskId = res.body.taskId; diff --git a/src/test/apps-test.js b/src/test/apps-test.js index 81e5cec6a..6f90c36c6 100644 --- a/src/test/apps-test.js +++ b/src/test/apps-test.js @@ -115,6 +115,7 @@ describe('Apps', function () { memoryLimit: 0, reverseProxyConfig: null, sso: false, + mailboxDomain: DOMAIN_0.domain, env: { 'CUSTOM_KEY': 'CUSTOM_VALUE' }, @@ -138,6 +139,7 @@ describe('Apps', function () { memoryLimit: 0, env: {}, dataDir: '', + mailboxDomain: DOMAIN_0.domain, installationState: 'installed', runState: 'running' }; @@ -159,6 +161,7 @@ describe('Apps', function () { sso: false, env: {}, dataDir: '', + mailboxDomain: DOMAIN_0.domain, installationState: 'installed', runState: 'running' }; diff --git a/src/test/apptask-test.js b/src/test/apptask-test.js index 1fcf191df..834674de4 100644 --- a/src/test/apptask-test.js +++ b/src/test/apptask-test.js @@ -93,6 +93,7 @@ var APP = { portBindings: null, accessRestriction: null, memoryLimit: 0, + mailboxDomain: DOMAIN_0.domain, alternateDomains: [] }; diff --git a/src/test/database-test.js b/src/test/database-test.js index 3ee1d3ae9..34a2e48ff 100644 --- a/src/test/database-test.js +++ b/src/test/database-test.js @@ -411,6 +411,7 @@ describe('database', function () { enableBackup: true, env: {}, mailboxName: 'talktome', + mailboxDomain: DOMAIN_0.domain, enableAutomaticUpdate: true, dataDir: null, tags: [], @@ -991,6 +992,7 @@ describe('database', function () { 'CUSTOM_KEY': 'CUSTOM_VALUE' }, mailboxName: 'talktome', + mailboxDomain: DOMAIN_0.domain, enableAutomaticUpdate: true, dataDir: null, tags: [], @@ -1020,6 +1022,7 @@ describe('database', function () { alternateDomains: [], env: {}, mailboxName: 'callme', + mailboxDomain: DOMAIN_0.domain, enableAutomaticUpdate: true, dataDir: null, tags: [],