diff --git a/CHANGES b/CHANGES index 56ea4054c..e831d3d6c 100644 --- a/CHANGES +++ b/CHANGES @@ -2353,4 +2353,5 @@ * Each app can now have a custom crontab * services: add recovery mode * postgresql: fix restore issue with long table names +* recvmail: make the addon work again diff --git a/migrations/20211001160903-apps-add-inbox.js b/migrations/20211001160903-apps-add-inbox.js new file mode 100644 index 000000000..a9a863152 --- /dev/null +++ b/migrations/20211001160903-apps-add-inbox.js @@ -0,0 +1,19 @@ +'use strict'; + +var async = require('async'); + +exports.up = function(db, callback) { + async.series([ + db.runSql.bind(db, 'ALTER TABLE apps ADD COLUMN enableInbox BOOLEAN DEFAULT 0'), + db.runSql.bind(db, 'ALTER TABLE apps ADD COLUMN inboxName VARCHAR(128)'), + db.runSql.bind(db, 'ALTER TABLE apps ADD COLUMN inboxDomain VARCHAR(128)'), + ], callback); +}; + +exports.down = function(db, callback) { + async.series([ + db.runSql.bind(db, 'ALTER TABLE apps DROP COLUMN enableInbox'), + db.runSql.bind(db, 'ALTER TABLE apps DROP COLUMN inboxName'), + db.runSql.bind(db, 'ALTER TABLE apps DROP COLUMN inboxDomain'), + ], callback); +}; diff --git a/migrations/schema.sql b/migrations/schema.sql index 1c240110f..cb0e19a69 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -86,6 +86,9 @@ CREATE TABLE IF NOT EXISTS apps( enableMailbox BOOLEAN DEFAULT 1, // whether sendmail addon is enabled mailboxName VARCHAR(128), // mailbox of this app mailboxDomain VARCHAR(128), // mailbox domain of this apps + enableInbox BOOLEAN DEFAULT 0, // whether recvmail addon is enabled + inboxName VARCHAR(128), // mailbox of this app + inboxDomain VARCHAR(128), // mailbox domain of this apps label VARCHAR(128), // display name tagsJson VARCHAR(2048), // array of tags dataDir VARCHAR(256) UNIQUE, diff --git a/src/apps.js b/src/apps.js index 3cab65020..27d1adfd7 100644 --- a/src/apps.js +++ b/src/apps.js @@ -40,6 +40,7 @@ exports = module.exports = { setDebugMode, setEnvironment, setMailbox, + setInbox, setLocation, setDataDir, repair, @@ -180,7 +181,8 @@ const APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationS 'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.accessRestrictionJson', 'apps.memoryLimit', 'apps.cpuShares', 'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.servicesConfigJson', 'apps.operatorsJson', 'apps.sso', 'apps.debugModeJson', 'apps.enableBackup', 'apps.proxyAuth', 'apps.containerIp', 'apps.crontab', - 'apps.creationTime', 'apps.updateTime', 'apps.enableMailbox', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableAutomaticUpdate', + 'apps.creationTime', 'apps.updateTime', 'apps.enableAutomaticUpdate', + 'apps.enableMailbox', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableInbox', 'apps.inboxName', 'apps.inboxDomain', 'apps.dataDir', 'apps.ts', 'apps.healthTime', '(apps.icon IS NOT NULL) AS hasIcon', '(apps.appStoreIcon IS NOT NULL) AS hasAppStoreIcon' ].join(','); // const PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(','); @@ -494,10 +496,11 @@ function getDataDir(app, dataDir) { function removeInternalFields(app) { return _.pick(app, 'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId', - 'location', 'domain', 'fqdn', 'mailboxName', 'mailboxDomain', 'crontab', + 'location', 'domain', 'fqdn', 'crontab', 'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'cpuShares', 'operators', 'sso', 'debugMode', 'reverseProxyConfig', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags', - 'label', 'alternateDomains', 'aliasDomains', 'env', 'enableAutomaticUpdate', 'dataDir', 'mounts', 'enableMailbox'); + 'label', 'alternateDomains', 'aliasDomains', 'env', 'enableAutomaticUpdate', 'dataDir', 'mounts', + 'enableMailbox', 'mailboxName', 'mailboxDomain', 'enableInbox', 'inboxName', 'inboxDomain'); } // non-admins can only see these @@ -586,6 +589,7 @@ function postProcess(result) { result.enableBackup = !!result.enableBackup; result.enableAutomaticUpdate = !!result.enableAutomaticUpdate; result.enableMailbox = !!result.enableMailbox; + result.enableInbox = !!result.enableInbox; result.proxyAuth = !!result.proxyAuth; result.hasIcon = !!result.hasIcon; result.hasAppStoreIcon = !!result.hasAppStoreIcon; @@ -702,30 +706,31 @@ async function add(id, appStoreId, manifest, location, domain, portBindings, dat portBindings = portBindings || { }; - const manifestJson = JSON.stringify(manifest); - const accessRestriction = data.accessRestriction || null; - const accessRestrictionJson = JSON.stringify(accessRestriction); - const memoryLimit = data.memoryLimit || 0; - const cpuShares = data.cpuShares || 512; - const installationState = data.installationState; - const runState = data.runState; - const sso = 'sso' in data ? data.sso : null; - const debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null; - const env = data.env || {}; - 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; - const servicesConfigJson = data.servicesConfig ? JSON.stringify(data.servicesConfig) : null; - const enableMailbox = data.enableMailbox || false; - const icon = data.icon || null; + const manifestJson = JSON.stringify(manifest), + accessRestriction = data.accessRestriction || null, + accessRestrictionJson = JSON.stringify(accessRestriction), + memoryLimit = data.memoryLimit || 0, + cpuShares = data.cpuShares || 512, + installationState = data.installationState, + runState = data.runState, + sso = 'sso' in data ? data.sso : null, + debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null, + env = data.env || {}, + label = data.label || null, + tagsJson = data.tags ? JSON.stringify(data.tags) : null, + mailboxName = data.mailboxName || null, + mailboxDomain = data.mailboxDomain || null, + reverseProxyConfigJson = data.reverseProxyConfig ? JSON.stringify(data.reverseProxyConfig) : null, + servicesConfigJson = data.servicesConfig ? JSON.stringify(data.servicesConfig) : null, + enableMailbox = data.enableMailbox || false, + icon = data.icon || null; - let queries = []; + const queries = []; queries.push({ query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, cpuShares, ' - + 'sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson, servicesConfigJson, icon, enableMailbox) ' + + 'sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson, servicesConfigJson, icon, ' + + 'enableMailbox) ' + ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', args: [ id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, cpuShares, sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson, servicesConfigJson, icon, enableMailbox ] @@ -1178,7 +1183,6 @@ async function install(data, auditSource) { overwriteDns = 'overwriteDns' in data ? data.overwriteDns : false, skipDnsSetup = 'skipDnsSetup' in data ? data.skipDnsSetup : false, appStoreId = data.appStoreId, - enableMailbox = 'enabledMailbox' in data ? data.enableMailbox : true, manifest = data.manifest; let error = manifestFormat.parse(manifest); @@ -1216,6 +1220,7 @@ async function install(data, auditSource) { if (settings.isDemo() && constants.DEMO_BLACKLISTED_APPS.includes(appStoreId)) throw new BoxError(BoxError.BAD_FIELD, 'This app is blacklisted in the demo'); // sendmail is enabled by default + const enableMailbox = 'enableMailbox' in data ? data.enableMailbox : true; const mailboxName = manifest.addons?.sendmail ? mailboxNameForLocation(location, manifest) : null; const mailboxDomain = manifest.addons?.sendmail ? domain : null; @@ -1477,8 +1482,9 @@ async function setMailbox(app, data, auditSource) { assert.strictEqual(typeof data, 'object'); assert.strictEqual(typeof auditSource, 'object'); - const { enable, mailboxDomain } = data; - assert.strictEqual(typeof enable, 'boolean'); + assert.strictEqual(typeof data.enable, 'boolean'); + + const enableMailbox = data.enable; const appId = app.id; let error = checkAppState(app, exports.ISTATE_PENDING_RECREATE_CONTAINER); @@ -1486,25 +1492,64 @@ async function setMailbox(app, data, auditSource) { if (!app.manifest.addons?.sendmail) throw new BoxError(BoxError.BAD_FIELD, 'App does not use sendmail'); const optional = 'optional' in app.manifest.addons.sendmail ? app.manifest.addons.sendmail.optional : false; - if (!optional && !enable) throw new BoxError(BoxError.BAD_FIELD, 'App requires sendmail to be enabled'); + if (!optional && !enableMailbox) throw new BoxError(BoxError.BAD_FIELD, 'App requires sendmail to be enabled'); - await mail.getDomain(mailboxDomain); // check if domain exists + let mailboxName = data.mailboxName || null; + const mailboxDomain = data.mailboxDomain || null; - let mailboxName = data.mailboxName; - if (mailboxName) { - error = mail.validateName(mailboxName); - if (error) throw new BoxError(BoxError.BAD_FIELD, error.message, { field: 'mailboxName' }); - } else { - mailboxName = mailboxNameForLocation(app.location, app.domain, app.manifest); + if (enableMailbox) { + await mail.getDomain(mailboxDomain); // check if domain exists + + if (mailboxName) { + error = mail.validateName(mailboxName); + if (error) throw new BoxError(BoxError.BAD_FIELD, error.message, { field: 'mailboxName' }); + } else { + mailboxName = mailboxNameForLocation(app.location, app.domain, app.manifest); + } } const task = { args: {}, - values: { enableMailbox: enable, mailboxName, mailboxDomain } + values: { enableMailbox, mailboxName, mailboxDomain } }; const taskId = await addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task); - await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, mailboxName, taskId }); + await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, mailboxName, mailboxDomain, taskId }); + + return { taskId }; +} + +async function setInbox(app, data, auditSource) { + assert.strictEqual(typeof app, 'object'); + assert.strictEqual(typeof data, 'object'); + assert.strictEqual(typeof auditSource, 'object'); + + assert.strictEqual(typeof data.enable, 'boolean'); + + const enableInbox = data.enable; + const appId = app.id; + let error = checkAppState(app, exports.ISTATE_PENDING_RECREATE_CONTAINER); + if (error) throw error; + + if (!app.manifest.addons?.recvmail) throw new BoxError(BoxError.BAD_FIELD, 'App does not use recvmail addon'); + + const inboxName = data.inboxName || null; + const inboxDomain = data.inboxDomain || null; + if (enableInbox) { + const domain = await mail.getDomain(data.inboxDomain); // check if domain exists + if (!domain.enabled) throw new BoxError(BoxError.BAD_FIELD, 'Domain does not have incoming email enabled'); + + error = mail.validateName(data.inboxName); + if (error) throw new BoxError(BoxError.BAD_FIELD, error.message, { field: 'inboxName' }); + } + + const task = { + args: {}, + values: { enableInbox, inboxName, inboxDomain } + }; + const taskId = await addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task); + + await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, enableInbox, inboxName, inboxDomain, taskId }); return { taskId }; } @@ -1826,7 +1871,7 @@ async function repair(app, data, auditSource) { error = checkManifestConstraints(data.manifest); if (error) throw error; - if (!hasMailAddon(data.manifest)) { // clear if repair removed addon + if (!data.manifest.addons?.sendmail) { // clear if repair removed addon task.values.mailboxName = task.values.mailboxDomain = null; } else if (!app.mailboxName || app.mailboxName.endsWith('.app')) { // allocate since repair added the addon task.values.mailboxName = mailboxNameForLocation(app.location, data.manifest); diff --git a/src/routes/apps.js b/src/routes/apps.js index b37cf308a..a6e803c27 100644 --- a/src/routes/apps.js +++ b/src/routes/apps.js @@ -33,6 +33,7 @@ exports = module.exports = { setDebugMode, setEnvironment, setMailbox, + setInbox, setLocation, setDataDir, setMounts, @@ -369,6 +370,22 @@ async function setMailbox(req, res, next) { next(new HttpSuccess(202, { taskId: result.taskId })); } +async function setInbox(req, res, next) { + assert.strictEqual(typeof req.body, 'object'); + assert.strictEqual(typeof req.app, 'object'); + + if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enable must be a boolean')); + if (req.body.enable) { + if (typeof req.body.inboxName !== 'string') return next(new HttpError(400, 'inboxName must be a string')); + if (typeof req.body.inboxDomain !== 'string') return next(new HttpError(400, 'inboxDomain must be a string')); + } + + const [error, result] = await safe(apps.setInbox(req.app, req.body, AuditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); + + next(new HttpSuccess(202, { taskId: result.taskId })); +} + async function setLocation(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.app, 'object'); @@ -621,11 +638,11 @@ function getLogStream(req, res, next) { async function getLogs(req, res, next) { assert.strictEqual(typeof req.app, 'object'); - var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; + const lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number')); - var options = { - lines: lines, + const options = { + lines, follow: false, format: req.query.format || 'json' }; diff --git a/src/server.js b/src/server.js index 873ece7f4..0911e3793 100644 --- a/src/server.js +++ b/src/server.js @@ -216,6 +216,7 @@ function initializeExpressSync() { router.post('/api/v1/apps/:id/configure/cert', json, token, routes.apps.load, authorizeOperator, routes.apps.setCertificate); router.post('/api/v1/apps/:id/configure/debug_mode', json, token, routes.apps.load, authorizeOperator, routes.apps.setDebugMode); router.post('/api/v1/apps/:id/configure/mailbox', json, token, routes.apps.load, authorizeAdmin, routes.apps.setMailbox); + router.post('/api/v1/apps/:id/configure/inbox', json, token, routes.apps.load, authorizeAdmin, routes.apps.setInbox); router.post('/api/v1/apps/:id/configure/env', json, token, routes.apps.load, authorizeOperator, routes.apps.setEnvironment); router.post('/api/v1/apps/:id/configure/data_dir', json, token, routes.apps.load, authorizeAdmin, routes.apps.setDataDir); router.post('/api/v1/apps/:id/configure/location', json, token, routes.apps.load, authorizeAdmin, routes.apps.setLocation); diff --git a/src/services.js b/src/services.js index a644a7bf5..f5fca36d8 100644 --- a/src/services.js +++ b/src/services.js @@ -1063,7 +1063,9 @@ async function setupRecvMail(app, options) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); - debug('Setting up recvmail'); + debug('setupRecvMail: setting up recvmail'); + + if (!app.enableInbox) return await addonConfigs.set(app.id, 'recvmail', []); const existingPassword = await addonConfigs.getByName(app.id, 'recvmail', '%MAIL_IMAP_PASSWORD'); @@ -1074,13 +1076,13 @@ async function setupRecvMail(app, options) { const env = [ { name: `${envPrefix}MAIL_IMAP_SERVER`, value: 'mail' }, { name: `${envPrefix}MAIL_IMAP_PORT`, value: '9993' }, - { name: `${envPrefix}MAIL_IMAP_USERNAME`, value: app.mailboxName + '@' + app.mailboxDomain }, + { name: `${envPrefix}MAIL_IMAP_USERNAME`, value: app.inboxName + '@' + app.inboxDomain }, { name: `${envPrefix}MAIL_IMAP_PASSWORD`, value: password }, - { name: `${envPrefix}MAIL_TO`, value: app.mailboxName + '@' + app.mailboxDomain }, - { name: `${envPrefix}MAIL_DOMAIN`, value: app.mailboxDomain } + { name: `${envPrefix}MAIL_TO`, value: app.inboxName + '@' + app.inboxDomain }, + { name: `${envPrefix}MAIL_TO_DOMAIN`, value: app.inboxDomain }, ]; - debug('Setting sendmail addon config to %j', env); + debug('setupRecvMail: setting recvmail addon config to %j', env); await addonConfigs.set(app.id, 'recvmail', env); } @@ -1088,7 +1090,7 @@ async function teardownRecvMail(app, options) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof options, 'object'); - debug('Tearing down recvmail'); + debug('teardownRecvMail: tearing down recvmail'); await addonConfigs.unset(app.id, 'recvmail'); }