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);