diff --git a/docs/references/api.md b/docs/references/api.md index 512e21fde..a903a9f9d 100644 --- a/docs/references/api.md +++ b/docs/references/api.md @@ -1196,6 +1196,34 @@ Request: } ``` +### Get Catch All Address + +GET `/api/v1/settings/catch_all_address` admin + +Gets the address(es) to which emails addressed to a non-existent mailbox are forwarded to. +Configuring a catch-all address can help avoid losing emails due to misspelling. + +Response(200): +``` +{ + "address": [ ] // array of mailbox names +} +``` + +### Set Catch All Address + +PUT `/api/v1/settings/catch_all_address` admin + +Sets the address(es) to which emails addressed to a non-existent mailbox are forwarded. +Configuring a catch-all address can help avoid losing emails due to misspelling. + +Request: +``` +{ + "address": [ ] // array of mailbox names +} +``` + ### Get DNS Configuration GET `/api/v1/settings/dns_config` admin internal @@ -1221,7 +1249,7 @@ This is currently internal API and is documented here for completeness. ### Get Email Configuration -GET `/api/v1/settings/mail_config` admin internal +GET `/api/v1/settings/mail_config` admin Gets the email configuration. The Cloudron has a built-in email server for users. This configuration can be used to disable the server. Note that the Cloudron will @@ -1236,7 +1264,7 @@ Response(200): ### Set Email Configuration -POST `/api/v1/settings/mail_config` admin internal +POST `/api/v1/settings/mail_config` admin Sets the email configuration. The Cloudron has a built-in email server for users. This configuration can be used to enable or disable the email server. Note that diff --git a/src/platform.js b/src/platform.js index 44088fc4a..7b754f6ac 100644 --- a/src/platform.js +++ b/src/platform.js @@ -239,16 +239,22 @@ function createMailConfig(callback) { const mailFqdn = config.adminFqdn(); const alertsFrom = 'no-reply@' + config.fqdn(); + debug('createMailConfig: generating mail config'); + user.getOwner(function (error, owner) { var alertsTo = config.provider() === 'caas' ? [ 'support@cloudron.io' ] : [ ]; alertsTo.concat(error ? [] : owner.email).join(','); - if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/mail.ini', - `mail_domain=${fqdn}\nmail_server_name=${mailFqdn}\nalerts_from=${alertsFrom}\nalerts_to=${alertsTo}`, 'utf8')) { - return callback(new Error('Could not create mail var file:' + safe.error.message)); - } + settings.getCatchAllAddress(function (error, address) { + var catchAll = address.join(','); - callback(); + if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/mail.ini', + `mail_domain=${fqdn}\nmail_server_name=${mailFqdn}\nalerts_from=${alertsFrom}\nalerts_to=${alertsTo}\ncatch_all=${catchAll}\n`, 'utf8')) { + return callback(new Error('Could not create mail var file:' + safe.error.message)); + } + + callback(); + }); }); } diff --git a/src/routes/settings.js b/src/routes/settings.js index d5950fe3c..05847d9cc 100644 --- a/src/routes/settings.js +++ b/src/routes/settings.js @@ -24,6 +24,9 @@ exports = module.exports = { getMailConfig: getMailConfig, setMailConfig: setMailConfig, + getCatchAllAddress: getCatchAllAddress, + setCatchAllAddress: setCatchAllAddress, + getAppstoreConfig: getAppstoreConfig, setAppstoreConfig: setAppstoreConfig, @@ -125,6 +128,31 @@ function setMailConfig(req, res, next) { }); } +function getCatchAllAddress(req, res, next) { + settings.getCatchAllAddress(function (error, address) { + if (error) return next(new HttpError(500, error)); + + next(new HttpSuccess(200, { address: address })); + }); +} + +function setCatchAllAddress(req, res, next) { + assert.strictEqual(typeof req.body, 'object'); + + if (!req.body.address || !Array.isArray(req.body.address)) return next(new HttpError(400, 'address array is required')); + + for (var i = 0; i < req.body.address.length; i++) { + if (typeof req.body.address[i] !== 'string') return next(new HttpError(400, 'address must be an array of string')); + } + + settings.setCatchAllAddress(req.body.address, function (error) { + if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message)); + if (error) return next(new HttpError(500, error)); + + next(new HttpSuccess(200)); + }); +} + function setCloudronAvatar(req, res, next) { assert.strictEqual(typeof req.files, 'object'); diff --git a/src/routes/test/settings-test.js b/src/routes/test/settings-test.js index 06f629525..75593d9bf 100644 --- a/src/routes/test/settings-test.js +++ b/src/routes/test/settings-test.js @@ -315,6 +315,57 @@ describe('Settings API', function () { }); }); + describe('catch_all', function () { + it('get catch_all succeeds', function (done) { + superagent.get(SERVER_URL + '/api/v1/settings/catch_all_address') + .query({ access_token: token }) + .end(function (err, res) { + expect(res.statusCode).to.equal(200); + expect(res.body).to.eql({ address: [ ] }); + done(); + }); + }); + + it('cannot set without address field', function (done) { + superagent.put(SERVER_URL + '/api/v1/settings/catch_all_address') + .query({ access_token: token }) + .end(function (err, res) { + expect(res.statusCode).to.equal(400); + done(); + }); + }); + + it('cannot set with bad address field', function (done) { + superagent.put(SERVER_URL + '/api/v1/settings/catch_all_address') + .query({ access_token: token }) + .send({ address: [ "user1", 123 ] }) + .end(function (err, res) { + expect(res.statusCode).to.equal(400); + done(); + }); + }); + + it('set succeeds', function (done) { + superagent.put(SERVER_URL + '/api/v1/settings/catch_all_address') + .query({ access_token: token }) + .send({ address: [ "user1" ] }) + .end(function (err, res) { + expect(res.statusCode).to.equal(200); + done(); + }); + }); + + it('get succeeds', function (done) { + superagent.get(SERVER_URL + '/api/v1/settings/catch_all_address') + .query({ access_token: token }) + .end(function (err, res) { + expect(res.statusCode).to.equal(200); + expect(res.body).to.eql({ address: [ "user1" ] }); + done(); + }); + }); + }); + describe('Certificates API', function () { var validCert0, validKey0, // foobar.com validCert1, validKey1; // *.foobar.com diff --git a/src/server.js b/src/server.js index 6205fe7e3..57562563f 100644 --- a/src/server.js +++ b/src/server.js @@ -211,6 +211,8 @@ function initializeExpressSync() { router.post('/api/v1/settings/appstore_config', settingsScope, routes.user.requireAdmin, routes.settings.setAppstoreConfig); router.get ('/api/v1/settings/mail_config', settingsScope, routes.user.requireAdmin, routes.settings.getMailConfig); router.post('/api/v1/settings/mail_config', settingsScope, routes.user.requireAdmin, routes.settings.setMailConfig); + router.get ('/api/v1/settings/catch_all_address', settingsScope, routes.user.requireAdmin, routes.settings.getCatchAllAddress); + router.put ('/api/v1/settings/catch_all_address', settingsScope, routes.user.requireAdmin, routes.settings.setCatchAllAddress); // feedback router.post('/api/v1/feedback', usersScope, routes.cloudron.feedback); diff --git a/src/settings.js b/src/settings.js index 5a7ccc6fe..5851311d8 100644 --- a/src/settings.js +++ b/src/settings.js @@ -44,6 +44,9 @@ exports = module.exports = { getMailConfig: getMailConfig, setMailConfig: setMailConfig, + setCatchAllAddress: setCatchAllAddress, + getCatchAllAddress: getCatchAllAddress, + getDefaultSync: getDefaultSync, getAll: getAll, @@ -58,6 +61,7 @@ exports = module.exports = { UPDATE_CONFIG_KEY: 'update_config', APPSTORE_CONFIG_KEY: 'appstore_config', MAIL_CONFIG_KEY: 'mail_config', + CATCH_ALL_ADDRESS: 'catch_all_address', events: null }; @@ -77,6 +81,7 @@ var assert = require('assert'), moment = require('moment-timezone'), net = require('net'), paths = require('./paths.js'), + platform = require('./platform.js'), safe = require('safetydance'), settingsdb = require('./settingsdb.js'), subdomains = require('./subdomains.js'), @@ -104,6 +109,7 @@ var gDefaults = (function () { result[exports.UPDATE_CONFIG_KEY] = { prerelease: false }; result[exports.APPSTORE_CONFIG_KEY] = {}; result[exports.MAIL_CONFIG_KEY] = { enabled: false }; + result[exports.CATCH_ALL_ADDRESS] = [ ]; return result; })(); @@ -653,6 +659,32 @@ function setMailConfig(mailConfig, callback) { }); } +function getCatchAllAddress(callback) { + assert.strictEqual(typeof callback, 'function'); + + settingsdb.get(exports.CATCH_ALL_ADDRESS, function (error, value) { + if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.CATCH_ALL_ADDRESS]); + if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error)); + + callback(null, JSON.parse(value)); + }); +} + +function setCatchAllAddress(address, callback) { + assert(Array.isArray(address)); + assert.strictEqual(typeof callback, 'function'); + + settingsdb.set(exports.CATCH_ALL_ADDRESS, JSON.stringify(address), function (error) { + if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error)); + + exports.events.emit(exports.CATCH_ALL_ADDRESS, address); + + platform.createMailConfig(NOOP_CALLBACK); + + callback(null); + }); +} + function getAppstoreConfig(callback) { assert.strictEqual(typeof callback, 'function'); diff --git a/src/test/settings-test.js b/src/test/settings-test.js index 7a350d074..99005c987 100644 --- a/src/test/settings-test.js +++ b/src/test/settings-test.js @@ -181,6 +181,26 @@ describe('Settings', function () { }); }); + it('can get catch all address', function (done) { + settings.getCatchAllAddress(function (error, address) { + expect(error).to.be(null); + expect(address).to.eql([ ]); + done(); + }); + }); + + it('can set catch all address', function (done) { + settings.setCatchAllAddress([ "user1", "user2" ], function (error) { + expect(error).to.be(null); + + settings.getCatchAllAddress(function (error, address) { + expect(error).to.be(null); + expect(address).to.eql([ "user1", "user2" ]); + done(); + }); + }); + }); + it('can get all values', function (done) { settings.getAll(function (error, allSettings) { expect(error).to.be(null);