Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 44fb39c810 | |||
| 58627a33c6 | |||
| dd8ec75b1c | |||
| 59c3525bc2 | |||
| 2da7f1aca6 | |||
| 3f90fe4c5a | |||
| 52532900b6 | |||
| d1a597a9f7 | |||
| 0af86436cd | |||
| 8b4e233780 | |||
| df4a2ab9e6 |
@@ -888,11 +888,9 @@
|
||||
[1.0.0]
|
||||
* Make selfhosting great again
|
||||
|
||||
[1.1.0]
|
||||
* Add support for email catch-all
|
||||
* Support Cloudrons on subdomains
|
||||
[1.0.1]
|
||||
* Notification improvements
|
||||
|
||||
[1.2.0]
|
||||
* Relay emails optionally via external SMTP server email (mailgun, sendgrid etc)
|
||||
* (experimental) Preserver the docker storage driver across updates
|
||||
[1.0.2]
|
||||
* Notification improvements
|
||||
|
||||
|
||||
+2
-30
@@ -1196,34 +1196,6 @@ Request:
|
||||
}
|
||||
```
|
||||
|
||||
### Get Catch All Address
|
||||
|
||||
GET `/api/v1/settings/catch_all_address` <scope>admin</scope>
|
||||
|
||||
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": [ <string> ] // array of mailbox names
|
||||
}
|
||||
```
|
||||
|
||||
### Set Catch All Address
|
||||
|
||||
PUT `/api/v1/settings/catch_all_address` <scope>admin</scope>
|
||||
|
||||
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": [ <string> ] // array of mailbox names
|
||||
}
|
||||
```
|
||||
|
||||
### Get DNS Configuration
|
||||
|
||||
GET `/api/v1/settings/dns_config` <scope>admin</scope> <scope>internal</scope>
|
||||
@@ -1249,7 +1221,7 @@ This is currently internal API and is documented here for completeness.
|
||||
|
||||
### Get Email Configuration
|
||||
|
||||
GET `/api/v1/settings/mail_config` <scope>admin</scope>
|
||||
GET `/api/v1/settings/mail_config` <scope>admin</scope> <scope>internal</scope>
|
||||
|
||||
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
|
||||
@@ -1264,7 +1236,7 @@ Response(200):
|
||||
|
||||
### Set Email Configuration
|
||||
|
||||
POST `/api/v1/settings/mail_config` <scope>admin</scope>
|
||||
POST `/api/v1/settings/mail_config` <scope>admin</scope> <scope>internal</scope>
|
||||
|
||||
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
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('INSERT settings (name, value) VALUES("mail_relay", ?)', [ JSON.stringify({ provider: 'cloudron-smtp' }) ], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('DELETE * FROM settings WHERE name="mail_relay"', [ ], callback);
|
||||
};
|
||||
Generated
+55
-35
@@ -29,6 +29,11 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.11.tgz",
|
||||
"dev": true
|
||||
},
|
||||
"addressparser": {
|
||||
"version": "0.3.2",
|
||||
"from": "addressparser@>=0.3.2 <0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/addressparser/-/addressparser-0.3.2.tgz"
|
||||
},
|
||||
"ajv": {
|
||||
"version": "4.11.7",
|
||||
"from": "ajv@>=4.9.1 <5.0.0",
|
||||
@@ -326,6 +331,18 @@
|
||||
"from": "buffer-shims@>=1.0.0 <1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz"
|
||||
},
|
||||
"buildmail": {
|
||||
"version": "2.0.0",
|
||||
"from": "buildmail@>=2.0.0 <3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/buildmail/-/buildmail-2.0.0.tgz",
|
||||
"dependencies": {
|
||||
"needle": {
|
||||
"version": "0.10.0",
|
||||
"from": "needle@>=0.10.0 <0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/needle/-/needle-0.10.0.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"builtin-modules": {
|
||||
"version": "1.1.1",
|
||||
"from": "builtin-modules@^1.0.0",
|
||||
@@ -1946,23 +1963,6 @@
|
||||
"from": "http-signature@>=1.1.0 <1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz"
|
||||
},
|
||||
"httpntlm": {
|
||||
"version": "1.6.1",
|
||||
"from": "httpntlm@1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/httpntlm/-/httpntlm-1.6.1.tgz",
|
||||
"dependencies": {
|
||||
"underscore": {
|
||||
"version": "1.7.0",
|
||||
"from": "underscore@>=1.7.0 <1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"httpreq": {
|
||||
"version": "0.4.23",
|
||||
"from": "httpreq@>=0.4.22",
|
||||
"resolved": "https://registry.npmjs.org/httpreq/-/httpreq-0.4.23.tgz"
|
||||
},
|
||||
"i": {
|
||||
"version": "0.3.5",
|
||||
"from": "i@>=0.3.0 <0.4.0",
|
||||
@@ -2407,6 +2407,21 @@
|
||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
|
||||
"dev": true
|
||||
},
|
||||
"libbase64": {
|
||||
"version": "0.1.0",
|
||||
"from": "libbase64@>=0.1.0 <0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/libbase64/-/libbase64-0.1.0.tgz"
|
||||
},
|
||||
"libmime": {
|
||||
"version": "1.2.0",
|
||||
"from": "libmime@>=1.2.0 <2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/libmime/-/libmime-1.2.0.tgz"
|
||||
},
|
||||
"libqp": {
|
||||
"version": "1.1.0",
|
||||
"from": "libqp@>=1.1.0 <2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/libqp/-/libqp-1.1.0.tgz"
|
||||
},
|
||||
"liftoff": {
|
||||
"version": "2.3.0",
|
||||
"from": "liftoff@>=2.1.0 <3.0.0",
|
||||
@@ -2611,6 +2626,11 @@
|
||||
"resolved": "https://registry.npmjs.org/macaddress/-/macaddress-0.2.8.tgz",
|
||||
"dev": true
|
||||
},
|
||||
"mailcomposer": {
|
||||
"version": "2.1.0",
|
||||
"from": "mailcomposer@>=2.1.0 <3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mailcomposer/-/mailcomposer-2.1.0.tgz"
|
||||
},
|
||||
"map-cache": {
|
||||
"version": "0.2.2",
|
||||
"from": "map-cache@>=0.2.0 <0.3.0",
|
||||
@@ -2840,6 +2860,11 @@
|
||||
"from": "ncp@>=1.0.0 <1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ncp/-/ncp-1.0.1.tgz"
|
||||
},
|
||||
"needle": {
|
||||
"version": "0.11.0",
|
||||
"from": "needle@>=0.11.0 <0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/needle/-/needle-0.11.0.tgz"
|
||||
},
|
||||
"negotiator": {
|
||||
"version": "0.6.1",
|
||||
"from": "negotiator@0.6.1",
|
||||
@@ -2908,28 +2933,23 @@
|
||||
"resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz"
|
||||
},
|
||||
"nodemailer": {
|
||||
"version": "4.0.1",
|
||||
"from": "nodemailer@latest",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-4.0.1.tgz"
|
||||
"version": "1.11.0",
|
||||
"from": "nodemailer@>=1.3.0 <2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-1.11.0.tgz"
|
||||
},
|
||||
"nodemailer-fetch": {
|
||||
"version": "1.6.0",
|
||||
"from": "nodemailer-fetch@1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer-fetch/-/nodemailer-fetch-1.6.0.tgz"
|
||||
},
|
||||
"nodemailer-shared": {
|
||||
"nodemailer-direct-transport": {
|
||||
"version": "1.1.0",
|
||||
"from": "nodemailer-shared@1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer-shared/-/nodemailer-shared-1.1.0.tgz"
|
||||
"from": "nodemailer-direct-transport@>=1.1.0 <2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer-direct-transport/-/nodemailer-direct-transport-1.1.0.tgz"
|
||||
},
|
||||
"nodemailer-smtp-transport": {
|
||||
"version": "2.7.4",
|
||||
"from": "nodemailer-smtp-transport@latest",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer-smtp-transport/-/nodemailer-smtp-transport-2.7.4.tgz"
|
||||
"version": "1.1.0",
|
||||
"from": "nodemailer-smtp-transport@>=1.0.3 <2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer-smtp-transport/-/nodemailer-smtp-transport-1.1.0.tgz"
|
||||
},
|
||||
"nodemailer-wellknown": {
|
||||
"version": "0.1.10",
|
||||
"from": "nodemailer-wellknown@0.1.10",
|
||||
"from": "nodemailer-wellknown@>=0.1.7 <0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer-wellknown/-/nodemailer-wellknown-0.1.10.tgz"
|
||||
},
|
||||
"nopt": {
|
||||
@@ -4180,9 +4200,9 @@
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz"
|
||||
},
|
||||
"smtp-connection": {
|
||||
"version": "2.12.0",
|
||||
"from": "smtp-connection@2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-2.12.0.tgz"
|
||||
"version": "1.3.8",
|
||||
"from": "smtp-connection@>=1.3.1 <2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-1.3.8.tgz"
|
||||
},
|
||||
"sntp": {
|
||||
"version": "1.0.9",
|
||||
|
||||
+2
-2
@@ -44,8 +44,8 @@
|
||||
"multiparty": "^4.1.2",
|
||||
"mysql": "^2.7.0",
|
||||
"node-uuid": "^1.4.3",
|
||||
"nodemailer": "^4.0.1",
|
||||
"nodemailer-smtp-transport": "^2.7.4",
|
||||
"nodemailer": "^1.3.0",
|
||||
"nodemailer-smtp-transport": "^1.0.3",
|
||||
"oauth2orize": "^1.0.1",
|
||||
"once": "^1.3.2",
|
||||
"parse-links": "^0.1.0",
|
||||
|
||||
@@ -282,6 +282,5 @@ fi
|
||||
|
||||
if [[ "${rebootServer}" == "true" ]]; then
|
||||
echo -e "\n\nRebooting this server now to let bootloader changes take effect.\n"
|
||||
systemctl stop mysql # sometimes mysql ends up having corrupt privilege tables
|
||||
systemctl reboot
|
||||
fi
|
||||
|
||||
@@ -58,7 +58,7 @@ else
|
||||
fi
|
||||
|
||||
echo "Building webadmin assets"
|
||||
(cd "${bundle_dir}" && ./node_modules/.bin/gulp)
|
||||
(cd "${bundle_dir}" && gulp)
|
||||
|
||||
echo "Remove intermediate files required at build-time only"
|
||||
rm -rf "${bundle_dir}/node_modules/"
|
||||
@@ -84,3 +84,4 @@ echo "Cleaning up ${bundle_dir}"
|
||||
rm -rf "${bundle_dir}"
|
||||
|
||||
echo "Tarball saved at ${bundle_file}"
|
||||
|
||||
|
||||
+1
-5
@@ -40,13 +40,9 @@ systemctl enable apparmor
|
||||
systemctl restart apparmor
|
||||
|
||||
usermod ${USER} -a -G docker
|
||||
# preserve the existing storage driver (user might be using overlay2)
|
||||
storage_driver=$(docker info | grep "Storage Driver" | sed 's/.*: //')
|
||||
[[ -n "${storage_driver}" ]] || storage_driver="devicemapper" # if the above command fails
|
||||
|
||||
temp_file=$(mktemp)
|
||||
# create systemd drop-in. some apps do not work with aufs
|
||||
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=${storage_driver}" > "${temp_file}"
|
||||
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=devicemapper" > "${temp_file}"
|
||||
|
||||
systemctl enable docker
|
||||
# restart docker if options changed
|
||||
|
||||
+43
-36
@@ -17,6 +17,7 @@ exports = module.exports = {
|
||||
var assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:appstore'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
os = require('os'),
|
||||
settings = require('./settings.js'),
|
||||
superagent = require('superagent'),
|
||||
@@ -106,7 +107,6 @@ function purchase(appId, appstoreId, callback) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
|
||||
if (result.statusCode === 402) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED, result.body.message));
|
||||
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
|
||||
|
||||
callback(null);
|
||||
@@ -149,45 +149,52 @@ function sendAliveStatus(data, callback) {
|
||||
settings.getAll(function (error, result) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
|
||||
var backendSettings = {
|
||||
dnsConfig: {
|
||||
provider: result[settings.DNS_CONFIG_KEY].provider,
|
||||
wildcard: result[settings.DNS_CONFIG_KEY].provider === 'manual' ? result[settings.DNS_CONFIG_KEY].wildcard : undefined
|
||||
},
|
||||
tlsConfig: {
|
||||
provider: result[settings.TLS_CONFIG_KEY].provider
|
||||
},
|
||||
backupConfig: {
|
||||
provider: result[settings.BACKUP_CONFIG_KEY].provider
|
||||
},
|
||||
mailConfig: {
|
||||
enabled: result[settings.MAIL_CONFIG_KEY].enabled
|
||||
},
|
||||
autoupdatePattern: result[settings.AUTOUPDATE_PATTERN_KEY],
|
||||
timeZone: result[settings.TIME_ZONE_KEY]
|
||||
};
|
||||
eventlog.getAllPaged(eventlog.ACTION_USER_LOGIN, null, 1, 1, function (error, loginEvents) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
|
||||
var data = {
|
||||
domain: config.fqdn(),
|
||||
version: config.version(),
|
||||
provider: config.provider(),
|
||||
backendSettings: backendSettings,
|
||||
machine: {
|
||||
cpus: os.cpus(),
|
||||
totalmem: os.totalmem()
|
||||
}
|
||||
};
|
||||
var backendSettings = {
|
||||
dnsConfig: {
|
||||
provider: result[settings.DNS_CONFIG_KEY].provider,
|
||||
wildcard: result[settings.DNS_CONFIG_KEY].provider === 'manual' ? result[settings.DNS_CONFIG_KEY].wildcard : undefined
|
||||
},
|
||||
tlsConfig: {
|
||||
provider: result[settings.TLS_CONFIG_KEY].provider
|
||||
},
|
||||
backupConfig: {
|
||||
provider: result[settings.BACKUP_CONFIG_KEY].provider
|
||||
},
|
||||
mailConfig: {
|
||||
enabled: result[settings.MAIL_CONFIG_KEY].enabled
|
||||
},
|
||||
autoupdatePattern: result[settings.AUTOUPDATE_PATTERN_KEY],
|
||||
timeZone: result[settings.TIME_ZONE_KEY],
|
||||
};
|
||||
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
if (error) return callback(error);
|
||||
var data = {
|
||||
domain: config.fqdn(),
|
||||
version: config.version(),
|
||||
provider: config.provider(),
|
||||
backendSettings: backendSettings,
|
||||
machine: {
|
||||
cpus: os.cpus(),
|
||||
totalmem: os.totalmem()
|
||||
},
|
||||
events: {
|
||||
lastLogin: loginEvents[0] ? (new Date(loginEvents[0].creationTime).getTime()) : 0
|
||||
}
|
||||
};
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/alive';
|
||||
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
|
||||
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Sending alive status failed. %s %j', result.status, result.body)));
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null);
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/alive';
|
||||
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
|
||||
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Sending alive status failed. %s %j', result.status, result.body)));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+5
-12
@@ -58,7 +58,6 @@ var appdb = require('./appdb.js'),
|
||||
subdomains = require('./subdomains.js'),
|
||||
superagent = require('superagent'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
tld = require('tldjs'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
updateChecker = require('./updatechecker.js'),
|
||||
user = require('./user.js'),
|
||||
@@ -166,24 +165,18 @@ function onDomainConfigured(callback) {
|
||||
], callback);
|
||||
}
|
||||
|
||||
function dnsSetup(dnsConfig, domain, zoneName, callback) {
|
||||
function dnsSetup(dnsConfig, domain, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (config.fqdn()) return callback(new CloudronError(CloudronError.ALREADY_SETUP));
|
||||
|
||||
if (!zoneName) zoneName = tld.getDomain(domain) || '';
|
||||
|
||||
debug('dnsSetup: Setting up Cloudron with domain %s and zone %s', domain, zoneName);
|
||||
|
||||
settings.setDnsConfig(dnsConfig, domain, zoneName, function (error) {
|
||||
settings.setDnsConfig(dnsConfig, domain, function (error) {
|
||||
if (error && error.reason === SettingsError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
config.setFqdn(domain); // set fqdn only after dns config is valid, otherwise cannot re-setup if we failed
|
||||
config.setZoneName(zoneName);
|
||||
config.set('fqdn', domain); // set fqdn only after dns config is valid, otherwise cannot re-setup if we failed
|
||||
|
||||
async.series([ // do not block
|
||||
onDomainConfigured,
|
||||
@@ -862,9 +855,9 @@ function migrate(options, callback) {
|
||||
|
||||
if (!options.domain) return doMigrate(options, callback);
|
||||
|
||||
var dnsConfig = _.pick(options, 'domain', 'provider', 'accessKeyId', 'secretAccessKey', 'region', 'endpoint', 'token', 'zoneName');
|
||||
var dnsConfig = _.pick(options, 'domain', 'provider', 'accessKeyId', 'secretAccessKey', 'region', 'endpoint', 'token');
|
||||
|
||||
settings.setDnsConfig(dnsConfig, options.domain, options.zoneName || tld.getDomain(options.domain), function (error) {
|
||||
settings.setDnsConfig(dnsConfig, options.domain, function (error) {
|
||||
if (error && error.reason === SettingsError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
|
||||
+7
-20
@@ -17,7 +17,6 @@ exports = module.exports = {
|
||||
apiServerOrigin: apiServerOrigin,
|
||||
webServerOrigin: webServerOrigin,
|
||||
fqdn: fqdn,
|
||||
setFqdn: setFqdn,
|
||||
token: token,
|
||||
version: version,
|
||||
setVersion: setVersion,
|
||||
@@ -32,7 +31,6 @@ exports = module.exports = {
|
||||
mailFqdn: mailFqdn,
|
||||
appFqdn: appFqdn,
|
||||
zoneName: zoneName,
|
||||
setZoneName: setZoneName,
|
||||
|
||||
isDemo: isDemo,
|
||||
|
||||
@@ -48,7 +46,6 @@ var assert = require('assert'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
safe = require('safetydance'),
|
||||
tld = require('tldjs'),
|
||||
_ = require('underscore');
|
||||
|
||||
var homeDir = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
|
||||
@@ -77,7 +74,6 @@ function _reset(callback) {
|
||||
function initConfig() {
|
||||
// setup defaults
|
||||
data.fqdn = 'localhost';
|
||||
data.zoneName = '';
|
||||
|
||||
data.token = null;
|
||||
data.version = null;
|
||||
@@ -147,26 +143,10 @@ function webServerOrigin() {
|
||||
return get('webServerOrigin');
|
||||
}
|
||||
|
||||
function setFqdn(fqdn) {
|
||||
set('fqdn', fqdn);
|
||||
}
|
||||
|
||||
function fqdn() {
|
||||
return get('fqdn');
|
||||
}
|
||||
|
||||
function setZoneName(zone) {
|
||||
set('zoneName', zone);
|
||||
}
|
||||
|
||||
function zoneName() {
|
||||
var zone = get('zoneName');
|
||||
if (zone) return zone;
|
||||
|
||||
// TODO: move this to migration code path instead
|
||||
return tld.getDomain(fqdn()) || '';
|
||||
}
|
||||
|
||||
// keep this in sync with start.sh admin.conf generation code
|
||||
function appFqdn(location) {
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
@@ -211,6 +191,13 @@ function isCustomDomain() {
|
||||
return get('isCustomDomain');
|
||||
}
|
||||
|
||||
function zoneName() {
|
||||
if (isCustomDomain()) return fqdn(); // the appstore sets up the custom domain as a zone
|
||||
|
||||
// for shared domain name, strip out the hostname
|
||||
return fqdn().substr(fqdn().indexOf('.') + 1);
|
||||
}
|
||||
|
||||
function database() {
|
||||
return get('database');
|
||||
}
|
||||
|
||||
+22
-9
@@ -15,6 +15,7 @@ var apps = require('./apps.js'),
|
||||
constants = require('./constants.js'),
|
||||
CronJob = require('cron').CronJob,
|
||||
debug = require('debug')('box:cron'),
|
||||
digest = require('./digest.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
janitor = require('./janitor.js'),
|
||||
scheduler = require('./scheduler.js'),
|
||||
@@ -22,20 +23,21 @@ var apps = require('./apps.js'),
|
||||
semver = require('semver'),
|
||||
updateChecker = require('./updatechecker.js');
|
||||
|
||||
var gAutoupdaterJob = null,
|
||||
gBoxUpdateCheckerJob = null,
|
||||
var gAliveJob = null, // send periodic stats
|
||||
gAppUpdateCheckerJob = null,
|
||||
gHeartbeatJob = null, // for CaaS health check
|
||||
gAliveJob = null, // send periodic stats
|
||||
gAutoupdaterJob = null,
|
||||
gBackupJob = null,
|
||||
gCleanupTokensJob = null,
|
||||
gCleanupBackupsJob = null,
|
||||
gDockerVolumeCleanerJob = null,
|
||||
gSchedulerSyncJob = null,
|
||||
gBoxUpdateCheckerJob = null,
|
||||
gCertificateRenewJob = null,
|
||||
gCheckDiskSpaceJob = null,
|
||||
gCleanupBackupsJob = null,
|
||||
gCleanupEventlogJob = null,
|
||||
gDynamicDNSJob = null;
|
||||
gCleanupTokensJob = null,
|
||||
gDockerVolumeCleanerJob = null,
|
||||
gDynamicDNSJob = null,
|
||||
gHeartbeatJob = null, // for CaaS health check
|
||||
gSchedulerSyncJob = null,
|
||||
gDigestEmailJob = null;
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
|
||||
var AUDIT_SOURCE = { userId: null, username: 'cron' };
|
||||
@@ -173,6 +175,14 @@ function recreateJobs(tz) {
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
|
||||
if (gDigestEmailJob) gDigestEmailJob.stop();
|
||||
gDigestEmailJob = new CronJob({
|
||||
cronTime: '00 00 00 * * 3', // every wednesday
|
||||
onTick: digest.maybeSend,
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
}
|
||||
|
||||
function autoupdatePatternChanged(pattern) {
|
||||
@@ -272,5 +282,8 @@ function uninitialize(callback) {
|
||||
if (gDynamicDNSJob) gDynamicDNSJob.stop();
|
||||
gDynamicDNSJob = null;
|
||||
|
||||
if (gDigestEmailJob) gDigestEmailJob.stop();
|
||||
gDigestEmailJob = null;
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
'use strict';
|
||||
|
||||
var appstore = require('./appstore.js'),
|
||||
debug = require('debug')('box:digest'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
updatechecker = require('./updatechecker.js'),
|
||||
mailer = require('./mailer.js'),
|
||||
settings = require('./settings.js');
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
exports = module.exports = {
|
||||
maybeSend: maybeSend
|
||||
};
|
||||
|
||||
function maybeSend(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
settings.getEmailDigest(function (error, enabled) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (!enabled) {
|
||||
debug('Email digest is disabled');
|
||||
return callback();
|
||||
}
|
||||
|
||||
var updateInfo = updatechecker.getUpdateInfo();
|
||||
var pendingAppUpdates = updateInfo.apps || {};
|
||||
pendingAppUpdates = Object.keys(pendingAppUpdates).map(function (key) { return pendingAppUpdates[key]; });
|
||||
|
||||
appstore.getSubscription(function (error, result) {
|
||||
if (error) debug('Error getting subscription:', error);
|
||||
|
||||
var hasSubscription = result && result.plan.id !== 'free' && result.plan.id !== 'undecided';
|
||||
|
||||
eventlog.getByActionLastWeek(eventlog.ACTION_APP_UPDATE, function (error, appUpdates) {
|
||||
if (error) return callback(error);
|
||||
|
||||
eventlog.getByActionLastWeek(eventlog.ACTION_UPDATE, function (error, boxUpdates) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var info = {
|
||||
hasSubscription: hasSubscription,
|
||||
|
||||
pendingAppUpdates: pendingAppUpdates,
|
||||
pendingBoxUpdate: updateInfo.box || null,
|
||||
|
||||
finishedAppUpdates: (appUpdates || []).map(function (e) { return e.data; }),
|
||||
finishedBoxUpdates: (boxUpdates || []).map(function (e) { return e.data; })
|
||||
};
|
||||
|
||||
if (info.pendingAppUpdates.length || info.pendingBoxUpdate || info.finishedAppUpdates.length || info.finishedBoxUpdates.length) {
|
||||
debug('maybeSend: sending digest email', info);
|
||||
mailer.sendDigest(info);
|
||||
} else {
|
||||
debug('maybeSend: nothing happened, NOT sending digest email');
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
+1
-2
@@ -112,10 +112,9 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
|
||||
function verifyDnsConfig(dnsConfig, domain, ip, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
|
||||
@@ -180,10 +180,9 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
function verifyDnsConfig(dnsConfig, domain, ip, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof fqdn, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -194,7 +193,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
|
||||
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
|
||||
|
||||
dns.resolveNs(zoneName, function (error, nameservers) {
|
||||
dns.resolveNs(domain, function (error, nameservers) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
|
||||
if (error || !nameservers) return callback(new SubdomainError(SubdomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
|
||||
|
||||
@@ -203,9 +202,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Domain nameservers are not set to Digital Ocean'));
|
||||
}
|
||||
|
||||
const name = constants.ADMIN_LOCATION + (fqdn === zoneName ? '' : '.' + fqdn.slice(0, - zoneName.length - 1));
|
||||
|
||||
upsert(credentials, zoneName, name, 'A', [ ip ], function (error, changeId) {
|
||||
upsert(credentials, domain, constants.ADMIN_LOCATION, 'A', [ ip ], function (error, changeId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: A record added with change id %s', changeId);
|
||||
|
||||
@@ -56,10 +56,9 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
callback(new Error('not implemented'));
|
||||
}
|
||||
|
||||
function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
|
||||
function verifyDnsConfig(dnsConfig, domain, ip, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
|
||||
+2
-3
@@ -51,16 +51,15 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
|
||||
function verifyDnsConfig(dnsConfig, domain, ip, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var adminDomain = constants.ADMIN_LOCATION + '.' + domain;
|
||||
|
||||
dns.resolveNs(zoneName, function (error, nameservers) {
|
||||
dns.resolveNs(domain, function (error, nameservers) {
|
||||
if (error || !nameservers) return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Unable to get nameservers'));
|
||||
|
||||
async.every(nameservers, function (nameserver, everyNsCallback) {
|
||||
|
||||
+2
-4
@@ -46,9 +46,8 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
function waitForDns(domain, zoneName, value, type, options, callback) {
|
||||
function waitForDns(domain, value, type, options, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert(typeof value === 'string' || util.isRegExp(value));
|
||||
assert(type === 'A' || type === 'CNAME' || type === 'TXT');
|
||||
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
|
||||
@@ -57,10 +56,9 @@ function waitForDns(domain, zoneName, value, type, options, callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
|
||||
function verifyDnsConfig(dnsConfig, domain, ip, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
|
||||
+5
-8
@@ -218,10 +218,9 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
function verifyDnsConfig(dnsConfig, domain, ip, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof fqdn, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -235,11 +234,11 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
|
||||
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
|
||||
|
||||
dns.resolveNs(zoneName, function (error, nameservers) {
|
||||
dns.resolveNs(domain, function (error, nameservers) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
|
||||
if (error || !nameservers) return callback(new SubdomainError(SubdomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
|
||||
|
||||
getHostedZone(credentials, zoneName, function (error, zone) {
|
||||
getHostedZone(credentials, domain, function (error, zone) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (!_.isEqual(zone.DelegationSet.NameServers.sort(), nameservers.sort())) {
|
||||
@@ -247,9 +246,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Domain nameservers are not set to Route53'));
|
||||
}
|
||||
|
||||
const name = constants.ADMIN_LOCATION + (fqdn === zoneName ? '' : '.' + fqdn.slice(0, - zoneName.length - 1));
|
||||
|
||||
upsert(credentials, zoneName, name, 'A', [ ip ], function (error, changeId) {
|
||||
upsert(credentials, domain, constants.ADMIN_LOCATION, 'A', [ ip ], function (error, changeId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: A record added with change id %s', changeId);
|
||||
|
||||
@@ -8,6 +8,7 @@ var assert = require('assert'),
|
||||
dig = require('../dig.js'),
|
||||
dns = require('dns'),
|
||||
SubdomainError = require('../subdomains.js').SubdomainError,
|
||||
tld = require('tldjs'),
|
||||
util = require('util');
|
||||
|
||||
function isChangeSynced(domain, value, type, nameserver, callback) {
|
||||
@@ -37,7 +38,7 @@ function isChangeSynced(domain, value, type, nameserver, callback) {
|
||||
}
|
||||
|
||||
if (!answer || answer.length === 0) {
|
||||
debug('bad answer from nameserver %s (%s) resolving %s (%s)', nameserver, nsIp, domain, type);
|
||||
debug('bad answer from nameserver %s (%s) resolving %s (%s): %j', nameserver, nsIp, domain, type, answer);
|
||||
return iteratorCallback(null, false);
|
||||
}
|
||||
|
||||
@@ -59,19 +60,18 @@ function isChangeSynced(domain, value, type, nameserver, callback) {
|
||||
}
|
||||
|
||||
// check if IP change has propagated to every nameserver
|
||||
function waitForDns(domain, zoneName, value, type, options, callback) {
|
||||
function waitForDns(domain, value, type, options, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert(typeof value === 'string' || util.isRegExp(value));
|
||||
assert(type === 'A' || type === 'CNAME' || type === 'TXT');
|
||||
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var zoneName = tld.getDomain(domain);
|
||||
if (typeof value === 'string') {
|
||||
// http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
|
||||
value = new RegExp('^' + value.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '$');
|
||||
}
|
||||
|
||||
debug('waitForIp: domain %s to be %s in zone %s.', domain, value, zoneName);
|
||||
|
||||
var attempt = 1;
|
||||
|
||||
-303
@@ -1,303 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
verifyRelay: verifyRelay,
|
||||
getStatus: getStatus,
|
||||
|
||||
EmailError: EmailError
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
cloudron = require('./cloudron.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:email'),
|
||||
dig = require('./dig.js'),
|
||||
net = require('net'),
|
||||
nodemailer = require('nodemailer'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
smtpTransport = require('nodemailer-smtp-transport'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
const digOptions = { server: '127.0.0.1', port: 53, timeout: 5000 };
|
||||
|
||||
function EmailError(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(EmailError, Error);
|
||||
EmailError.INTERNAL_ERROR = 'Internal Error';
|
||||
EmailError.BAD_FIELD = 'Bad Field';
|
||||
|
||||
function checkOutboundPort25(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var smtpServer = _.sample([
|
||||
'smtp.gmail.com',
|
||||
'smtp.live.com',
|
||||
'smtp.mail.yahoo.com',
|
||||
'smtp.o2.ie',
|
||||
'smtp.comcast.net',
|
||||
'outgoing.verizon.net'
|
||||
]);
|
||||
|
||||
var relay = {
|
||||
value: 'OK',
|
||||
status: false
|
||||
};
|
||||
|
||||
var client = new net.Socket();
|
||||
client.setTimeout(5000);
|
||||
client.connect(25, smtpServer);
|
||||
client.on('connect', function () {
|
||||
relay.status = true;
|
||||
relay.value = 'OK';
|
||||
client.destroy(); // do not use end() because it still triggers timeout
|
||||
callback(null, relay);
|
||||
});
|
||||
client.on('timeout', function () {
|
||||
relay.status = false;
|
||||
relay.value = 'Connect to ' + smtpServer + ' timed out';
|
||||
client.destroy();
|
||||
callback(new Error('Timeout'), relay);
|
||||
});
|
||||
client.on('error', function (error) {
|
||||
relay.status = false;
|
||||
relay.value = 'Connect to ' + smtpServer + ' failed: ' + error.message;
|
||||
client.destroy();
|
||||
callback(error, relay);
|
||||
});
|
||||
}
|
||||
|
||||
function checkSmtpRelay(relay, callback) {
|
||||
var result = {
|
||||
value: 'OK',
|
||||
status: false
|
||||
};
|
||||
|
||||
var transporter = nodemailer.createTransport(smtpTransport({
|
||||
host: relay.host,
|
||||
port: relay.port,
|
||||
auth: {
|
||||
user: relay.username,
|
||||
pass: relay.password
|
||||
}
|
||||
}));
|
||||
|
||||
transporter.verify(function(error) {
|
||||
result.status = !error;
|
||||
if (error) {
|
||||
result.value = error.message;
|
||||
return callback(error, result);
|
||||
}
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function verifyRelay(relay, callback) {
|
||||
assert.strictEqual(typeof relay, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var verifier = relay.provider === 'cloudron-smtp' ? checkOutboundPort25 : checkSmtpRelay.bind(null, relay);
|
||||
|
||||
verifier(function (error) {
|
||||
if (error) return callback(new EmailError(EmailError.BAD_FIELD, error.message));
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function checkDkim(callback) {
|
||||
var dkim = {
|
||||
domain: constants.DKIM_SELECTOR + '._domainkey.' + config.fqdn(),
|
||||
type: 'TXT',
|
||||
expected: null,
|
||||
value: null,
|
||||
status: false
|
||||
};
|
||||
|
||||
var dkimKey = cloudron.readDkimPublicKeySync();
|
||||
if (!dkimKey) return callback(new Error('Failed to read dkim public key'), dkim);
|
||||
|
||||
dkim.expected = '"v=DKIM1; t=s; p=' + dkimKey + '"';
|
||||
|
||||
dig.resolve(dkim.domain, dkim.type, digOptions, function (error, txtRecords) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(null, dkim); // not setup
|
||||
if (error) return callback(error, dkim);
|
||||
|
||||
if (Array.isArray(txtRecords) && txtRecords.length !== 0) {
|
||||
dkim.value = txtRecords[0];
|
||||
dkim.status = (dkim.value === dkim.expected);
|
||||
}
|
||||
|
||||
callback(null, dkim);
|
||||
});
|
||||
}
|
||||
|
||||
function checkSpf(callback) {
|
||||
var spf = {
|
||||
domain: config.fqdn(),
|
||||
type: 'TXT',
|
||||
value: null,
|
||||
expected: '"v=spf1 a:' + config.adminFqdn() + ' ~all"',
|
||||
status: false
|
||||
};
|
||||
|
||||
// https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
|
||||
dig.resolve(spf.domain, spf.type, digOptions, function (error, txtRecords) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(null, spf); // not setup
|
||||
if (error) return callback(error, spf);
|
||||
|
||||
if (!Array.isArray(txtRecords)) return callback(null, spf);
|
||||
|
||||
var i;
|
||||
for (i = 0; i < txtRecords.length; i++) {
|
||||
if (txtRecords[i].indexOf('"v=spf1 ') !== 0) continue; // not SPF
|
||||
spf.value = txtRecords[i];
|
||||
spf.status = spf.value.indexOf(' a:' + config.adminFqdn()) !== -1;
|
||||
break;
|
||||
}
|
||||
|
||||
if (spf.status) {
|
||||
spf.expected = spf.value;
|
||||
} else if (i !== txtRecords.length) {
|
||||
spf.expected = '"v=spf1 a:' + config.adminFqdn() + ' ' + spf.value.slice('"v=spf1 '.length);
|
||||
}
|
||||
|
||||
callback(null, spf);
|
||||
});
|
||||
}
|
||||
|
||||
function checkMx(callback) {
|
||||
var mx = {
|
||||
domain: config.fqdn(),
|
||||
type: 'MX',
|
||||
value: null,
|
||||
expected: '10 ' + config.mailFqdn() + '.',
|
||||
status: false
|
||||
};
|
||||
|
||||
dig.resolve(mx.domain, mx.type, digOptions, function (error, mxRecords) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(null, mx); // not setup
|
||||
if (error) return callback(error, mx);
|
||||
|
||||
if (Array.isArray(mxRecords) && mxRecords.length !== 0) {
|
||||
mx.status = mxRecords.length == 1 && mxRecords[0].exchange === (config.mailFqdn() + '.');
|
||||
mx.value = mxRecords.map(function (r) { return r.priority + ' ' + r.exchange; }).join(' ');
|
||||
}
|
||||
|
||||
callback(null, mx);
|
||||
});
|
||||
}
|
||||
|
||||
function checkDmarc(callback) {
|
||||
var dmarc = {
|
||||
domain: '_dmarc.' + config.fqdn(),
|
||||
type: 'TXT',
|
||||
value: null,
|
||||
expected: '"v=DMARC1; p=reject; pct=100"',
|
||||
status: false
|
||||
};
|
||||
|
||||
dig.resolve(dmarc.domain, dmarc.type, digOptions, function (error, txtRecords) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(null, dmarc); // not setup
|
||||
if (error) return callback(error, dmarc);
|
||||
|
||||
if (Array.isArray(txtRecords) && txtRecords.length !== 0) {
|
||||
dmarc.value = txtRecords[0];
|
||||
dmarc.status = (dmarc.value === dmarc.expected);
|
||||
}
|
||||
|
||||
callback(null, dmarc);
|
||||
});
|
||||
}
|
||||
|
||||
function checkPtr(callback) {
|
||||
var ptr = {
|
||||
domain: null,
|
||||
type: 'PTR',
|
||||
value: null,
|
||||
expected: config.mailFqdn() + '.',
|
||||
status: false
|
||||
};
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(error, ptr);
|
||||
|
||||
ptr.domain = ip.split('.').reverse().join('.') + '.in-addr.arpa';
|
||||
|
||||
dig.resolve(ip, 'PTR', digOptions, function (error, ptrRecords) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(null, ptr); // not setup
|
||||
if (error) return callback(error, ptr);
|
||||
|
||||
if (Array.isArray(ptrRecords) && ptrRecords.length !== 0) {
|
||||
ptr.value = ptrRecords.join(' ');
|
||||
ptr.status = ptrRecords.some(function (v) { return v === ptr.expected; });
|
||||
}
|
||||
|
||||
return callback(null, ptr);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getStatus(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var results = {};
|
||||
|
||||
function recordResult(what, func) {
|
||||
return function (callback) {
|
||||
func(function (error, result) {
|
||||
if (error) debug('Ignored error - ' + what + ':', error);
|
||||
|
||||
safe.set(results, what, result);
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
settings.getMailRelay(function (error, relay) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var checks = [
|
||||
recordResult('dns.mx', checkMx),
|
||||
recordResult('dns.dmarc', checkDmarc)
|
||||
];
|
||||
|
||||
if (relay.provider === 'cloudron-smtp') {
|
||||
// these tests currently only make sense when using Cloudron's SMTP server at this point
|
||||
checks.push(
|
||||
recordResult('dns.spf', checkSpf),
|
||||
recordResult('dns.dkim', checkDkim),
|
||||
recordResult('dns.ptr', checkPtr),
|
||||
recordResult('relay', checkOutboundPort25)
|
||||
);
|
||||
} else {
|
||||
checks.push(recordResult('relay', checkSmtpRelay.bind(null, relay)));
|
||||
}
|
||||
|
||||
async.parallel(checks, function () {
|
||||
callback(null, results);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -6,6 +6,7 @@ exports = module.exports = {
|
||||
add: add,
|
||||
get: get,
|
||||
getAllPaged: getAllPaged,
|
||||
getByActionLastWeek: getByActionLastWeek,
|
||||
cleanup: cleanup,
|
||||
|
||||
// keep in sync with webadmin index.js filter and CLI tool
|
||||
@@ -103,6 +104,17 @@ function getAllPaged(action, search, page, perPage, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getByActionLastWeek(action, callback) {
|
||||
assert(typeof action === 'string' || action === null);
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
eventlogdb.getByActionLastWeek(action, function (error, boxes) {
|
||||
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, boxes);
|
||||
});
|
||||
}
|
||||
|
||||
function cleanup(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
exports = module.exports = {
|
||||
get: get,
|
||||
getAllPaged: getAllPaged,
|
||||
getByActionLastWeek: getByActionLastWeek,
|
||||
add: add,
|
||||
count: count,
|
||||
delByCreationTime: delByCreationTime,
|
||||
@@ -71,6 +72,20 @@ function getAllPaged(action, search, page, perPage, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getByActionLastWeek(action, callback) {
|
||||
assert(typeof action === 'string' || action === null);
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var query = 'SELECT ' + EVENTLOGS_FIELDS + ' FROM eventlog WHERE action=? AND creationTime >= DATE_SUB(NOW(), INTERVAL 1 WEEK) ORDER BY creationTime DESC';
|
||||
database.query(query, [ action ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
results.forEach(postProcess);
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
function add(id, action, source, data, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof action, 'string');
|
||||
|
||||
@@ -7,18 +7,18 @@
|
||||
exports = module.exports = {
|
||||
// a major version makes all apps restore from backup
|
||||
// a minor version makes all apps re-configure themselves
|
||||
'version': '48.4.0',
|
||||
'version': '48.3.0',
|
||||
|
||||
'baseImages': [ 'cloudron/base:0.10.0' ],
|
||||
|
||||
// Note that if any of the databases include an upgrade, bump the infra version above
|
||||
// This is because we upgrade using dumps instead of mysql_upgrade, pg_upgrade etc
|
||||
'images': {
|
||||
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:0.18.0' },
|
||||
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:0.17.0' },
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:0.17.0' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:0.13.0' },
|
||||
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:0.11.0' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.34.1' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.32.0' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:0.11.0' }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<% if (format === 'text') { -%>
|
||||
|
||||
Dear <%= cloudronName %> Admin,
|
||||
|
||||
This is the weekly summary of activities on your Cloudron <%= fqdn %>.
|
||||
<% if (info.pendingBoxUpdate) { -%>
|
||||
|
||||
Cloudron v<%- info.pendingBoxUpdate.version %> is available:
|
||||
<% for (var i = 0; i < info.pendingBoxUpdate.changelog.length; i++) { -%>
|
||||
* <%- info.pendingBoxUpdate.changelog[i] %>
|
||||
<% }} -%>
|
||||
<% if (info.pendingAppUpdates.length) { -%>
|
||||
|
||||
One or more app updates are available:
|
||||
<% for (var i = 0; i < info.pendingAppUpdates.length; i++) { -%>
|
||||
- <%= info.pendingAppUpdates[i].manifest.title %> package v<%= info.pendingAppUpdates[i].manifest.version %>
|
||||
<% for (var j = 0; j < info.pendingAppUpdates[i].manifest.changelog.trim().split('\n').length; j++) { -%>
|
||||
<%= info.pendingAppUpdates[i].manifest.changelog.trim().split('\n')[j] %>
|
||||
<% }}} -%>
|
||||
<% if (info.finishedBoxUpdates.length) { -%>
|
||||
|
||||
Cloudron was updated with the following releases:
|
||||
<% for (var i = 0; i < info.finishedBoxUpdates.length; i++) { -%>
|
||||
- Version <%= info.finishedBoxUpdates[i].boxUpdateInfo.version %>
|
||||
<% for (var j = 0; j < info.finishedBoxUpdates[i].boxUpdateInfo.changelog.length; j++) { -%>
|
||||
* <%= info.finishedBoxUpdates[i].boxUpdateInfo.changelog[j] %>
|
||||
<% }}} -%>
|
||||
<% if (info.finishedAppUpdates.length) { -%>
|
||||
|
||||
The following apps were updated:
|
||||
<% for (var i = 0; i < info.finishedAppUpdates.length; i++) { -%>
|
||||
- <%= info.finishedAppUpdates[i].toManifest.title %> package v<%= info.finishedAppUpdates[i].toManifest.version %>
|
||||
<% for (var j = 0; j < info.finishedAppUpdates[i].toManifest.changelog.trim().split('\n').length; j++) { -%>
|
||||
<%= info.finishedAppUpdates[i].toManifest.changelog.trim().split('\n')[j] %>
|
||||
<% }}} -%>
|
||||
<% if (!info.hasSubscription) { -%>
|
||||
|
||||
*Keep your Cloudron automatically up-to-date and secure by upgrading to a paid plan at* <%= webadminUrl %>/#/settings
|
||||
<% } -%>
|
||||
|
||||
Powered by https://cloudron.io
|
||||
|
||||
Sent at: <%= new Date().toUTCString() %>
|
||||
|
||||
<% } else { %>
|
||||
|
||||
<% } %>
|
||||
+44
-4
@@ -10,6 +10,7 @@ exports = module.exports = {
|
||||
passwordReset: passwordReset,
|
||||
boxUpdateAvailable: boxUpdateAvailable,
|
||||
appUpdateAvailable: appUpdateAvailable,
|
||||
sendDigest: sendDigest,
|
||||
|
||||
sendInvite: sendInvite,
|
||||
unexpectedExit: unexpectedExit,
|
||||
@@ -45,6 +46,7 @@ var assert = require('assert'),
|
||||
settings = require('./settings.js'),
|
||||
showdown = require('showdown'),
|
||||
smtpTransport = require('nodemailer-smtp-transport'),
|
||||
subdomains = require('./subdomains.js'),
|
||||
users = require('./user.js'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
@@ -54,7 +56,7 @@ var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
var MAIL_TEMPLATES_DIR = path.join(__dirname, 'mail_templates');
|
||||
|
||||
var gMailQueue = [ ],
|
||||
gPaused = false;
|
||||
gDnsReady = false;
|
||||
|
||||
function splatchError(error) {
|
||||
var result = { };
|
||||
@@ -70,7 +72,7 @@ function splatchError(error) {
|
||||
function start(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (process.env.BOX_ENV === 'test') gPaused = true;
|
||||
checkDns();
|
||||
|
||||
callback(null);
|
||||
}
|
||||
@@ -92,8 +94,22 @@ function mailConfig() {
|
||||
};
|
||||
}
|
||||
|
||||
// keep this in sync with the cloudron.js dns changes
|
||||
function checkDns() {
|
||||
if (process.env.BOX_ENV === 'test') return;
|
||||
|
||||
subdomains.waitForDns(config.fqdn(), new RegExp('^"v=spf1 .*a:' + config.adminFqdn().replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '.*'), 'TXT', { interval: 60000, times: Infinity }, function (error) {
|
||||
if (error) return debug(error); // can never happen
|
||||
|
||||
debug('checkDns: SPF check passed. commencing mail processing');
|
||||
|
||||
gDnsReady = true;
|
||||
processQueue();
|
||||
});
|
||||
}
|
||||
|
||||
function processQueue() {
|
||||
assert(!gPaused);
|
||||
assert(gDnsReady);
|
||||
|
||||
sendMails(gMailQueue);
|
||||
gMailQueue = [ ];
|
||||
@@ -141,7 +157,7 @@ function enqueue(mailOptions) {
|
||||
debug('Queued mail for ' + mailOptions.from + ' to ' + mailOptions.to);
|
||||
gMailQueue.push(mailOptions);
|
||||
|
||||
if (!gPaused) processQueue();
|
||||
if (gDnsReady) processQueue();
|
||||
}
|
||||
|
||||
function render(templateFile, params) {
|
||||
@@ -403,6 +419,30 @@ function appUpdateAvailable(app, updateInfo) {
|
||||
});
|
||||
}
|
||||
|
||||
function sendDigest(info) {
|
||||
assert.strictEqual(typeof info, 'object');
|
||||
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return console.log('Error getting admins', error);
|
||||
|
||||
settings.getCloudronName(function (error, cloudronName) {
|
||||
if (error) {
|
||||
debug(error);
|
||||
cloudronName = 'Cloudron';
|
||||
}
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig().from,
|
||||
to: adminEmails.join(', '),
|
||||
subject: util.format('[%s] Weekly event digest', config.fqdn()),
|
||||
text: render('digest.ejs', { fqdn: config.fqdn(), webadminUrl: config.adminOrigin(), cloudronName: cloudronName, info: info, format: 'text' })
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function outOfDiskSpace(message) {
|
||||
assert.strictEqual(typeof message, 'string');
|
||||
|
||||
|
||||
+6
-31
@@ -40,9 +40,7 @@ function start(callback) {
|
||||
|
||||
debug('initializing addon infrastructure');
|
||||
|
||||
// restart mail container if any of these keys change
|
||||
settings.events.on(settings.MAIL_CONFIG_KEY, function () { startMail(NOOP_CALLBACK); });
|
||||
settings.events.on(settings.MAIL_RELAY_KEY, function () { startMail(NOOP_CALLBACK); });
|
||||
|
||||
certificates.events.on(certificates.EVENT_CERT_CHANGED, function (domain) {
|
||||
if (domain === '*.' + config.fqdn() || domain === config.adminFqdn()) startMail(NOOP_CALLBACK);
|
||||
@@ -241,39 +239,16 @@ 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(','); // owner may not exist yet
|
||||
alertsTo.concat(error ? [] : owner.email).join(',');
|
||||
|
||||
settings.getCatchAllAddress(function (error, address) {
|
||||
if (error) return callback(error);
|
||||
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));
|
||||
}
|
||||
|
||||
var catchAll = address.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}\ncatch_all=${catchAll}\n`, 'utf8')) {
|
||||
return callback(new Error('Could not create mail var file:' + safe.error.message));
|
||||
}
|
||||
|
||||
settings.getMailRelay(function (error, relay) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const enabled = relay.provider !== 'cloudron-smtp' ? true : false,
|
||||
host = relay.host || '',
|
||||
port = relay.port || 25,
|
||||
username = relay.username || '',
|
||||
password = relay.password || '';
|
||||
|
||||
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/smtp_forward.ini',
|
||||
`enable_outbound=${enabled}\nhost=${host}\nport=${port}\nenable_tls=true\nauth_type=plain\nauth_user=${username}\nauth_pass=${password}`, 'utf8')) {
|
||||
return callback(new Error('Could not create mail var file:' + safe.error.message));
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -139,7 +139,7 @@ function installApp(req, res, next) {
|
||||
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
|
||||
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
|
||||
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === AppsError.BILLING_REQUIRED) return next(new HttpError(402, error.message));
|
||||
if (error && error.reason === AppsError.BILLING_REQUIRED) return next(new HttpError(402, 'Billing required'));
|
||||
if (error && error.reason === AppsError.BAD_CERTIFICATE) return next(new HttpError(400, error.message));
|
||||
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(503, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
@@ -80,9 +80,7 @@ function dnsSetup(req, res, next) {
|
||||
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
|
||||
if (typeof req.body.domain !== 'string' || !req.body.domain) return next(new HttpError(400, 'domain is required'));
|
||||
|
||||
if ('zoneName' in req.body && typeof req.body.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be a string'));
|
||||
|
||||
cloudron.dnsSetup(req.body, req.body.domain.toLowerCase(), req.body.zoneName || '', function (error) {
|
||||
cloudron.dnsSetup(req.body, req.body.domain.toLowerCase(), function (error) {
|
||||
if (error && error.reason === CloudronError.ALREADY_SETUP) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
@@ -161,8 +159,6 @@ function migrate(req, res, next) {
|
||||
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider must be string'));
|
||||
}
|
||||
|
||||
if ('zoneName' in req.body && typeof req.body.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be string'));
|
||||
|
||||
debug('Migration requested domain:%s size:%s region:%s', req.body.domain, req.body.size, req.body.region);
|
||||
|
||||
var options = _.pick(req.body, 'domain', 'size', 'region');
|
||||
|
||||
+4
-61
@@ -24,12 +24,6 @@ exports = module.exports = {
|
||||
getMailConfig: getMailConfig,
|
||||
setMailConfig: setMailConfig,
|
||||
|
||||
getMailRelay: getMailRelay,
|
||||
setMailRelay: setMailRelay,
|
||||
|
||||
getCatchAllAddress: getCatchAllAddress,
|
||||
setCatchAllAddress: setCatchAllAddress,
|
||||
|
||||
getAppstoreConfig: getAppstoreConfig,
|
||||
setAppstoreConfig: setAppstoreConfig,
|
||||
|
||||
@@ -41,7 +35,6 @@ var assert = require('assert'),
|
||||
certificates = require('../certificates.js'),
|
||||
CertificatesError = require('../certificates.js').CertificatesError,
|
||||
config = require('../config.js'),
|
||||
email = require('../email.js'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
safe = require('safetydance'),
|
||||
@@ -128,57 +121,7 @@ function setMailConfig(req, res, next) {
|
||||
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(202));
|
||||
});
|
||||
}
|
||||
|
||||
function getMailRelay(req, res, next) {
|
||||
settings.getMailRelay(function (error, mail) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, mail));
|
||||
});
|
||||
}
|
||||
|
||||
function setMailRelay(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
|
||||
if ('host' in req.body && typeof req.body.host !== 'string') return next(new HttpError(400, 'host must be a string'));
|
||||
if ('port' in req.body && typeof req.body.port !== 'number') return next(new HttpError(400, 'port must be a string'));
|
||||
if ('username' in req.body && typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be a string'));
|
||||
if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be a string'));
|
||||
|
||||
settings.setMailRelay(req.body, 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(202));
|
||||
});
|
||||
}
|
||||
|
||||
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(202));
|
||||
next(new HttpSuccess(200));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -191,7 +134,7 @@ function setCloudronAvatar(req, res, next) {
|
||||
settings.setCloudronAvatar(avatar, function (error) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202));
|
||||
next(new HttpSuccess(202, {}));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -208,7 +151,7 @@ function getCloudronAvatar(req, res, next) {
|
||||
}
|
||||
|
||||
function getEmailStatus(req, res, next) {
|
||||
email.getStatus(function (error, records) {
|
||||
settings.getEmailStatus(function (error, records) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, records));
|
||||
@@ -228,7 +171,7 @@ function setDnsConfig(req, res, next) {
|
||||
|
||||
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
|
||||
|
||||
settings.setDnsConfig(req.body, config.fqdn(), config.zoneName(), function (error) {
|
||||
settings.setDnsConfig(req.body, config.fqdn(), function (error) {
|
||||
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
|
||||
@@ -149,8 +149,7 @@ function startBox(done) {
|
||||
safe.fs.unlinkSync(paths.INFRA_VERSION_FILE);
|
||||
child_process.execSync('docker ps -qa | xargs --no-run-if-empty docker rm -f');
|
||||
|
||||
config.setFqdn('foobar.com');
|
||||
config.setZoneName('foobar.com');
|
||||
config.set('fqdn', 'foobar.com');
|
||||
|
||||
awsHostedZones = {
|
||||
HostedZones: [{
|
||||
@@ -232,7 +231,7 @@ function startBox(done) {
|
||||
}, callback);
|
||||
},
|
||||
|
||||
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }, config.fqdn(), config.zoneName()),
|
||||
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }, config.fqdn()),
|
||||
settings.setTlsConfig.bind(null, { provider: 'caas' }),
|
||||
settings.setBackupConfig.bind(null, { provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' })
|
||||
], function (error) {
|
||||
@@ -641,7 +640,7 @@ describe('App installation', function () {
|
||||
apiHockServer = http.createServer(apiHockInstance.handler).listen(port, callback);
|
||||
},
|
||||
|
||||
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }, config.fqdn(), config.zoneName()),
|
||||
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }, config.fqdn()),
|
||||
|
||||
settings.setTlsConfig.bind(null, { provider: 'caas' }),
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ function setup(done) {
|
||||
nock.cleanAll();
|
||||
config._reset();
|
||||
config.setVersion('1.2.3');
|
||||
config.setFqdn('localhost');
|
||||
config.set('fqdn', 'localhost');
|
||||
|
||||
async.series([
|
||||
server.start.bind(server),
|
||||
|
||||
@@ -27,7 +27,7 @@ function setup(done) {
|
||||
nock.cleanAll();
|
||||
config._reset();
|
||||
config.set('version', '0.5.0');
|
||||
config.setFqdn('localhost');
|
||||
config.set('fqdn', 'localhost');
|
||||
|
||||
server.start(function (error) {
|
||||
if (error) return done(error);
|
||||
|
||||
@@ -15,10 +15,9 @@ var appdb = require('../../appdb.js'),
|
||||
expect = require('expect.js'),
|
||||
path = require('path'),
|
||||
paths = require('../../paths.js'),
|
||||
superagent = require('superagent'),
|
||||
server = require('../../server.js'),
|
||||
settings = require('../../settings.js'),
|
||||
settingsdb = require('../../settingsdb.js'),
|
||||
superagent = require('superagent'),
|
||||
fs = require('fs'),
|
||||
nock = require('nock');
|
||||
|
||||
@@ -29,7 +28,7 @@ var token = null;
|
||||
|
||||
var server;
|
||||
function setup(done) {
|
||||
config.setFqdn('foobar.com');
|
||||
config.set('fqdn', 'foobar.com');
|
||||
|
||||
async.series([
|
||||
server.start.bind(server),
|
||||
@@ -300,7 +299,7 @@ describe('Settings API', function () {
|
||||
.query({ access_token: token })
|
||||
.send({ enabled: true })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(202);
|
||||
expect(res.statusCode).to.equal(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -316,57 +315,6 @@ 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(202);
|
||||
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
|
||||
@@ -758,7 +706,7 @@ describe('Settings API', function () {
|
||||
expect(res.body.dns.ptr.status).to.eql(false);
|
||||
// expect(res.body.ptr.value).to.eql(null); this will be anything random
|
||||
|
||||
expect(res.body.relay).to.be.an('object');
|
||||
expect(res.body.outboundPort25).to.be.an('object');
|
||||
|
||||
done();
|
||||
});
|
||||
@@ -826,62 +774,4 @@ describe('Settings API', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mail relay', function () {
|
||||
it('get mail relay succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/settings/mail_relay')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body).to.eql({ provider: 'cloudron-smtp' });
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set without provider field', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/mail_relay')
|
||||
.query({ access_token: token })
|
||||
.send({ })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set with bad host', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/mail_relay')
|
||||
.query({ access_token: token })
|
||||
.send({ provider: 'external-smtp', host: true })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('set fails because mail server is unreachable', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/mail_relay')
|
||||
.query({ access_token: token })
|
||||
.send({ provider: 'external-smtp', host: 'host', port: 25, username: 'u', password: 'p', tls: true })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get succeeds', function (done) {
|
||||
var relay = { provider: 'external-smtp', host: 'host', port: 25, username: 'u', password: 'p', tls: true };
|
||||
|
||||
settingsdb.set(settings.MAIL_RELAY_KEY, JSON.stringify(relay), function (error) { // skip the mail server verify()
|
||||
expect(error).to.not.be.ok();
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/settings/mail_relay')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body).to.eql(relay);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ var token = null;
|
||||
|
||||
var server;
|
||||
function setup(done) {
|
||||
config.setFqdn('foobar.com');
|
||||
config.set('fqdn', 'foobar.com');
|
||||
|
||||
async.series([
|
||||
server.start.bind(server),
|
||||
|
||||
+1
-1
@@ -20,4 +20,4 @@ fi
|
||||
echo "Running node with memory constraints"
|
||||
|
||||
# note BOX_ENV and NODE_ENV are derived from parent process
|
||||
exec env "DEBUG=box*,connect-lastmile" /usr/bin/node --max_old_space_size=300 "$@"
|
||||
exec env "DEBUG=box*,connect-lastmile" /usr/bin/node --max_old_space_size=200 "$@"
|
||||
|
||||
@@ -209,14 +209,8 @@ function initializeExpressSync() {
|
||||
router.post('/api/v1/settings/time_zone', settingsScope, routes.user.requireAdmin, routes.settings.setTimeZone);
|
||||
router.get ('/api/v1/settings/appstore_config', settingsScope, routes.user.requireAdmin, routes.settings.getAppstoreConfig);
|
||||
router.post('/api/v1/settings/appstore_config', settingsScope, routes.user.requireAdmin, routes.settings.setAppstoreConfig);
|
||||
|
||||
// email routes
|
||||
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/mail_relay', settingsScope, routes.user.requireAdmin, routes.settings.getMailRelay);
|
||||
router.post('/api/v1/settings/mail_relay', settingsScope, routes.user.requireAdmin, routes.settings.setMailRelay);
|
||||
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);
|
||||
|
||||
+221
-55
@@ -6,6 +6,8 @@ exports = module.exports = {
|
||||
initialize: initialize,
|
||||
uninitialize: uninitialize,
|
||||
|
||||
getEmailStatus: getEmailStatus,
|
||||
|
||||
getAutoupdatePattern: getAutoupdatePattern,
|
||||
setAutoupdatePattern: setAutoupdatePattern,
|
||||
|
||||
@@ -42,19 +44,17 @@ exports = module.exports = {
|
||||
getMailConfig: getMailConfig,
|
||||
setMailConfig: setMailConfig,
|
||||
|
||||
getMailRelay: getMailRelay,
|
||||
setMailRelay: setMailRelay,
|
||||
|
||||
setCatchAllAddress: setCatchAllAddress,
|
||||
getCatchAllAddress: getCatchAllAddress,
|
||||
|
||||
getDefaultSync: getDefaultSync,
|
||||
getEmailDigest: getEmailDigest,
|
||||
setEmailDigest: setEmailDigest,
|
||||
|
||||
getAll: getAll,
|
||||
|
||||
AUTOUPDATE_PATTERN_KEY: 'autoupdate_pattern',
|
||||
TIME_ZONE_KEY: 'time_zone',
|
||||
CLOUDRON_NAME_KEY: 'cloudron_name',
|
||||
DEVELOPER_MODE_KEY: 'developer_mode',
|
||||
EMAIL_DIGEST: 'email_digest',
|
||||
DNS_CONFIG_KEY: 'dns_config',
|
||||
DYNAMIC_DNS_KEY: 'dynamic_dns',
|
||||
BACKUP_CONFIG_KEY: 'backup_config',
|
||||
@@ -62,13 +62,12 @@ exports = module.exports = {
|
||||
UPDATE_CONFIG_KEY: 'update_config',
|
||||
APPSTORE_CONFIG_KEY: 'appstore_config',
|
||||
MAIL_CONFIG_KEY: 'mail_config',
|
||||
MAIL_RELAY_KEY: 'mail_relay',
|
||||
CATCH_ALL_ADDRESS: 'catch_all_address',
|
||||
|
||||
events: null
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
backups = require('./backups.js'),
|
||||
BackupsError = backups.BackupsError,
|
||||
config = require('./config.js'),
|
||||
@@ -76,12 +75,12 @@ var assert = require('assert'),
|
||||
CronJob = require('cron').CronJob,
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:settings'),
|
||||
dig = require('./dig.js'),
|
||||
cloudron = require('./cloudron.js'),
|
||||
CloudronError = cloudron.CloudronError,
|
||||
moment = require('moment-timezone'),
|
||||
net = require('net'),
|
||||
paths = require('./paths.js'),
|
||||
platform = require('./platform.js'),
|
||||
email = require('./email.js'),
|
||||
EmailError = email.EmailError,
|
||||
safe = require('safetydance'),
|
||||
settingsdb = require('./settingsdb.js'),
|
||||
subdomains = require('./subdomains.js'),
|
||||
@@ -109,8 +108,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.MAIL_RELAY_KEY] = { provider: 'cloudron-smtp' };
|
||||
result[exports.CATCH_ALL_ADDRESS] = [ ];
|
||||
result[exports.EMAIL_DIGEST] = true;
|
||||
|
||||
return result;
|
||||
})();
|
||||
@@ -155,6 +153,206 @@ function uninitialize(callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
function getEmailStatus(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var digOptions = { server: '127.0.0.1', port: 53, timeout: 5000 };
|
||||
|
||||
var records = {}, outboundPort25 = {};
|
||||
|
||||
var dkimKey = cloudron.readDkimPublicKeySync();
|
||||
if (!dkimKey) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, new Error('Failed to read dkim public key')));
|
||||
|
||||
function checkDkim(callback) {
|
||||
records.dkim = {
|
||||
domain: constants.DKIM_SELECTOR + '._domainkey.' + config.fqdn(),
|
||||
type: 'TXT',
|
||||
expected: '"v=DKIM1; t=s; p=' + dkimKey + '"',
|
||||
value: null,
|
||||
status: false
|
||||
};
|
||||
|
||||
dig.resolve(records.dkim.domain, records.dkim.type, digOptions, function (error, txtRecords) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(null); // not setup
|
||||
if (error) return callback(error);
|
||||
|
||||
if (Array.isArray(txtRecords) && txtRecords.length !== 0) {
|
||||
records.dkim.value = txtRecords[0];
|
||||
records.dkim.status = (records.dkim.value === records.dkim.expected);
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function checkSpf(callback) {
|
||||
records.spf = {
|
||||
domain: config.fqdn(),
|
||||
type: 'TXT',
|
||||
value: null,
|
||||
expected: '"v=spf1 a:' + config.adminFqdn() + ' ~all"',
|
||||
status: false
|
||||
};
|
||||
|
||||
// https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
|
||||
dig.resolve(records.spf.domain, records.spf.type, digOptions, function (error, txtRecords) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(null); // not setup
|
||||
if (error) return callback(error);
|
||||
|
||||
if (!Array.isArray(txtRecords)) return callback();
|
||||
|
||||
var i;
|
||||
for (i = 0; i < txtRecords.length; i++) {
|
||||
if (txtRecords[i].indexOf('"v=spf1 ') !== 0) continue; // not SPF
|
||||
records.spf.value = txtRecords[i];
|
||||
records.spf.status = records.spf.value.indexOf(' a:' + config.adminFqdn()) !== -1;
|
||||
break;
|
||||
}
|
||||
|
||||
if (records.spf.status) {
|
||||
records.spf.expected = records.spf.value;
|
||||
} else if (i !== txtRecords.length) {
|
||||
records.spf.expected = '"v=spf1 a:' + config.adminFqdn() + ' ' + records.spf.value.slice('"v=spf1 '.length);
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function checkMx(callback) {
|
||||
records.mx = {
|
||||
domain: config.fqdn(),
|
||||
type: 'MX',
|
||||
value: null,
|
||||
expected: '10 ' + config.mailFqdn() + '.',
|
||||
status: false
|
||||
};
|
||||
|
||||
dig.resolve(records.mx.domain, records.mx.type, digOptions, function (error, mxRecords) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(null); // not setup
|
||||
if (error) return callback(error);
|
||||
|
||||
if (Array.isArray(mxRecords) && mxRecords.length !== 0) {
|
||||
records.mx.status = mxRecords.length == 1 && mxRecords[0].exchange === (config.mailFqdn() + '.');
|
||||
records.mx.value = mxRecords.map(function (r) { return r.priority + ' ' + r.exchange; }).join(' ');
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function checkDmarc(callback) {
|
||||
records.dmarc = {
|
||||
domain: '_dmarc.' + config.fqdn(),
|
||||
type: 'TXT',
|
||||
value: null,
|
||||
expected: '"v=DMARC1; p=reject; pct=100"',
|
||||
status: false
|
||||
};
|
||||
|
||||
dig.resolve(records.dmarc.domain, records.dmarc.type, digOptions, function (error, txtRecords) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(null); // not setup
|
||||
if (error) return callback(error);
|
||||
|
||||
if (Array.isArray(txtRecords) && txtRecords.length !== 0) {
|
||||
records.dmarc.value = txtRecords[0];
|
||||
records.dmarc.status = (records.dmarc.value === records.dmarc.expected);
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function checkPtr(callback) {
|
||||
records.ptr = {
|
||||
domain: null,
|
||||
type: 'PTR',
|
||||
value: null,
|
||||
expected: config.mailFqdn() + '.',
|
||||
status: false
|
||||
};
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
records.ptr.domain = ip.split('.').reverse().join('.') + '.in-addr.arpa';
|
||||
|
||||
dig.resolve(ip, 'PTR', digOptions, function (error, ptrRecords) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(null); // not setup
|
||||
if (error) return callback(error);
|
||||
|
||||
if (Array.isArray(ptrRecords) && ptrRecords.length !== 0) {
|
||||
records.ptr.value = ptrRecords.join(' ');
|
||||
records.ptr.status = ptrRecords.some(function (v) { return v === records.ptr.expected; });
|
||||
}
|
||||
|
||||
return callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function checkOutbound25(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var smtpServer = _.sample([
|
||||
'smtp.gmail.com',
|
||||
'smtp.live.com',
|
||||
'smtp.mail.yahoo.com',
|
||||
'smtp.o2.ie',
|
||||
'smtp.comcast.net',
|
||||
'outgoing.verizon.net'
|
||||
]);
|
||||
|
||||
outboundPort25 = {
|
||||
value: 'OK',
|
||||
status: false
|
||||
};
|
||||
|
||||
var client = new net.Socket();
|
||||
client.setTimeout(5000);
|
||||
client.connect(25, smtpServer);
|
||||
client.on('connect', function () {
|
||||
outboundPort25.status = true;
|
||||
outboundPort25.value = 'OK';
|
||||
client.destroy(); // do not use end() because it still triggers timeout
|
||||
callback();
|
||||
});
|
||||
client.on('timeout', function () {
|
||||
outboundPort25.status = false;
|
||||
outboundPort25.value = 'Connect to ' + smtpServer + ' timed out';
|
||||
client.destroy();
|
||||
callback(new Error('Timeout'));
|
||||
});
|
||||
client.on('error', function (error) {
|
||||
outboundPort25.status = false;
|
||||
outboundPort25.value = 'Connect to ' + smtpServer + ' failed: ' + error.message;
|
||||
client.destroy();
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
|
||||
function ignoreError(what, func) {
|
||||
return function (callback) {
|
||||
func(function (error) {
|
||||
if (error) debug('Ignored error - ' + what + ':', error);
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
async.parallel([
|
||||
ignoreError('mx', checkMx),
|
||||
ignoreError('spf', checkSpf),
|
||||
ignoreError('dmarc', checkDmarc),
|
||||
ignoreError('dkim', checkDkim),
|
||||
ignoreError('ptr', checkPtr),
|
||||
ignoreError('port25', checkOutbound25)
|
||||
], function () {
|
||||
callback(null, { dns: records, outboundPort25: outboundPort25 } );
|
||||
});
|
||||
}
|
||||
|
||||
function setAutoupdatePattern(pattern, callback) {
|
||||
assert.strictEqual(typeof pattern, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -299,16 +497,15 @@ function getDnsConfig(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function setDnsConfig(dnsConfig, domain, zoneName, callback) {
|
||||
function setDnsConfig(dnsConfig, domain, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
|
||||
|
||||
subdomains.verifyDnsConfig(dnsConfig, domain, zoneName, ip, function (error, result) {
|
||||
subdomains.verifyDnsConfig(dnsConfig, domain, ip, function (error, result) {
|
||||
if (error && error.reason === SubdomainError.ACCESS_DENIED) return callback(new SettingsError(SettingsError.BAD_FIELD, 'Error adding A record. Access denied'));
|
||||
if (error && error.reason === SubdomainError.NOT_FOUND) return callback(new SettingsError(SettingsError.BAD_FIELD, 'Zone not found'));
|
||||
if (error && error.reason === SubdomainError.EXTERNAL_ERROR) return callback(new SettingsError(SettingsError.BAD_FIELD, 'Error adding A record:' + error.message));
|
||||
@@ -461,56 +658,25 @@ function setMailConfig(mailConfig, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getMailRelay(callback) {
|
||||
function getEmailDigest(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settingsdb.get(exports.MAIL_RELAY_KEY, function (error, value) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.MAIL_RELAY_KEY]);
|
||||
settingsdb.get(exports.EMAIL_DIGEST, function (error, enabled) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.EMAIL_DIGEST]);
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, JSON.parse(value));
|
||||
callback(null, !!enabled); // settingsdb holds string values only
|
||||
});
|
||||
}
|
||||
|
||||
function setMailRelay(relay, callback) {
|
||||
assert.strictEqual(typeof relay, 'object');
|
||||
function setEmailDigest(enabled, callback) {
|
||||
assert.strictEqual(typeof enabled, 'boolean');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
email.verifyRelay(relay, function (error) {
|
||||
if (error && error.reason === EmailError.BAD_FIELD) return callback(new SettingsError(SettingsError.BAD_FIELD, error.message));
|
||||
settingsdb.set(exports.EMAIL_DIGEST, enabled ? 'enabled' : '', function (error) {
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
settingsdb.set(exports.MAIL_RELAY_KEY, JSON.stringify(relay), function (error) {
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
exports.events.emit(exports.MAIL_RELAY_KEY, relay);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
exports.events.emit(exports.EMAIL_DIGEST, enabled);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
|
||||
+6
-24
@@ -13,7 +13,6 @@ module.exports = exports = {
|
||||
var assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
settings = require('./settings.js'),
|
||||
tld = require('tldjs'),
|
||||
util = require('util');
|
||||
|
||||
function SubdomainError(reason, errorOrMessage) {
|
||||
@@ -59,17 +58,6 @@ function api(provider) {
|
||||
}
|
||||
}
|
||||
|
||||
function getName(subdomain) {
|
||||
// support special caas domains
|
||||
if (!config.isCustomDomain()) return subdomain;
|
||||
|
||||
if (config.fqdn() === config.zoneName()) return subdomain;
|
||||
|
||||
var part = config.fqdn().slice(0, -config.zoneName().length - 1);
|
||||
|
||||
return subdomain === '' ? part : subdomain + '.' + part;
|
||||
}
|
||||
|
||||
function get(subdomain, type, callback) {
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
@@ -78,7 +66,7 @@ function get(subdomain, type, callback) {
|
||||
settings.getDnsConfig(function (error, dnsConfig) {
|
||||
if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error));
|
||||
|
||||
api(dnsConfig.provider).get(dnsConfig, config.zoneName(), getName(subdomain), type, function (error, values) {
|
||||
api(dnsConfig.provider).get(dnsConfig, config.zoneName(), subdomain, type, function (error, values) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, values);
|
||||
@@ -95,7 +83,7 @@ function upsert(subdomain, type, values, callback) {
|
||||
settings.getDnsConfig(function (error, dnsConfig) {
|
||||
if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error));
|
||||
|
||||
api(dnsConfig.provider).upsert(dnsConfig, config.zoneName(), getName(subdomain), type, values, function (error, changeId) {
|
||||
api(dnsConfig.provider).upsert(dnsConfig, config.zoneName(), subdomain, type, values, function (error, changeId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, changeId);
|
||||
@@ -112,7 +100,7 @@ function remove(subdomain, type, values, callback) {
|
||||
settings.getDnsConfig(function (error, dnsConfig) {
|
||||
if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error));
|
||||
|
||||
api(dnsConfig.provider).del(dnsConfig, config.zoneName(), getName(subdomain), type, values, function (error) {
|
||||
api(dnsConfig.provider).del(dnsConfig, config.zoneName(), subdomain, type, values, function (error) {
|
||||
if (error && error.reason !== SubdomainError.NOT_FOUND) return callback(error);
|
||||
|
||||
callback(null);
|
||||
@@ -130,25 +118,19 @@ function waitForDns(domain, value, type, options, callback) {
|
||||
settings.getDnsConfig(function (error, dnsConfig) {
|
||||
if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error));
|
||||
|
||||
var zoneName = config.zoneName();
|
||||
|
||||
// if the domain is on another zone in case of external domain, use the correct zone
|
||||
if (!domain.endsWith(zoneName)) zoneName = tld.getDomain(domain);
|
||||
|
||||
api(dnsConfig.provider).waitForDns(domain, zoneName, value, type, options, callback);
|
||||
api(dnsConfig.provider).waitForDns(domain, value, type, options, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
|
||||
function verifyDnsConfig(dnsConfig, domain, ip, callback) {
|
||||
assert(dnsConfig && typeof dnsConfig === 'object'); // the dns config to test with
|
||||
assert(typeof dnsConfig.provider === 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var backend = api(dnsConfig.provider);
|
||||
if (!backend) return callback(new SubdomainError(SubdomainError.INVALID_PROVIDER));
|
||||
|
||||
api(dnsConfig.provider).verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback);
|
||||
api(dnsConfig.provider).verifyDnsConfig(dnsConfig, domain, ip, callback);
|
||||
}
|
||||
|
||||
@@ -68,8 +68,7 @@ var APP = {
|
||||
describe('apptask', function () {
|
||||
before(function (done) {
|
||||
config.set('version', '0.5.0');
|
||||
config.setFqdn('foobar.com');
|
||||
config.setZoneName('foobar.com');
|
||||
config.set('fqdn', 'foobar.com');
|
||||
config.set('provider', 'caas');
|
||||
|
||||
awsHostedZones = {
|
||||
@@ -91,7 +90,7 @@ describe('apptask', function () {
|
||||
database.initialize,
|
||||
appdb.add.bind(null, APP.id, APP.appStoreId, APP.manifest, APP.location, APP.portBindings, APP),
|
||||
settings.initialize,
|
||||
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }, config.fqdn(), config.zoneName()),
|
||||
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }, config.fqdn()),
|
||||
settings.setTlsConfig.bind(null, { provider: 'caas' })
|
||||
], done);
|
||||
});
|
||||
|
||||
@@ -41,7 +41,7 @@ describe('config', function () {
|
||||
expect(config.fqdn()).to.equal('localhost');
|
||||
expect(config.adminOrigin()).to.equal('https://' + constants.ADMIN_LOCATION + '.localhost');
|
||||
expect(config.appFqdn('app')).to.equal('app.localhost');
|
||||
expect(config.zoneName()).to.equal('');
|
||||
expect(config.zoneName()).to.equal('localhost');
|
||||
});
|
||||
|
||||
it('set saves value in file', function (done) {
|
||||
@@ -63,7 +63,7 @@ describe('config', function () {
|
||||
});
|
||||
|
||||
it('uses dotted locations with custom domain', function () {
|
||||
config.setFqdn('example.com');
|
||||
config.set('fqdn', 'example.com');
|
||||
config.set('isCustomDomain', true);
|
||||
|
||||
expect(config.isCustomDomain()).to.equal(true);
|
||||
@@ -74,7 +74,7 @@ describe('config', function () {
|
||||
});
|
||||
|
||||
it('uses hyphen locations with non-custom domain', function () {
|
||||
config.setFqdn('test.example.com');
|
||||
config.set('fqdn', 'test.example.com');
|
||||
config.set('isCustomDomain', false);
|
||||
|
||||
expect(config.isCustomDomain()).to.equal(false);
|
||||
@@ -95,3 +95,4 @@ describe('config', function () {
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/* 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'),
|
||||
digest = require('../digest.js'),
|
||||
eventlog = require('../eventlog.js'),
|
||||
expect = require('expect.js'),
|
||||
mailer = require('../mailer.js'),
|
||||
paths = require('../paths.js'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('../settings.js'),
|
||||
updatechecker = require('../updatechecker.js'),
|
||||
user = require('../user.js');
|
||||
|
||||
// owner
|
||||
var USER_0 = {
|
||||
username: 'username0',
|
||||
password: 'Username0pass?1234',
|
||||
email: 'user0@email.com',
|
||||
displayName: 'User 0'
|
||||
};
|
||||
|
||||
var AUDIT_SOURCE = {
|
||||
ip: '1.2.3.4'
|
||||
};
|
||||
|
||||
function checkMails(number, done) {
|
||||
// mails are enqueued async
|
||||
setTimeout(function () {
|
||||
expect(mailer._getMailQueue().length).to.equal(number);
|
||||
mailer._clearMailQueue();
|
||||
done();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
describe('digest', function () {
|
||||
function cleanup(done) {
|
||||
mailer._clearMailQueue();
|
||||
safe.fs.unlinkSync(paths.UPDATE_CHECKER_FILE);
|
||||
|
||||
async.series([
|
||||
settings.uninitialize,
|
||||
database._clear
|
||||
], done);
|
||||
}
|
||||
|
||||
before(function (done) {
|
||||
config._reset();
|
||||
config.set('version', '1.0.0');
|
||||
config.set('apiServerOrigin', 'http://localhost:4444');
|
||||
config.set('provider', 'notcaas');
|
||||
safe.fs.unlinkSync(paths.UPDATE_CHECKER_FILE);
|
||||
|
||||
async.series([
|
||||
database.initialize,
|
||||
database._clear,
|
||||
settings.initialize,
|
||||
user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE),
|
||||
eventlog.add.bind(null, eventlog.ACTION_UPDATE, AUDIT_SOURCE, { boxUpdateInfo: { sourceTarballUrl: 'xx', version: '1.2.3', changelog: [ 'good stuff' ] } }),
|
||||
mailer.start,
|
||||
mailer._clearMailQueue
|
||||
], done);
|
||||
});
|
||||
|
||||
after(cleanup);
|
||||
|
||||
describe('disabled', function () {
|
||||
before(function (done) {
|
||||
settings.setEmailDigest(false, done);
|
||||
});
|
||||
|
||||
it('does not send mail with digest disabled', function (done) {
|
||||
digest.maybeSend(function (error) {
|
||||
if (error) return done(error);
|
||||
checkMails(0, done);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('enabled', function () {
|
||||
before(function (done) {
|
||||
settings.setEmailDigest(true, done);
|
||||
});
|
||||
|
||||
it('sends mail for box update', function (done) {
|
||||
digest.maybeSend(function (error) {
|
||||
if (error) return done(error);
|
||||
|
||||
checkMails(1, done);
|
||||
});
|
||||
});
|
||||
|
||||
it('sends mail for pending update', function (done) {
|
||||
updatechecker._setUpdateInfo({ box: null, apps: { 'appid': { manifest: { version: '1.2.5', changelog: 'noop\nreally' } } } });
|
||||
|
||||
digest.maybeSend(function (error) {
|
||||
if (error) return done(error);
|
||||
|
||||
checkMails(1, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+18
-28
@@ -18,11 +18,10 @@ var async = require('async'),
|
||||
|
||||
describe('dns provider', function () {
|
||||
before(function (done) {
|
||||
config._reset();
|
||||
|
||||
async.series([
|
||||
database.initialize,
|
||||
settings.initialize
|
||||
settings.initialize,
|
||||
config._reset
|
||||
], done);
|
||||
});
|
||||
|
||||
@@ -36,10 +35,7 @@ describe('dns provider', function () {
|
||||
provider: 'noop'
|
||||
};
|
||||
|
||||
config.setFqdn('example.com');
|
||||
config.setZoneName('example.com');
|
||||
|
||||
settings.setDnsConfig(data, config.fqdn(), config.zoneName(), done);
|
||||
settings.setDnsConfig(data, config.fqdn(), done);
|
||||
});
|
||||
|
||||
it('upsert succeeds', function (done) {
|
||||
@@ -80,10 +76,7 @@ describe('dns provider', function () {
|
||||
token: TOKEN
|
||||
};
|
||||
|
||||
config.setFqdn('example.com');
|
||||
config.setZoneName('example.com');
|
||||
|
||||
settings.setDnsConfig(data, config.fqdn(), config.zoneName(), done);
|
||||
settings.setDnsConfig(data, config.fqdn(), done);
|
||||
});
|
||||
|
||||
it('upsert non-existing record succeeds', function (done) {
|
||||
@@ -100,10 +93,10 @@ describe('dns provider', function () {
|
||||
};
|
||||
|
||||
var req1 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; })
|
||||
.get('/v2/domains/' + config.zoneName() + '/records')
|
||||
.get('/v2/domains/localhost/records')
|
||||
.reply(200, { domain_records: [] });
|
||||
var req2 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; })
|
||||
.post('/v2/domains/' + config.zoneName() + '/records')
|
||||
.post('/v2/domains/localhost/records')
|
||||
.reply(201, { domain_record: DOMAIN_RECORD_0 });
|
||||
|
||||
subdomains.upsert('test', 'A', [ '1.2.3.4' ], function (error, result) {
|
||||
@@ -150,10 +143,10 @@ describe('dns provider', function () {
|
||||
};
|
||||
|
||||
var req1 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; })
|
||||
.get('/v2/domains/' + config.zoneName() + '/records')
|
||||
.get('/v2/domains/localhost/records')
|
||||
.reply(200, { domain_records: [ DOMAIN_RECORD_0, DOMAIN_RECORD_1 ] });
|
||||
var req2 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; })
|
||||
.put('/v2/domains/' + config.zoneName() + '/records/' + DOMAIN_RECORD_1.id)
|
||||
.put('/v2/domains/localhost/records/' + DOMAIN_RECORD_1.id)
|
||||
.reply(200, { domain_records: DOMAIN_RECORD_1_NEW });
|
||||
|
||||
subdomains.upsert('test', 'A', [ DOMAIN_RECORD_1_NEW.data ], function (error, result) {
|
||||
@@ -230,16 +223,16 @@ describe('dns provider', function () {
|
||||
};
|
||||
|
||||
var req1 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; })
|
||||
.get('/v2/domains/' + config.zoneName() + '/records')
|
||||
.get('/v2/domains/localhost/records')
|
||||
.reply(200, { domain_records: [ DOMAIN_RECORD_0, DOMAIN_RECORD_1, DOMAIN_RECORD_2 ] });
|
||||
var req2 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; })
|
||||
.put('/v2/domains/' + config.zoneName() + '/records/' + DOMAIN_RECORD_1.id)
|
||||
.put('/v2/domains/localhost/records/' + DOMAIN_RECORD_1.id)
|
||||
.reply(200, { domain_records: DOMAIN_RECORD_1_NEW });
|
||||
var req3 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; })
|
||||
.put('/v2/domains/' + config.zoneName() + '/records/' + DOMAIN_RECORD_2.id)
|
||||
.put('/v2/domains/localhost/records/' + DOMAIN_RECORD_2.id)
|
||||
.reply(200, { domain_records: DOMAIN_RECORD_2_NEW });
|
||||
var req4 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; })
|
||||
.post('/v2/domains/' + config.zoneName() + '/records')
|
||||
.post('/v2/domains/localhost/records')
|
||||
.reply(201, { domain_records: DOMAIN_RECORD_2_NEW });
|
||||
|
||||
subdomains.upsert('', 'TXT', [ DOMAIN_RECORD_2_NEW.data, DOMAIN_RECORD_1_NEW.data, DOMAIN_RECORD_3_NEW.data ], function (error, result) {
|
||||
@@ -278,7 +271,7 @@ describe('dns provider', function () {
|
||||
};
|
||||
|
||||
var req1 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; })
|
||||
.get('/v2/domains/' + config.zoneName() + '/records')
|
||||
.get('/v2/domains/localhost/records')
|
||||
.reply(200, { domain_records: [ DOMAIN_RECORD_0, DOMAIN_RECORD_1 ] });
|
||||
|
||||
subdomains.get('test', 'A', function (error, result) {
|
||||
@@ -316,10 +309,10 @@ describe('dns provider', function () {
|
||||
};
|
||||
|
||||
var req1 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; })
|
||||
.get('/v2/domains/' + config.zoneName() + '/records')
|
||||
.get('/v2/domains/localhost/records')
|
||||
.reply(200, { domain_records: [ DOMAIN_RECORD_0, DOMAIN_RECORD_1 ] });
|
||||
var req2 = nock(DIGITALOCEAN_ENDPOINT).filteringRequestBody(function () { return false; })
|
||||
.delete('/v2/domains/' + config.zoneName() + '/records/' + DOMAIN_RECORD_1.id)
|
||||
.delete('/v2/domains/localhost/records/' + DOMAIN_RECORD_1.id)
|
||||
.reply(204, {});
|
||||
|
||||
subdomains.remove('test', 'A', ['1.2.3.4'], function (error) {
|
||||
@@ -333,16 +326,13 @@ describe('dns provider', function () {
|
||||
});
|
||||
|
||||
describe('route53', function () {
|
||||
config.setFqdn('example.com');
|
||||
config.setZoneName('example.com');
|
||||
|
||||
// do not clear this with [] but .length = 0 so we don't loose the reference in mockery
|
||||
var awsAnswerQueue = [];
|
||||
|
||||
var AWS_HOSTED_ZONES = {
|
||||
HostedZones: [{
|
||||
Id: '/hostedzone/Z34G16B38TNZ9L',
|
||||
Name: config.zoneName() + '.',
|
||||
Name: 'localhost.',
|
||||
CallerReference: '305AFD59-9D73-4502-B020-F4E6F889CB30',
|
||||
ResourceRecordSetCount: 2,
|
||||
ChangeInfo: {
|
||||
@@ -411,7 +401,7 @@ describe('dns provider', function () {
|
||||
AWS._originalRoute53 = AWS.Route53;
|
||||
AWS.Route53 = Route53Mock;
|
||||
|
||||
settings.setDnsConfig(data, config.fqdn(), config.zoneName(), done);
|
||||
settings.setDnsConfig(data, config.fqdn(), done);
|
||||
});
|
||||
|
||||
after(function () {
|
||||
@@ -480,7 +470,7 @@ describe('dns provider', function () {
|
||||
awsAnswerQueue.push([null, AWS_HOSTED_ZONES]);
|
||||
awsAnswerQueue.push([null, {
|
||||
ResourceRecordSets: [{
|
||||
Name: 'test.' + config.zoneName() + '.',
|
||||
Name: 'test.localhost.',
|
||||
Type: 'A',
|
||||
ResourceRecords: [{
|
||||
Value: '1.2.3.4'
|
||||
|
||||
@@ -9,8 +9,7 @@ var async = require('async'),
|
||||
config = require('../config.js'),
|
||||
database = require('../database.js'),
|
||||
expect = require('expect.js'),
|
||||
settings = require('../settings.js'),
|
||||
settingsdb = require('../settingsdb.js');
|
||||
settings = require('../settings.js');
|
||||
|
||||
function setup(done) {
|
||||
config.set('provider', 'caas');
|
||||
@@ -96,7 +95,7 @@ describe('Settings', function () {
|
||||
});
|
||||
|
||||
it('can set dns config', function (done) {
|
||||
settings.setDnsConfig({ provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey' }, config.fqdn(), config.zoneName(), function (error) {
|
||||
settings.setDnsConfig({ provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey' }, config.fqdn(), function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
@@ -182,47 +181,21 @@ describe('Settings', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('can get mail relay', function (done) {
|
||||
settings.getMailRelay(function (error, address) {
|
||||
it('can enable mail digest', function (done) {
|
||||
settings.setEmailDigest(true, function (error) {
|
||||
expect(error).to.be(null);
|
||||
expect(address).to.eql({ provider: 'cloudron-smtp' });
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can set mail relay', function (done) {
|
||||
var relay = { provider: 'external-smtp', host: 'mx.foo.com', port: 25 };
|
||||
+ settingsdb.set(settings.MAIL_RELAY_KEY, JSON.stringify(relay), function (error) { // skip the mail server verify()
|
||||
it('can get mail digest', function (done) {
|
||||
settings.getEmailDigest(function (error, enabled) {
|
||||
expect(error).to.be(null);
|
||||
|
||||
settings.getMailRelay(function (error, address) {
|
||||
expect(error).to.be(null);
|
||||
expect(address).to.eql(relay);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can get catch all address', function (done) {
|
||||
settings.getCatchAllAddress(function (error, address) {
|
||||
expect(error).to.be(null);
|
||||
expect(address).to.eql([ ]);
|
||||
expect(enabled).to.be(true);
|
||||
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);
|
||||
|
||||
@@ -43,7 +43,6 @@ function checkMails(number, done) {
|
||||
|
||||
function cleanup(done) {
|
||||
mailer._clearMailQueue();
|
||||
safe.fs.unlinkSync(paths.UPDATE_CHECKER_FILE);
|
||||
|
||||
async.series([
|
||||
settings.uninitialize,
|
||||
@@ -51,7 +50,7 @@ function cleanup(done) {
|
||||
], done);
|
||||
}
|
||||
|
||||
describe('updatechecker - box - manual (email)', function () {
|
||||
describe('updatechecker - box - manual (mail)', function () {
|
||||
before(function (done) {
|
||||
config._reset();
|
||||
config.set('version', '1.0.0');
|
||||
@@ -97,17 +96,11 @@ describe('updatechecker - box - manual (email)', function () {
|
||||
.query({ boxVersion: config.version(), accessToken: 'token' })
|
||||
.reply(200, { version: '2.0.0', changelog: [''], sourceTarballUrl: '2.0.0.tar.gz' } );
|
||||
|
||||
var scope2 = nock('http://localhost:4444')
|
||||
.get('/api/v1/users/uid/cloudrons/cid/subscription')
|
||||
.query({ accessToken: 'token' })
|
||||
.reply(200, { subscription: { plan: { id: 'pro' } } } );
|
||||
|
||||
updatechecker.checkBoxUpdates(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
expect(updatechecker.getUpdateInfo().box.version).to.be('2.0.0');
|
||||
expect(updatechecker.getUpdateInfo().box.sourceTarballUrl).to.be('2.0.0.tar.gz');
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
checkMails(1, done);
|
||||
});
|
||||
@@ -141,16 +134,10 @@ describe('updatechecker - box - manual (email)', function () {
|
||||
.query({ boxVersion: config.version(), accessToken: 'token' })
|
||||
.reply(200, { version: '2.0.0-pre.0', changelog: [''], sourceTarballUrl: '2.0.0-pre.0.tar.gz' } );
|
||||
|
||||
var scope2 = nock('http://localhost:4444')
|
||||
.get('/api/v1/users/uid/cloudrons/cid/subscription')
|
||||
.query({ accessToken: 'token' })
|
||||
.reply(200, { subscription: { plan: { id: 'pro' } } } );
|
||||
|
||||
updatechecker.checkBoxUpdates(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
expect(updatechecker.getUpdateInfo().box.version).to.be('2.0.0-pre.0');
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
checkMails(1, done);
|
||||
});
|
||||
@@ -175,7 +162,7 @@ describe('updatechecker - box - manual (email)', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatechecker - box - automatic (no email)', function () {
|
||||
describe('updatechecker - box - automatic', function () {
|
||||
before(function (done) {
|
||||
config.set('version', '1.0.0');
|
||||
config.set('apiServerOrigin', 'http://localhost:4444');
|
||||
@@ -199,63 +186,17 @@ describe('updatechecker - box - automatic (no email)', function () {
|
||||
.query({ boxVersion: config.version(), accessToken: 'token' })
|
||||
.reply(200, { version: '2.0.0', sourceTarballUrl: '2.0.0.tar.gz' } );
|
||||
|
||||
var scope2 = nock('http://localhost:4444')
|
||||
.get('/api/v1/users/uid/cloudrons/cid/subscription')
|
||||
.query({ accessToken: 'token' })
|
||||
.reply(200, { subscription: { plan: { id: 'pro' } } } );
|
||||
|
||||
updatechecker.checkBoxUpdates(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
expect(updatechecker.getUpdateInfo().box.version).to.be('2.0.0');
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
checkMails(0, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatechecker - box - automatic free (email)', function () {
|
||||
before(function (done) {
|
||||
config.set('version', '1.0.0');
|
||||
config.set('apiServerOrigin', 'http://localhost:4444');
|
||||
config.set('provider', 'notcaas');
|
||||
async.series([
|
||||
database.initialize,
|
||||
settings.initialize,
|
||||
mailer._clearMailQueue,
|
||||
user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE),
|
||||
settingsdb.set.bind(null, settings.APPSTORE_CONFIG_KEY, JSON.stringify({ userId: 'uid', cloudronId: 'cid', token: 'token' }))
|
||||
], done);
|
||||
});
|
||||
|
||||
after(cleanup);
|
||||
|
||||
it('new version', function (done) {
|
||||
nock.cleanAll();
|
||||
|
||||
var scope = nock('http://localhost:4444')
|
||||
.get('/api/v1/users/uid/cloudrons/cid/boxupdate')
|
||||
.query({ boxVersion: config.version(), accessToken: 'token' })
|
||||
.reply(200, { version: '2.0.0', changelog: [''], sourceTarballUrl: '2.0.0.tar.gz' } );
|
||||
|
||||
var scope2 = nock('http://localhost:4444')
|
||||
.get('/api/v1/users/uid/cloudrons/cid/subscription')
|
||||
.query({ accessToken: 'token' })
|
||||
.reply(200, { subscription: { plan: { id: 'free' } } } );
|
||||
|
||||
updatechecker.checkBoxUpdates(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
expect(updatechecker.getUpdateInfo().box.version).to.be('2.0.0');
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
checkMails(1, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatechecker - app - manual (email)', function () {
|
||||
describe('updatechecker - app - manual (mails)', function () {
|
||||
var APP_0 = {
|
||||
id: 'appid-0',
|
||||
appStoreId: 'io.cloudron.app',
|
||||
@@ -341,16 +282,10 @@ describe('updatechecker - app - manual (email)', function () {
|
||||
.query({ boxVersion: config.version(), accessToken: 'token', appId: APP_0.appStoreId, appVersion: APP_0.manifest.version })
|
||||
.reply(200, { manifest: { version: '2.0.0' } } );
|
||||
|
||||
var scope2 = nock('http://localhost:4444')
|
||||
.get('/api/v1/users/uid/cloudrons/cid/subscription')
|
||||
.query({ accessToken: 'token' })
|
||||
.reply(200, { subscription: { plan: { id: 'pro' } } } );
|
||||
|
||||
updatechecker.checkAppUpdates(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
expect(updatechecker.getUpdateInfo().apps).to.eql({ 'appid-0': { manifest: { version: '2.0.0' } } });
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
checkMails(1, done);
|
||||
});
|
||||
@@ -367,7 +302,7 @@ describe('updatechecker - app - manual (email)', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatechecker - app - automatic (no email)', function () {
|
||||
describe('updatechecker - app - automatic (no emails)', function () {
|
||||
var APP_0 = {
|
||||
id: 'appid-0',
|
||||
appStoreId: 'io.cloudron.app',
|
||||
@@ -427,70 +362,3 @@ describe('updatechecker - app - automatic (no email)', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatechecker - app - automatic free (email)', function () {
|
||||
var APP_0 = {
|
||||
id: 'appid-0',
|
||||
appStoreId: 'io.cloudron.app',
|
||||
installationState: appdb.ISTATE_PENDING_INSTALL,
|
||||
installationProgress: null,
|
||||
runState: null,
|
||||
location: 'some-location-0',
|
||||
manifest: {
|
||||
version: '1.0.0', dockerImage: 'docker/app0', healthCheckPath: '/', httpPort: 80, title: 'app0',
|
||||
tcpPorts: {
|
||||
PORT: {
|
||||
description: 'this is a port that i expose',
|
||||
containerPort: '1234'
|
||||
}
|
||||
}
|
||||
},
|
||||
httpPort: null,
|
||||
containerId: null,
|
||||
portBindings: { PORT: 5678 },
|
||||
healthy: null,
|
||||
accessRestriction: null,
|
||||
memoryLimit: 0
|
||||
};
|
||||
|
||||
before(function (done) {
|
||||
config.set('version', '1.0.0');
|
||||
config.set('apiServerOrigin', 'http://localhost:4444');
|
||||
config.set('provider', 'notcaas');
|
||||
|
||||
async.series([
|
||||
database.initialize,
|
||||
database._clear,
|
||||
settings.initialize,
|
||||
mailer._clearMailQueue,
|
||||
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0),
|
||||
user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE),
|
||||
settingsdb.set.bind(null, settings.APPSTORE_CONFIG_KEY, JSON.stringify({ userId: 'uid', cloudronId: 'cid', token: 'token' }))
|
||||
], done);
|
||||
});
|
||||
|
||||
after(cleanup);
|
||||
|
||||
it('offers new version', function (done) {
|
||||
nock.cleanAll();
|
||||
|
||||
var scope = nock('http://localhost:4444')
|
||||
.get('/api/v1/users/uid/cloudrons/cid/appupdate')
|
||||
.query({ boxVersion: config.version(), accessToken: 'token', appId: APP_0.appStoreId, appVersion: APP_0.manifest.version })
|
||||
.reply(200, { manifest: { version: '2.0.0' } } );
|
||||
|
||||
var scope2 = nock('http://localhost:4444')
|
||||
.get('/api/v1/users/uid/cloudrons/cid/subscription')
|
||||
.query({ accessToken: 'token' })
|
||||
.reply(200, { subscription: { plan: { id: 'free' } } } );
|
||||
|
||||
updatechecker.checkAppUpdates(function (error) {
|
||||
expect(!error).to.be.ok();
|
||||
expect(updatechecker.getUpdateInfo().apps).to.eql({ 'appid-0': { manifest: { version: '2.0.0' } } });
|
||||
expect(scope.isDone()).to.be.ok();
|
||||
expect(scope2.isDone()).to.be.ok();
|
||||
|
||||
checkMails(1, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+24
-44
@@ -6,7 +6,9 @@ exports = module.exports = {
|
||||
|
||||
getUpdateInfo: getUpdateInfo,
|
||||
resetUpdateInfo: resetUpdateInfo,
|
||||
resetAppUpdateInfo: resetAppUpdateInfo
|
||||
resetAppUpdateInfo: resetAppUpdateInfo,
|
||||
|
||||
_setUpdateInfo: setUpdateInfo
|
||||
};
|
||||
|
||||
var apps = require('./apps.js'),
|
||||
@@ -41,6 +43,11 @@ function getUpdateInfo() {
|
||||
};
|
||||
}
|
||||
|
||||
function setUpdateInfo(info) {
|
||||
gBoxUpdateInfo = info.box;
|
||||
gAppUpdateInfo = info.apps;
|
||||
}
|
||||
|
||||
function resetUpdateInfo() {
|
||||
gBoxUpdateInfo = null;
|
||||
resetAppUpdateInfo();
|
||||
@@ -95,34 +102,19 @@ function checkAppUpdates(callback) {
|
||||
|
||||
if (oldState[app.id] === newState[app.id]) {
|
||||
debug('Skipping notification of app update %s since user was already notified', app.id);
|
||||
return iteratorDone();
|
||||
}
|
||||
|
||||
appstore.getSubscription(function (error, result) {
|
||||
if (error) {
|
||||
debug('Error getting subscription for %s', app.id, error);
|
||||
return iteratorDone();
|
||||
}
|
||||
|
||||
// always send notifications if user is on the free plan
|
||||
if (result.plan.id === 'free' || result.plan.id === 'undecided') {
|
||||
debug('Notifying user of app update for %s from %s to %s', app.id, app.manifest.version, updateInfo.manifest.version);
|
||||
mailer.appUpdateAvailable(app, updateInfo);
|
||||
return iteratorDone();
|
||||
}
|
||||
|
||||
} else {
|
||||
// only send notifications if update pattern is 'never'
|
||||
settings.getAutoupdatePattern(function (error, result) {
|
||||
if (error) {
|
||||
debug(error);
|
||||
} else if (result === constants.AUTOUPDATE_PATTERN_NEVER) {
|
||||
debug('Notifying user of app update for %s from %s to %s', app.id, app.manifest.version, updateInfo.manifest.version);
|
||||
mailer.appUpdateAvailable(app, updateInfo);
|
||||
}
|
||||
if (error) return debug(error);
|
||||
if (result !== constants.AUTOUPDATE_PATTERN_NEVER) return;
|
||||
|
||||
iteratorDone();
|
||||
debug('Notifying user of app update for %s from %s to %s', app.id, app.manifest.version, updateInfo.manifest.version);
|
||||
|
||||
mailer.appUpdateAvailable(app, updateInfo);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
iteratorDone();
|
||||
});
|
||||
}, function () {
|
||||
newState.box = loadState().box; // preserve the latest box state information
|
||||
@@ -162,28 +154,16 @@ function checkBoxUpdates(callback) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
appstore.getSubscription(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
state.box = updateInfo.version;
|
||||
|
||||
function done() {
|
||||
state.box = updateInfo.version;
|
||||
saveState(state);
|
||||
callback();
|
||||
}
|
||||
saveState(state);
|
||||
|
||||
// always send notifications if user is on the free plan
|
||||
if (result.plan.id === 'free' || result.plan.id === 'undecided') {
|
||||
mailer.boxUpdateAvailable(updateInfo.version, updateInfo.changelog);
|
||||
return done();
|
||||
}
|
||||
// only send notifications if update pattern is 'never'
|
||||
settings.getAutoupdatePattern(function (error, result) {
|
||||
if (error) debug(error);
|
||||
else if (result === constants.AUTOUPDATE_PATTERN_NEVER) mailer.boxUpdateAvailable(updateInfo.version, updateInfo.changelog);
|
||||
|
||||
// only send notifications if update pattern is 'never'
|
||||
settings.getAutoupdatePattern(function (error, result) {
|
||||
if (error) debug(error);
|
||||
else if (result === constants.AUTOUPDATE_PATTERN_NEVER) mailer.boxUpdateAvailable(updateInfo.version, updateInfo.changelog);
|
||||
|
||||
done();
|
||||
});
|
||||
callback();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,354 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
angular.module("ui.multiselect", ["multiselect.tpl.html"])
|
||||
//from bootstrap-ui typeahead parser
|
||||
.factory("optionParser", ["$parse", function($parse) {
|
||||
// 00000111000000000000022200000000000000003333333333333330000000000044000
|
||||
var TYPEAHEAD_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/;
|
||||
|
||||
return {
|
||||
parse: function(input) {
|
||||
|
||||
var match = input.match(TYPEAHEAD_REGEXP);
|
||||
if(!match) {
|
||||
throw new Error("Expected typeahead specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_'" + " but got '" + input + "'.");
|
||||
}
|
||||
|
||||
return {
|
||||
itemName : match[3],
|
||||
source : $parse(match[4]),
|
||||
viewMapper : $parse(match[2] || match[1]),
|
||||
modelMapper: $parse(match[1])
|
||||
};
|
||||
}
|
||||
};
|
||||
}])
|
||||
.directive("multiselect", ["$parse", "$document", "$compile", "$interpolate", "optionParser", function($parse, $document, $compile, $interpolate, optionParser) {
|
||||
return {
|
||||
restrict: "E",
|
||||
require : "ngModel",
|
||||
link : function(originalScope, element, attrs, modelCtrl) {
|
||||
|
||||
var exp = attrs.options;
|
||||
var parsedResult = optionParser.parse(exp);
|
||||
var isMultiple = attrs.multiple ? true : false;
|
||||
var compareByKey = attrs.compareBy;
|
||||
var headerKey = attrs.headerKey;
|
||||
var dividerKey = attrs.dividerKey;
|
||||
var scrollAfterRows = attrs.scrollAfterRows;
|
||||
var tabindex = attrs.tabindex;
|
||||
var maxWidth = attrs.maxWidth;
|
||||
var required = false;
|
||||
var scope = originalScope.$new();
|
||||
scope.filterAfterRows = attrs.filterAfterRows;
|
||||
var changeHandler = attrs.change || angular.noop;
|
||||
|
||||
scope.items = [];
|
||||
scope.header = "Select";
|
||||
scope.multiple = isMultiple;
|
||||
scope.disabled = false;
|
||||
|
||||
scope.ulStyle = {};
|
||||
if(scrollAfterRows !== undefined && parseInt(scrollAfterRows).toString() === scrollAfterRows) {
|
||||
scope.ulStyle = {"max-height": (scrollAfterRows*26+14)+"px", "overflow-y": "auto", "overflow-x": "hidden"};
|
||||
}
|
||||
if(tabindex !== undefined && parseInt(tabindex).toString() === tabindex) {
|
||||
scope.tabindex = tabindex;
|
||||
}
|
||||
if(maxWidth !== undefined && parseInt(maxWidth).toString() === maxWidth) {
|
||||
scope.maxWidth = {"max-width": maxWidth + "px"};
|
||||
}
|
||||
|
||||
originalScope.$on("$destroy", function() {
|
||||
scope.$destroy();
|
||||
});
|
||||
|
||||
var popUpEl = angular.element("<multiselect-popup></multiselect-popup>");
|
||||
|
||||
//required validator
|
||||
if(attrs.required || attrs.ngRequired) {
|
||||
required = true;
|
||||
}
|
||||
attrs.$observe("required", function(newVal) {
|
||||
required = newVal;
|
||||
});
|
||||
|
||||
//watch disabled state
|
||||
scope.$watch(function() {
|
||||
return $parse(attrs.ngDisabled)(originalScope);
|
||||
}, function(newVal) {
|
||||
scope.disabled = newVal;
|
||||
});
|
||||
|
||||
//watch single/multiple state for dynamically change single to multiple
|
||||
scope.$watch(function() {
|
||||
return $parse(attrs.multiple)(originalScope);
|
||||
}, function(newVal) {
|
||||
isMultiple = newVal || false;
|
||||
});
|
||||
|
||||
//watch option changes for options that are populated dynamically
|
||||
scope.$watch(function() {
|
||||
return parsedResult.source(originalScope);
|
||||
}, function(newVal) {
|
||||
if(angular.isDefined(newVal)) {
|
||||
parseModel();
|
||||
}
|
||||
}, true);
|
||||
|
||||
//watch model change
|
||||
scope.$watch(function() {
|
||||
return modelCtrl.$modelValue;
|
||||
}, function(newVal, oldVal) {
|
||||
//when directive initialize, newVal usually undefined. Also, if model value already set in the controller
|
||||
//for preselected list then we need to mark checked in our scope item. But we don't want to do this every time
|
||||
//model changes. We need to do this only if it is done outside directive scope, from controller, for example.
|
||||
if(angular.isDefined(newVal)) {
|
||||
markChecked(newVal);
|
||||
scope.$eval(changeHandler);
|
||||
}
|
||||
getHeaderText();
|
||||
modelCtrl.$setValidity("required", scope.valid());
|
||||
});
|
||||
|
||||
function parseModel() {
|
||||
scope.items.length = 0;
|
||||
var model = parsedResult.source(originalScope);
|
||||
if(!angular.isDefined(model) || model === null) {
|
||||
return;
|
||||
}
|
||||
for(var i = 0; i < model.length; i++) {
|
||||
var local = {};
|
||||
local[parsedResult.itemName] = model[i];
|
||||
|
||||
// calculate checked status of the option
|
||||
// https://github.com/sebastianha/angular-bootstrap-multiselect/pull/4/files
|
||||
var id = model[i];
|
||||
var checked = false;
|
||||
var modelValue = modelCtrl.$modelValue;
|
||||
if (modelValue) {
|
||||
if (angular.isArray(modelValue)) {
|
||||
for (var j = 0; j < modelValue.length; j++)
|
||||
if (modelValue[j] == id) {
|
||||
checked = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
checked = modelValue == id;
|
||||
}
|
||||
}
|
||||
|
||||
scope.items.push({
|
||||
label : parsedResult.viewMapper(local),
|
||||
model : model[i],
|
||||
checked: checked,
|
||||
header : model[i][headerKey],
|
||||
divider : model[i][dividerKey]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
parseModel();
|
||||
|
||||
element.append($compile(popUpEl)(scope));
|
||||
|
||||
function getHeaderText() {
|
||||
if(isEmpty(modelCtrl.$modelValue)) {
|
||||
scope.header = attrs.msHeader || "Select";
|
||||
return scope.header;
|
||||
}
|
||||
|
||||
if(isMultiple) {
|
||||
if(attrs.msSelected) {
|
||||
scope.header = $interpolate(attrs.msSelected)(scope);
|
||||
} else {
|
||||
scope.header = modelCtrl.$modelValue.length + " " + "selected";
|
||||
}
|
||||
} else {
|
||||
var local = {};
|
||||
local[parsedResult.itemName] = modelCtrl.$modelValue;
|
||||
scope.header = parsedResult.viewMapper(local);
|
||||
}
|
||||
}
|
||||
|
||||
function isEmpty(obj) {
|
||||
if(obj === true || obj === false) {
|
||||
return false;
|
||||
}
|
||||
if(!obj) {
|
||||
return true;
|
||||
}
|
||||
if(obj.length && obj.length > 0) {
|
||||
return false;
|
||||
}
|
||||
for(var prop in obj) {
|
||||
if(obj[prop]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if(compareByKey !== undefined && obj[compareByKey] !== undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
scope.valid = function validModel() {
|
||||
if(!required) {
|
||||
return true;
|
||||
}
|
||||
var value = modelCtrl.$modelValue;
|
||||
return (angular.isArray(value) && value.length > 0) || (!angular.isArray(value) && value !== null);
|
||||
};
|
||||
|
||||
function selectSingle(item) {
|
||||
if(!item.checked) {
|
||||
scope.uncheckAll();
|
||||
item.checked = !item.checked;
|
||||
}
|
||||
setModelValue(false);
|
||||
}
|
||||
|
||||
function selectMultiple(item) {
|
||||
item.checked = !item.checked;
|
||||
setModelValue(true);
|
||||
}
|
||||
|
||||
function setModelValue(isMultiple) {
|
||||
var value;
|
||||
|
||||
if(isMultiple) {
|
||||
value = [];
|
||||
angular.forEach(scope.items, function(item) {
|
||||
if(item.checked) {
|
||||
value.push(item.model);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
angular.forEach(scope.items, function(item) {
|
||||
if(item.checked) {
|
||||
value = item.model;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
modelCtrl.$setViewValue(value);
|
||||
}
|
||||
|
||||
function markChecked(newVal) {
|
||||
if(!angular.isArray(newVal)) {
|
||||
angular.forEach(scope.items, function(item) {
|
||||
item.checked = false;
|
||||
if(compareByKey === undefined && angular.equals(item.model, newVal)) {
|
||||
item.checked = true;
|
||||
} else if(compareByKey !== undefined && newVal !== null && item.model[compareByKey] !== undefined && angular.equals(item.model[compareByKey], newVal[compareByKey])) {
|
||||
item.checked = true;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
angular.forEach(scope.items, function(item) {
|
||||
item.checked = false;
|
||||
angular.forEach(newVal, function(i) {
|
||||
if(compareByKey === undefined && angular.equals(item.model, i)) {
|
||||
item.checked = true;
|
||||
} else if(compareByKey !== undefined && item.model[compareByKey] !== undefined && angular.equals(item.model[compareByKey], i[compareByKey])) {
|
||||
item.checked = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
scope.checkAll = function() {
|
||||
if(!isMultiple) {
|
||||
return;
|
||||
}
|
||||
angular.forEach(scope.items, function(item) {
|
||||
item.checked = true;
|
||||
});
|
||||
setModelValue(true);
|
||||
};
|
||||
|
||||
scope.uncheckAll = function() {
|
||||
angular.forEach(scope.items, function(item) {
|
||||
item.checked = false;
|
||||
});
|
||||
setModelValue(true);
|
||||
};
|
||||
|
||||
scope.select = function(event, item) {
|
||||
if(isMultiple === false) {
|
||||
selectSingle(item);
|
||||
scope.toggleSelect();
|
||||
} else {
|
||||
event.stopPropagation();
|
||||
selectMultiple(item);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}])
|
||||
.directive("multiselectPopup", ["$document", function($document) {
|
||||
return {
|
||||
restrict : "E",
|
||||
scope : false,
|
||||
replace : true,
|
||||
templateUrl: "multiselect.tpl.html",
|
||||
link : function(scope, element, attrs) {
|
||||
|
||||
scope.isVisible = false;
|
||||
|
||||
scope.toggleSelect = function() {
|
||||
if(element.hasClass("open")) {
|
||||
scope.filter = "";
|
||||
element.removeClass("open");
|
||||
$document.unbind("click", clickHandler);
|
||||
} else {
|
||||
scope.filter = "";
|
||||
element.addClass("open");
|
||||
$document.bind("click", clickHandler);
|
||||
}
|
||||
};
|
||||
|
||||
// $("ul.dropdown-menu").on("click", "[data-stopPropagation]", function(e) {
|
||||
// e.stopPropagation();
|
||||
// });
|
||||
|
||||
function clickHandler(event) {
|
||||
if(elementMatchesAnyInArray(event.target, element.find(event.target.tagName))) {
|
||||
return;
|
||||
}
|
||||
element.removeClass("open");
|
||||
$document.unbind("click", clickHandler);
|
||||
scope.$apply();
|
||||
}
|
||||
|
||||
var elementMatchesAnyInArray = function(element, elementArray) {
|
||||
for(var i = 0; i < elementArray.length; i++) {
|
||||
if(element === elementArray[i]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
angular.module("multiselect.tpl.html", []).run(["$templateCache", function($templateCache) {
|
||||
$templateCache.put("multiselect.tpl.html",
|
||||
"<div class=\"btn-group\">\n" +
|
||||
" <button tabindex=\"{{tabindex}}\" title=\"{{header}}\" type=\"button\" class=\"btn btn-default dropdown-toggle\" ng-click=\"toggleSelect()\" ng-disabled=\"disabled\" ng-class=\"{'error': !valid()}\">\n" +
|
||||
" <div ng-style=\"maxWidth\" style=\"padding-right: 13px; overflow: hidden; text-overflow: ellipsis;\">{{header}}</div><span class=\"caret\" style=\"position:absolute;right:10px;top:14px;\"></span>\n" +
|
||||
" </button>\n" +
|
||||
" <ul class=\"dropdown-menu\" style=\"margin-bottom:30px;padding-left:5px;padding-right:5px;\" ng-style=\"ulStyle\">\n" +
|
||||
" <input ng-show=\"items.length > filterAfterRows\" ng-model=\"filter\" style=\"padding: 0px 3px;margin-right: 15px; margin-bottom: 4px;\" placeholder=\"Type to filter options\">" +
|
||||
" <li data-stopPropagation=\"true\" ng-repeat=\"i in items | filter:filter\" ng-class=\"{'dropdown-header': i.header, 'divider': i.divider}\">\n" +
|
||||
" <a ng-if=\"!i.header && !i.divider\" ng-click=\"select($event, i)\" style=\"padding:3px 10px;cursor:pointer;\">\n" +
|
||||
" <i class=\"fa\" ng-class=\"{'fa-check': i.checked, 'empty': !i.checked}\"></i> {{i.label}}" +
|
||||
" </a>\n" +
|
||||
" <span ng-if=\"i.header\">{{i.label}}</span>" +
|
||||
" </li>\n" +
|
||||
" </ul>\n" +
|
||||
"</div>");
|
||||
}]);
|
||||
@@ -62,10 +62,6 @@
|
||||
<script type="text/javascript" src="/3rdparty/bootstrap-slider/bootstrap-slider.min.js"></script>
|
||||
<script type="text/javascript" src="/3rdparty/bootstrap-slider/slider.js"></script>
|
||||
|
||||
<!-- Anugular Multiselect -->
|
||||
<!-- https://github.com/sebastianha/angular-bootstrap-multiselect -->
|
||||
<script src="/3rdparty/js/angular-bootstrap-multiselect.js"></script>
|
||||
|
||||
<!-- Main Application -->
|
||||
<script src="js/index.js"></script>
|
||||
|
||||
@@ -192,7 +188,7 @@
|
||||
</li>
|
||||
<li ng-show="appstoreConfig && !config.update.box && user.admin && (currentSubscription.plan.id === 'undecided' || currentSubscription.plan.id === 'free')">
|
||||
<a ng-href="" ng-click="showSubscriptionModal()" style="cursor: pointer">
|
||||
<span class="badge badge-success">Setup Subscription</span>
|
||||
<span class="badge badge-success">Setup automatic updates</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
|
||||
@@ -404,20 +404,6 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
}
|
||||
};
|
||||
|
||||
Client.prototype.setCatchallAddresses = function (addresses, callback) {
|
||||
put('/api/v1/settings/catch_all_address', { address: addresses }).success(function(data, status) {
|
||||
if (status !== 202) return callback(new ClientError(status, data));
|
||||
callback(null);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.getCatchallAddresses = function (callback) {
|
||||
get('/api/v1/settings/catch_all_address').success(function(data, status) {
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
callback(null, data.address);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.setBackupConfig = function (backupConfig, callback) {
|
||||
post('/api/v1/settings/backup_config', backupConfig).success(function(data, status) {
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
@@ -499,21 +485,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
|
||||
Client.prototype.setMailConfig = function (config, callback) {
|
||||
post('/api/v1/settings/mail_config', config).success(function (data, status) {
|
||||
if (status !== 202) return callback(new ClientError(status, data));
|
||||
callback(null);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.getMailRelay = function (callback) {
|
||||
get('/api/v1/settings/mail_relay').success(function (data, status) {
|
||||
if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data));
|
||||
callback(null, data);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.setMailRelay = function (config, callback) {
|
||||
post('/api/v1/settings/mail_relay', config).success(function (data, status) {
|
||||
if (status !== 202) return callback(new ClientError(status, data));
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
callback(null);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ if (search.accessToken) localStorage.token = search.accessToken;
|
||||
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['ngFitText', 'ngRoute', 'ngAnimate', 'ngSanitize', 'angular-md5', 'base64', 'slick', 'ui-notification', 'ui.bootstrap', 'ui.bootstrap-slider', 'ngTld', 'ui.multiselect']);
|
||||
var app = angular.module('Application', ['ngFitText', 'ngRoute', 'ngAnimate', 'ngSanitize', 'angular-md5', 'base64', 'slick', 'ui-notification', 'ui.bootstrap', 'ui.bootstrap-slider', 'ngTld']);
|
||||
|
||||
app.config(['NotificationProvider', function (NotificationProvider) {
|
||||
NotificationProvider.setOptions({
|
||||
|
||||
+9
-16
@@ -140,28 +140,21 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
|
||||
|
||||
if (result.provider === 'caas') return;
|
||||
|
||||
Client.getMailRelay(function (error, result) {
|
||||
// Check if all email DNS records are set up properly only for non external DNS API
|
||||
Client.getEmailStatus(function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
// the email status checks are currently only useful when using Cloudron itself for relaying
|
||||
if (result.provider !== 'cloudron-smtp') return;
|
||||
if (!result.dns.spf.status || !result.dns.dkim.status || !result.dns.ptr.status || !result.outboundPort25.status) {
|
||||
var actionScope = $scope.$new(true);
|
||||
actionScope.action = '/#/email';
|
||||
|
||||
// Check if all email DNS records are set up properly only for non external DNS API
|
||||
Client.getEmailStatus(function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
if (!result.dns.spf.status || !result.dns.dkim.status || !result.dns.ptr.status || !result.relay.status) {
|
||||
var actionScope = $scope.$new(true);
|
||||
actionScope.action = '/#/email';
|
||||
|
||||
Client.notify('DNS Configuration', 'Please setup all required DNS records to guarantee correct mail delivery', false, 'info', actionScope);
|
||||
}
|
||||
});
|
||||
Client.notify('DNS Configuration', 'Please setup all required DNS records to guarantee correct mail delivery', false, 'info', actionScope);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$scope.getSubscription = function () {
|
||||
function getSubscription() {
|
||||
Client.getAppstoreConfig(function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
@@ -246,7 +239,7 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
|
||||
if ($scope.user.admin) {
|
||||
runConfigurationChecks();
|
||||
|
||||
if ($scope.config.provider !== 'caas') $scope.getSubscription();
|
||||
if ($scope.config.provider !== 'caas') getSubscription();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
/* global tld */
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['angular-md5', 'ui-notification']);
|
||||
var app = angular.module('Application', ['angular-md5', 'ui-notification', 'ngTld']);
|
||||
|
||||
app.filter('zoneName', function () {
|
||||
return function (domain) {
|
||||
return tld.getDomain(domain);
|
||||
};
|
||||
});
|
||||
|
||||
app.controller('SetupDNSController', ['$scope', '$http', 'Client', function ($scope, $http, Client) {
|
||||
app.controller('SetupDNSController', ['$scope', '$http', 'Client', 'ngTld', function ($scope, $http, Client, ngTld) {
|
||||
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
|
||||
$scope.initialized = false;
|
||||
@@ -20,22 +12,6 @@ app.controller('SetupDNSController', ['$scope', '$http', 'Client', function ($sc
|
||||
$scope.provider = '';
|
||||
$scope.showDNSSetup = false;
|
||||
$scope.instanceId = '';
|
||||
$scope.explicitZone = search.zone || '';
|
||||
$scope.isDomain = false;
|
||||
$scope.isSubdomain = false;
|
||||
|
||||
$scope.$watch('dnsCredentials.domain', function (newVal) {
|
||||
if (!newVal) {
|
||||
$scope.isDomain = false;
|
||||
$scope.isSubdomain = false;
|
||||
} else if (!tld.getDomain(newVal) || newVal[newVal.length-1] === '.') {
|
||||
$scope.isDomain = false;
|
||||
$scope.isSubdomain = false;
|
||||
} else {
|
||||
$scope.isDomain = true;
|
||||
$scope.isSubdomain = tld.getDomain(newVal) !== newVal;
|
||||
}
|
||||
});
|
||||
|
||||
// keep in sync with certs.js
|
||||
$scope.dnsProvider = [
|
||||
@@ -62,7 +38,6 @@ app.controller('SetupDNSController', ['$scope', '$http', 'Client', function ($sc
|
||||
|
||||
var data = {
|
||||
domain: $scope.dnsCredentials.domain,
|
||||
zoneName: $scope.explicitZone,
|
||||
provider: $scope.dnsCredentials.provider,
|
||||
accessKeyId: $scope.dnsCredentials.accessKeyId,
|
||||
secretAccessKey: $scope.dnsCredentials.secretAccessKey,
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
<input type="text" class="form-control" ng-model="account.displayName" id="inputDisplayName" name="displayName" placeholder="Display Name" required autocomplete="off" autofocus>
|
||||
</div>
|
||||
<div ng-show="account.requireEmail" class="form-group" ng-class="{ 'has-error': setupForm.email.$dirty && setupForm.email.$invalid }">
|
||||
<input type="email" class="form-control" ng-model="account.email" id="inputEmail" name="email" placeholder="Email" required autocomplete="off" tooltip-trigger="focus" uib-tooltip="This email address is local to your Cloudron and used for notifications and password reset. A valid email is also required for Let's Encrypt certificates.">
|
||||
<input type="email" class="form-control" ng-model="account.email" id="inputEmail" name="email" placeholder="Email" required autocomplete="off" tooltip-trigger="focus" uib-tooltip="This email address is local to your Cloudron and used for notifications and password reset.">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (setupForm.username.$dirty && setupForm.username.$invalid) || (!setupForm.username.$dirty && error.username) }">
|
||||
<p ng-show="!setupForm.username.$dirty && error.username">{{ error.username }}</p>
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
<!-- Angular directives for tldjs -->
|
||||
<script src="3rdparty/js/tld.js"></script>
|
||||
<script src="3rdparty/js/angular-tld.js"></script>
|
||||
|
||||
<!-- Setup Application -->
|
||||
<script src="js/setupdns.js"></script>
|
||||
@@ -54,11 +55,11 @@
|
||||
<div class="col-md-10 col-md-offset-1 text-center">
|
||||
<h1>Cloudron Setup</h1>
|
||||
<h3>Provide a domain for your Cloudron</h3>
|
||||
<p>Apps will be installed on subdomains of this domain.</p>
|
||||
<p>Apps will be installed on subdomains of that domain.</p>
|
||||
<div class="form-group" style="margin-bottom: 0;" ng-class="{ 'has-error': dnsCredentialsForm.domain.$dirty && dnsCredentialsForm.domain.$invalid }">
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.domain" name="domain" placeholder="example.com" required autofocus ng-disabled="dnsCredentials.busy">
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.domain" name="domain" check-tld placeholder="example.com" required autofocus ng-disabled="dnsCredentials.busy">
|
||||
</div>
|
||||
<p ng-show="isSubdomain" class="text-bold">Installing Cloudron on a subdomain requires an enterprise subscription.</p>
|
||||
<p> <span ng-show="dnsCredentialsForm.domain.$error.invalidSubdomain" class="text-danger">Subdomains are <a href="https://cloudron.io/references/selfhosting.html#domain-setup" target="_blank" title="Domain documentation">not supported</a></span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -77,14 +78,14 @@
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.secretAccessKey.$dirty && dnsCredentialsForm.secretAccessKey.$invalid }" ng-show="dnsCredentials.provider === 'route53'">
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.secretAccessKey" name="secretAccessKey" placeholder="Secret Access Key" ng-required="dnsCredentials.provider === 'route53'" ng-disabled="dnsCredentials.busy">
|
||||
<br/>
|
||||
<span ng-show="isDomain || explicitZone"><b>{{ explicitZone ? explicitZone : (dnsCredentials.domain | zoneName) }}</b> must be hosted on <a href="https://aws.amazon.com/route53/?nc2=h_m1" target="_blank">AWS Route53</a>.</span>
|
||||
<span>The domain {{ dnsCredentials.domain }} must be hosted on <a href="https://aws.amazon.com/route53/?nc2=h_m1" target="_blank">AWS Route53</a>.</span>
|
||||
</div>
|
||||
|
||||
<!-- DigitalOcean -->
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.digitalOceanToken.$dirty && dnsCredentialsForm.digitalOceanToken.$invalid }" ng-show="dnsCredentials.provider === 'digitalocean'">
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.digitalOceanToken" name="digitalOceanToken" placeholder="API Token" ng-required="dnsCredentials.provider === 'digitalocean'" ng-disabled="dnsCredentials.busy">
|
||||
<br/>
|
||||
<span ng-show="isDomain || explicitZone"><b>{{ explicitZone ? explicitZone : (dnsCredentials.domain | zoneName) }}</b> must be hosted on <a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-a-host-name-with-digitalocean#step-two%E2%80%94change-your-domain-server" target="_blank">DigitalOcean</a>.</span>
|
||||
<span>The domain {{ dnsCredentials.domain }} must be hosted on <a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-a-host-name-with-digitalocean#step-two%E2%80%94change-your-domain-server" target="_blank">DigitalOcean</a>.</span>
|
||||
</div>
|
||||
|
||||
<!-- Wildcard -->
|
||||
|
||||
@@ -167,6 +167,8 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
$scope.appInstall.error.location = 'This name is already taken.';
|
||||
$scope.appInstallForm.location.$setPristine();
|
||||
$('#appInstallLocationInput').focus();
|
||||
} else if (error.statusCode === 402) {
|
||||
$scope.appstoreLogin.show();
|
||||
} else if (error.statusCode === 400 && error.message.indexOf('cert') !== -1 ) {
|
||||
$scope.appInstall.error.cert = error.message;
|
||||
$scope.appInstall.certificateFileName = '';
|
||||
@@ -310,9 +312,6 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
|
||||
return;
|
||||
}
|
||||
|
||||
// check subscription right away after login
|
||||
$scope.$parent.getSubscription();
|
||||
|
||||
fetchAppstoreConfig();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.customDomainId.$invalid }" uib-tooltip="{{ config.provider === 'caas' ? '' : 'Changing the domain is not yet supported' }}">
|
||||
<label class="control-label" for="customDomainId">Domain name</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.customDomain" id="customDomainId" name="customDomainId" ng-disabled="dnsCredentials.busy || config.provider !== 'caas'" placeholder="example.com" required autofocus>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.customDomain" id="customDomainId" name="customDomainId" ng-disabled="dnsCredentials.busy || config.provider !== 'caas'" check-tld placeholder="example.com" required autofocus>
|
||||
<p> <span ng-show="dnsCredentialsForm.customDomainId.$error.invalidSubdomain && dnsCredentials.customDomain !== config.fqdn" class="text-danger">Subdomains are <a href="https://cloudron.io/references/selfhosting.html#domain-setup" target="_blank" title="Domain documentation">not supported</a></span></p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
|
||||
@@ -58,95 +58,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-header" ng-show="isPaying">
|
||||
<div class="text-left">
|
||||
<h3>Outbound Mail Relay</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;" ng-show="isPaying">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
Select the mail server through which Cloudron will send outbound mails:
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group">
|
||||
<select class="form-control" style="width: 50%;" ng-model="mailRelay.preset" ng-options="a.name for a in mailRelayPresets track by a.provider" ng-change="mailRelay.presetChanged()"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" ng-show="mailRelay.preset.provider !== 'cloudron-smtp'">
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<form name="mailRelayForm" role="form" ng-submit="mailRelay.submit()" autocomplete="off" novalidate>
|
||||
<div class="form-group" ng-class="{ 'has-error': (mailRelayForm.host.$dirty && mailRelayForm.host.$invalid) }">
|
||||
<label class="control-label">SMTP Host</label>
|
||||
<div class="control-label" ng-show="(!mailRelayForm.host.$dirty && mailRelay.error.host) || (mailRelayForm.host.$dirty && mailRelayForm.host.$invalid)">
|
||||
<small ng-show="!mailRelayForm.host.$dirty && mailRelay.error.host">{{ mailRelay.error.host }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="mailRelay.relay.host" name="host" required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (mailRelayForm.port.$dirty && mailRelayForm.port.$invalid) }">
|
||||
<label class="control-label">SMTP Port (STARTTLS)</label>
|
||||
<div class="control-label" ng-show="(!mailRelayForm.port.$dirty && mailRelay.error.port) || (mailRelayForm.port.$dirty && mailRelayForm.port.$invalid)">
|
||||
<small ng-show="!mailRelayForm.port.$dirty && mailRelay.error.port">{{ mailRelay.error.port }}</small>
|
||||
</div>
|
||||
<input type="number" class="form-control" ng-model="mailRelay.relay.port" name="port" required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (mailRelayForm.username.$dirty && mailRelayForm.username.$invalid) }">
|
||||
<label class="control-label">Username</label>
|
||||
<div class="control-label" ng-show="(!mailRelayForm.username.$dirty && mailRelay.error.username) || (mailRelayForm.username.$dirty && mailRelayForm.username.$invalid)">
|
||||
<small ng-show="!mailRelayForm.username.$dirty && mailRelay.error.username">{{ mailRelay.error.username }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="mailRelay.relay.username" name="username" required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (mailRelayForm.password.$dirty && mailRelayForm.password.$invalid) }">
|
||||
<label class="control-label">Password</label>
|
||||
<div class="control-label" ng-show="(!mailRelayForm.password.$dirty && mailRelay.error.password) || (mailRelayForm.password.$dirty && mailRelayForm.password.$invalid)">
|
||||
<small ng-show="!mailRelayForm.password.$dirty && mailRelay.error.password">{{ mailRelay.error.password }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="mailRelay.relay.password" name="password" required>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="mailRelayForm.$invalid"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<button class="btn btn-primary" ng-click="mailRelay.submit()" ng-disabled="(mailRelay.preset.provider !== 'cloudron-smtp && (!mailRelayForm.$dirty || mailRelayForm.$invalid)) || mailRelay.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="mailRelay.busy"></i> Save</button>
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<span class="has-error text-center" ng-show="mailRelay.error">{{ mailRelay.error }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-header" ng-show="mailConfig.enabled && isPaying">
|
||||
<div class="text-left">
|
||||
<h3>Catch-all</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;" ng-show="mailConfig.enabled && isPaying">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
Emails sent to non existing addresses will be forwarded to the following accounts:
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<multiselect ng-model="catchall.addresses" options="address for address in catchall.availableAddresses" data-multiple="true"></multiselect>
|
||||
<button class="btn btn-outline btn-primary" ng-disabled="catchall.busy" ng-click="catchall.submit()"><i class="fa fa-circle-o-notch fa-spin" ng-show="catchall.busy"></i> Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-header" ng-show="dnsConfig.provider && dnsConfig.provider !== 'caas'">
|
||||
<div class="text-left">
|
||||
<h3>DNS Records</h3>
|
||||
@@ -161,7 +72,7 @@
|
||||
<br/><br/>
|
||||
|
||||
<div ng-repeat="record in expectedDnsRecordsTypes">
|
||||
<div class="row" ng-if="expectedDnsRecords[record.value] && (mailConfig.enabled || (record.name !== 'DMARC' && record.name !== 'MX'))">
|
||||
<div class="row" ng-if="mailConfig.enabled || (record.name !== 'DMARC' && record.name !== 'MX')">
|
||||
<div class="col-xs-12">
|
||||
<p class="text-muted">
|
||||
<i ng-class="expectedDnsRecords[record.value].status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
@@ -183,15 +94,15 @@
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<p class="text-muted">
|
||||
<i ng-class="relay.status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<i ng-class="outboundPort25.status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i>
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_dns_port">
|
||||
Outbound SMTP
|
||||
Outbound SMTP (Port 25)
|
||||
</a>
|
||||
<button class="btn btn-xs btn-default" ng-click="email.refresh()" ng-disabled="email.refreshBusy" ng-show="!relay.status"><i class="fa fa-refresh" ng-class="{ 'fa-pulse': email.refreshBusy }"></i></button>
|
||||
<button class="btn btn-xs btn-default" ng-click="email.refresh()" ng-disabled="email.refreshBusy" ng-show="!outboundPort25.status"><i class="fa fa-refresh" ng-class="{ 'fa-pulse': email.refreshBusy }"></i></button>
|
||||
</p>
|
||||
<div id="collapse_dns_port" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<p><b> {{ relay.value }} </b> </p>
|
||||
<p><b> {{ outboundPort25.value }} </b> </p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+4
-127
@@ -1,13 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('Application').controller('EmailController', ['$scope', '$location', '$rootScope', 'Client', 'AppStore', function ($scope, $location, $rootScope, Client, AppStore) {
|
||||
angular.module('Application').controller('EmailController', ['$scope', '$location', '$rootScope', 'Client', function ($scope, $location, $rootScope, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
|
||||
|
||||
$scope.client = Client;
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.dnsConfig = {};
|
||||
$scope.relay = {};
|
||||
$scope.outboundPort25 = {};
|
||||
$scope.expectedDnsRecords = {};
|
||||
$scope.expectedDnsRecordsTypes = [
|
||||
{ name: 'MX', value: 'mx' },
|
||||
@@ -17,8 +17,6 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
{ name: 'PTR', value: 'ptr' }
|
||||
];
|
||||
$scope.mailConfig = null;
|
||||
$scope.users = [];
|
||||
$scope.isPaying = false;
|
||||
|
||||
$scope.showView = function (view) {
|
||||
// wait for dialog to be fully closed to avoid modal behavior breakage when moving to a different view already
|
||||
@@ -30,21 +28,6 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
$('.modal').modal('hide');
|
||||
};
|
||||
|
||||
$scope.catchall = {
|
||||
addresses: [],
|
||||
busy: false,
|
||||
|
||||
submit: function () {
|
||||
$scope.catchall.busy = true;
|
||||
|
||||
Client.setCatchallAddresses($scope.catchall.addresses, function (error) {
|
||||
if (error) console.error('Unable to add catchall address.', error);
|
||||
|
||||
$scope.catchall.busy = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.email = {
|
||||
refreshBusy: false,
|
||||
|
||||
@@ -84,54 +67,6 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
}
|
||||
};
|
||||
|
||||
$scope.mailRelayPresets = [
|
||||
{ provider: 'cloudron-smtp', name: 'Built-in SMTP server' },
|
||||
{ provider: 'external-smtp', name: 'External SMTP server', host: '', port: 587 },
|
||||
{ provider: 'ses-smtp', name: 'Amazon SES', host: 'email-smtp.us-east-1.amazonaws.com', port: 587 },
|
||||
{ provider: 'google-smtp', name: 'Google', host: 'smtp.gmail.com', port: 587 },
|
||||
{ provider: 'mailgun-smtp', name: 'Mailgun', host: 'smtp.mailgun.org', port: 587 },
|
||||
{ provider: 'postmark-smtp', name: 'Postmark', host: 'smtp.postmarkapp.com', port: 587 },
|
||||
{ provider: 'sendgrid-smtp', name: 'SendGrid', host: 'smtp.sendgrid.net', port: 587 },
|
||||
];
|
||||
|
||||
$scope.mailRelay = {
|
||||
error: null,
|
||||
busy: false,
|
||||
preset: $scope.mailRelayPresets[0],
|
||||
|
||||
presetChanged: function () {
|
||||
$scope.mailRelay.error = null;
|
||||
|
||||
$scope.mailRelay.relay.provider = $scope.mailRelay.preset.provider;
|
||||
$scope.mailRelay.relay.host = $scope.mailRelay.preset.host;
|
||||
$scope.mailRelay.relay.port = $scope.mailRelay.preset.port;
|
||||
$scope.mailRelay.relay.username = '';
|
||||
$scope.mailRelay.relay.password = '';
|
||||
},
|
||||
|
||||
// form data to be set on load
|
||||
relay: {
|
||||
provider: 'cloudron-smtp',
|
||||
host: '',
|
||||
port: 25,
|
||||
username: '',
|
||||
password: ''
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
$scope.mailRelay.error = null;
|
||||
$scope.mailRelay.busy = true;
|
||||
|
||||
Client.setMailRelay($scope.mailRelay.relay, function (error) {
|
||||
if (error) {
|
||||
$scope.mailRelay.error = error.message;
|
||||
}
|
||||
|
||||
$scope.mailRelay.busy = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function getMailConfig() {
|
||||
Client.getMailConfig(function (error, mailConfig) {
|
||||
if (error) return console.error(error);
|
||||
@@ -140,21 +75,6 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
});
|
||||
}
|
||||
|
||||
function getMailRelay() {
|
||||
Client.getMailRelay(function (error, relay) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.mailRelay.relay = relay;
|
||||
|
||||
for (var i = 0; i < $scope.mailRelayPresets.length; i++) {
|
||||
if ($scope.mailRelayPresets[i].provider === relay.provider) {
|
||||
$scope.mailRelay.preset = $scope.mailRelayPresets[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getDnsConfig() {
|
||||
Client.getDnsConfig(function (error, dnsConfig) {
|
||||
if (error) return console.error(error);
|
||||
@@ -170,7 +90,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
if (error) return callback(error);
|
||||
|
||||
$scope.expectedDnsRecords = result.dns;
|
||||
$scope.relay = result.relay;
|
||||
$scope.outboundPort25 = result.outboundPort25;
|
||||
|
||||
// open the record details if they are not correct
|
||||
for (var type in $scope.expectedDnsRecords) {
|
||||
@@ -179,7 +99,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
}
|
||||
}
|
||||
|
||||
if (!$scope.relay.status) {
|
||||
if (!$scope.outboundPort25.status) {
|
||||
$('#collapse_dns_port').collapse('show');
|
||||
}
|
||||
|
||||
@@ -187,52 +107,9 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
|
||||
});
|
||||
}
|
||||
|
||||
function getUsers() {
|
||||
Client.getUsers(function (error, result) {
|
||||
if (error) return console.error('Unable to get user listing.', error);
|
||||
|
||||
// only allow users with a Cloudron email address
|
||||
$scope.catchall.availableAddresses = result.filter(function (u) { return !!u.email; }).map(function (u) { return u.username; });
|
||||
});
|
||||
}
|
||||
|
||||
function getCatchallAddresses() {
|
||||
Client.getCatchallAddresses(function (error, result) {
|
||||
if (error) return console.error('Unable to get catchall address listing.', error);
|
||||
|
||||
// dedupe in case to avoid angular breakage
|
||||
$scope.catchall.addresses = result.filter(function(item, pos, self) {
|
||||
return self.indexOf(item) == pos;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getSubscription() {
|
||||
if ($scope.config.provider === 'caas') {
|
||||
$scope.isPaying = true;
|
||||
return;
|
||||
}
|
||||
|
||||
Client.getAppstoreConfig(function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
if (!result.token) return;
|
||||
|
||||
AppStore.getSubscription(result, function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.isPaying = result.plan.id !== 'free' && result.plan.id !== 'undecided';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Client.onReady(function () {
|
||||
getMailConfig();
|
||||
getMailRelay();
|
||||
getDnsConfig();
|
||||
getSubscription();
|
||||
getUsers();
|
||||
getCatchallAddresses();
|
||||
$scope.email.refresh();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user