Compare commits

..

36 Commits

Author SHA1 Message Date
Girish Ramakrishnan 5697bcf43f Update to node 6.11.1
Fixes a DDoS issue in node

https://nodejs.org/en/blog/vulnerability/july-2017-security-releases/
2017-07-13 09:02:32 -05:00
Johannes Zellner 5b7cc6642a Ensure only the token value ends up in the clipboard
Fixes #375
2017-07-12 11:53:50 +02:00
Girish Ramakrishnan ee528470a7 Add 1.2.1 changes 2017-07-11 10:37:11 -05:00
Girish Ramakrishnan 3eed481d22 Send mail relay and catch all as part of alive status 2017-07-11 09:53:06 -05:00
Girish Ramakrishnan 97b37cb45c bump mail container 2017-07-07 10:35:51 -05:00
Girish Ramakrishnan 49de39a1f3 Set max ttl to 5 minutes
This means the web ui will atleast work in 5 minutes.

Fixes #373
2017-07-07 09:50:29 -05:00
Girish Ramakrishnan 6fe390b957 Fix casing 2017-07-07 09:10:28 -05:00
Girish Ramakrishnan 1a68467ff2 Add one more 1.2.0 change 2017-07-06 09:12:44 -05:00
Girish Ramakrishnan 40de715f20 doc: relay API 2017-07-05 12:56:17 -05:00
Girish Ramakrishnan 8d9fbb9cea Create mail config regardless of whether owner exists 2017-07-05 11:31:51 -05:00
Girish Ramakrishnan e3910d6587 1.2.0 changes 2017-06-30 17:54:21 -05:00
Girish Ramakrishnan 50e712a93e preserve existing docker storage driver
fixes #364
2017-06-30 16:50:31 -05:00
Girish Ramakrishnan 1c8ddc10db mailer: Do not wait for SPF record to be in sync
This code logic does not work when a relay is used. Besides, the
SPF record can go out of sync at any time. This code is thus
not worth keeping.
2017-06-30 15:30:12 -05:00
Girish Ramakrishnan 1007a85fde Use latest haraka and restart on relay change 2017-06-30 09:07:43 -05:00
Girish Ramakrishnan a0903f0890 Allow relay to be reset back to cloudron 2017-06-30 09:07:43 -05:00
Girish Ramakrishnan 1c40e51999 Only starttls is supported by haraka
In addition, auth_type has to be set to PLAIN (or LOGIN)
2017-06-30 09:07:43 -05:00
Girish Ramakrishnan c07df68558 Fix typos 2017-06-30 09:07:37 -05:00
Girish Ramakrishnan fd5a05db6c suppress spf, dkim, ptr results when using external relay
part of #188
2017-06-28 22:20:41 -05:00
Girish Ramakrishnan 19d825db48 refactor code into email.js 2017-06-28 22:20:35 -05:00
Girish Ramakrishnan 2862fec819 outboundPort25 -> relay status 2017-06-28 22:20:25 -05:00
Girish Ramakrishnan 2df74ebe96 Only display the DNS records if they exist in the response
part of #188
2017-06-28 22:20:15 -05:00
Girish Ramakrishnan 5794aaee0a Display dns notification only when using cloudron for relay
part of #188
2017-06-28 22:20:04 -05:00
Girish Ramakrishnan 229ca7f86b return SettingsError and not CloudronError 2017-06-28 17:38:40 -05:00
Girish Ramakrishnan 7edf43c627 Select tls based on port
Note that starttls is always supported

part of #188
2017-06-28 16:56:11 -05:00
Girish Ramakrishnan ae1dff980a Set cloudron-smtp as default relay 2017-06-28 12:32:07 -05:00
Girish Ramakrishnan 01d0e56332 refactor code to have a relay provider
part of #188
2017-06-28 11:39:45 -05:00
Girish Ramakrishnan 00990b6837 Verify relay credentials
part of #188
2017-06-27 16:40:10 -05:00
Girish Ramakrishnan 5886671fba update nodemailer 2017-06-27 16:23:44 -05:00
Girish Ramakrishnan 5088cb47d9 Load and save the relay settings
part of #188
2017-06-27 15:13:09 -05:00
Girish Ramakrishnan 60ae4972b0 Bump mail container version (relay support) 2017-06-27 13:16:30 -05:00
Girish Ramakrishnan ad8ddf80f5 add mail relay tests
part of #188
2017-06-27 12:20:51 -05:00
Girish Ramakrishnan c4d313a2c0 generate smtp_forward.ini
part of #188
2017-06-27 11:08:50 -05:00
Johannes Zellner 140e9fdd94 Initial ui for email relay configuration 2017-06-27 13:39:08 +02:00
Johannes Zellner 82b5c11374 Use the locally installed gulp to avoid global dependency 2017-06-27 12:25:40 +02:00
Johannes Zellner 3307b581af Give the spawned node processes a bit more memory
Two cloudrons failed with 200 again and bumping it up helps
We still should look into the tarfs module to fix the root cause
2017-06-26 13:06:57 +02:00
Girish Ramakrishnan 45e68ef6da Use latest mysql image
have to reconfigure to re-inject new passwords

fixes #362
2017-06-22 14:16:58 -07:00
30 changed files with 822 additions and 584 deletions
+8 -13
View File
@@ -888,22 +888,17 @@
[1.0.0]
* Make selfhosting great again
[1.0.1]
* Notification improvements
[1.0.2]
* Notification improvements
[1.1.0]
* Add support for email catch-all
* Support Cloudrons on subdomains
[1.1.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
* Reduce mysql password length to 48
[1.1.2]
* Notification improvements
[1.1.3]
* Notification improvements
[1.2.1]
* Set max ttl of unbound to 5 minutes
* Fix issue where mail container does not cleanup LDAP connections properly
* Update node to 6.11.1
+4 -4
View File
@@ -42,10 +42,10 @@ apt-get -y install \
unbound
echo "==> Installing node.js"
mkdir -p /usr/local/node-6.9.2
curl -sL https://nodejs.org/dist/v6.9.2/node-v6.9.2-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-6.9.2
ln -sf /usr/local/node-6.9.2/bin/node /usr/bin/node
ln -sf /usr/local/node-6.9.2/bin/npm /usr/bin/npm
mkdir -p /usr/local/node-6.11.1
curl -sL https://nodejs.org/dist/v6.11.1/node-v6.11.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-6.11.1
ln -sf /usr/local/node-6.11.1/bin/node /usr/bin/node
ln -sf /usr/local/node-6.11.1/bin/npm /usr/bin/npm
apt-get install -y python # Install python which is required for npm rebuild
[[ "$(python --version 2>&1)" == "Python 2.7."* ]] || die "Expecting python version to be 2.7.x"
+47
View File
@@ -1347,6 +1347,53 @@ Request:
}
```
### Get Relay
GET `/api/v1/settings/mail_relay` <scope>admin</scope>
Gets the SMTP server through which outbound mails are relayed.
Response(200):
```
{
provider: <smtp provider>, // cloudron-smtp, external-smtp etc
host: <string>, // the host name of the SMTP relay
port: <number>, // the port number of the SMTP relay
username: <string>, // the username for authenticating with the SMTP relay
password: <string> // the password for authenticating with the SMTP relay
}
```
See the [set mail relay](/references/api.html#set-relay) API for more information on the fields.
### Set Relay
PUT `/api/v1/settings/mail_relay` <scope>admin</scope>
Sets the SMTP server through which outbound mails are relayed.
Request:
```
{
provider: <smtp provider>, // cloudron-smtp, external-smtp etc
host: <string>, // the host name of the SMTP relay
port: <number>, // the port number of the SMTP relay
username: <string>, // the username for authenticating with the SMTP relay
password: <string> // the password for authenticating with the SMTP relay
}
```
`provider` is one of the following values:
* `cloudron-smtp`
* `external-smtp`
* `ses-smtp`
* `google-smtp`
* `mailgun-smtp`
* `postmark-smtp`
* `sendgrid-smtp`
Cloudron requires the relay to support `STARTTLS`. Relaying using `SMTPS` (SMTP over TLS) is not supported.
### Get timezone
GET `/api/v1/settings/time_zone` <scope>admin</scope>
@@ -0,0 +1,9 @@
'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);
};
+35 -55
View File
@@ -29,11 +29,6 @@
"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",
@@ -331,18 +326,6 @@
"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",
@@ -1963,6 +1946,23 @@
"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,21 +2407,6 @@
"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",
@@ -2626,11 +2611,6 @@
"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",
@@ -2860,11 +2840,6 @@
"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",
@@ -2933,23 +2908,28 @@
"resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz"
},
"nodemailer": {
"version": "1.11.0",
"from": "nodemailer@>=1.3.0 <2.0.0",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-1.11.0.tgz"
"version": "4.0.1",
"from": "nodemailer@latest",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-4.0.1.tgz"
},
"nodemailer-direct-transport": {
"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": {
"version": "1.1.0",
"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"
"from": "nodemailer-shared@1.1.0",
"resolved": "https://registry.npmjs.org/nodemailer-shared/-/nodemailer-shared-1.1.0.tgz"
},
"nodemailer-smtp-transport": {
"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"
"version": "2.7.4",
"from": "nodemailer-smtp-transport@latest",
"resolved": "https://registry.npmjs.org/nodemailer-smtp-transport/-/nodemailer-smtp-transport-2.7.4.tgz"
},
"nodemailer-wellknown": {
"version": "0.1.10",
"from": "nodemailer-wellknown@>=0.1.7 <0.2.0",
"from": "nodemailer-wellknown@0.1.10",
"resolved": "https://registry.npmjs.org/nodemailer-wellknown/-/nodemailer-wellknown-0.1.10.tgz"
},
"nopt": {
@@ -4200,9 +4180,9 @@
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz"
},
"smtp-connection": {
"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"
"version": "2.12.0",
"from": "smtp-connection@2.12.0",
"resolved": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-2.12.0.tgz"
},
"sntp": {
"version": "1.0.9",
+2 -2
View File
@@ -44,8 +44,8 @@
"multiparty": "^4.1.2",
"mysql": "^2.7.0",
"node-uuid": "^1.4.3",
"nodemailer": "^1.3.0",
"nodemailer-smtp-transport": "^1.0.3",
"nodemailer": "^4.0.1",
"nodemailer-smtp-transport": "^2.7.4",
"oauth2orize": "^1.0.1",
"once": "^1.3.2",
"parse-links": "^0.1.0",
+3 -4
View File
@@ -31,8 +31,8 @@ if ! $(cd "${SOURCE_DIR}" && git diff --exit-code >/dev/null); then
exit 1
fi
if [[ "$(node --version)" != "v6.9.2" ]]; then
echo "This script requires node 6.9.2"
if [[ "$(node --version)" != "v6.11.1" ]]; then
echo "This script requires node 6.11.1"
exit 1
fi
@@ -58,7 +58,7 @@ else
fi
echo "Building webadmin assets"
(cd "${bundle_dir}" && gulp)
(cd "${bundle_dir}" && ./node_modules/.bin/gulp)
echo "Remove intermediate files required at build-time only"
rm -rf "${bundle_dir}/node_modules/"
@@ -84,4 +84,3 @@ echo "Cleaning up ${bundle_dir}"
rm -rf "${bundle_dir}"
echo "Tarball saved at ${bundle_file}"
+9
View File
@@ -34,6 +34,15 @@ while true; do
esac
done
echo "==> installer: updating node"
if [[ "$(node --version)" != "v6.11.1" ]]; then
mkdir -p /usr/local/node-6.11.1
$curl -sL https://nodejs.org/dist/v6.11.1/node-v6.11.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-6.11.1
ln -sf /usr/local/node-6.11.1/bin/node /usr/bin/node
ln -sf /usr/local/node-6.11.1/bin/npm /usr/bin/npm
rm -rf /usr/local/node-6.9.2
fi
for try in `seq 1 10`; do
# for reasons unknown, the dtrace package will fail. but rebuilding second time will work
+6 -2
View File
@@ -40,9 +40,13 @@ 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=devicemapper" > "${temp_file}"
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=${storage_driver}" > "${temp_file}"
systemctl enable docker
# restart docker if options changed
@@ -141,7 +145,7 @@ echo "==> Setting up unbound"
# DO uses Google nameservers by default. This causes RBL queries to fail (host 2.0.0.127.zen.spamhaus.org)
# We do not use dnsmasq because it is not a recursive resolver and defaults to the value in the interfaces file (which is Google DNS!)
# We listen on 0.0.0.0 because there is no way control ordering of docker (which creates the 172.18.0.0/16) and unbound
echo -e "server:\n\tinterface: 0.0.0.0\n\taccess-control: 127.0.0.1 allow\n\taccess-control: 172.18.0.1/16 allow\n\tcache-max-negative-ttl: 30" > /etc/unbound/unbound.conf.d/cloudron-network.conf
echo -e "server:\n\tinterface: 0.0.0.0\n\taccess-control: 127.0.0.1 allow\n\taccess-control: 172.18.0.1/16 allow\n\tcache-max-negative-ttl: 30\n\tcache-max-ttl: 300" > /etc/unbound/unbound.conf.d/cloudron-network.conf
echo "==> Adding systemd services"
cp -r "${script_dir}/start/systemd/." /etc/systemd/system/
+41 -43
View File
@@ -17,7 +17,6 @@ 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'),
@@ -150,52 +149,51 @@ function sendAliveStatus(data, callback) {
settings.getAll(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
eventlog.getAllPaged(eventlog.ACTION_USER_LOGIN, null, 1, 1, function (error, loginEvents) {
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
},
mailRelay: {
provider: result[settings.MAIL_RELAY_KEY].provider
},
mailCatchAll: {
count: result[settings.CATCH_ALL_ADDRESS].length
},
autoupdatePattern: result[settings.AUTOUPDATE_PATTERN_KEY],
timeZone: result[settings.TIME_ZONE_KEY]
};
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],
};
var data = {
domain: config.fqdn(),
version: config.version(),
provider: config.provider(),
backendSettings: backendSettings,
machine: {
cpus: os.cpus(),
totalmem: os.totalmem()
}
};
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
}
};
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
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)));
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);
});
callback(null);
});
});
});
+10 -23
View File
@@ -15,7 +15,6 @@ 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'),
@@ -23,21 +22,20 @@ var apps = require('./apps.js'),
semver = require('semver'),
updateChecker = require('./updatechecker.js');
var gAliveJob = null, // send periodic stats
gAppUpdateCheckerJob = null,
gAutoupdaterJob = null,
gBackupJob = null,
var gAutoupdaterJob = null,
gBoxUpdateCheckerJob = null,
gAppUpdateCheckerJob = null,
gHeartbeatJob = null, // for CaaS health check
gAliveJob = null, // send periodic stats
gBackupJob = null,
gCleanupTokensJob = null,
gCleanupBackupsJob = null,
gDockerVolumeCleanerJob = null,
gSchedulerSyncJob = null,
gCertificateRenewJob = null,
gCheckDiskSpaceJob = null,
gCleanupBackupsJob = null,
gCleanupEventlogJob = null,
gCleanupTokensJob = null,
gDockerVolumeCleanerJob = null,
gDynamicDNSJob = null,
gHeartbeatJob = null, // for CaaS health check
gSchedulerSyncJob = null,
gDigestEmailJob = null;
gDynamicDNSJob = null;
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
var AUDIT_SOURCE = { userId: null, username: 'cron' };
@@ -175,14 +173,6 @@ 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) {
@@ -282,8 +272,5 @@ function uninitialize(callback) {
if (gDynamicDNSJob) gDynamicDNSJob.stop();
gDynamicDNSJob = null;
if (gDigestEmailJob) gDigestEmailJob.stop();
gDigestEmailJob = null;
callback();
}
-64
View File
@@ -1,64 +0,0 @@
'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();
});
});
});
});
}
+303
View File
@@ -0,0 +1,303 @@
'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);
});
});
}
-12
View File
@@ -6,7 +6,6 @@ exports = module.exports = {
add: add,
get: get,
getAllPaged: getAllPaged,
getByActionLastWeek: getByActionLastWeek,
cleanup: cleanup,
// keep in sync with webadmin index.js filter and CLI tool
@@ -104,17 +103,6 @@ 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;
-15
View File
@@ -3,7 +3,6 @@
exports = module.exports = {
get: get,
getAllPaged: getAllPaged,
getByActionLastWeek: getByActionLastWeek,
add: add,
count: count,
delByCreationTime: delByCreationTime,
@@ -72,20 +71,6 @@ 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');
+3 -3
View File
@@ -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.3.0',
'version': '48.4.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.17.0' },
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:0.18.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.33.0' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.34.2' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:0.11.0' }
}
};
-47
View File
@@ -1,47 +0,0 @@
<% 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 { %>
<% } %>
+4 -44
View File
@@ -10,7 +10,6 @@ exports = module.exports = {
passwordReset: passwordReset,
boxUpdateAvailable: boxUpdateAvailable,
appUpdateAvailable: appUpdateAvailable,
sendDigest: sendDigest,
sendInvite: sendInvite,
unexpectedExit: unexpectedExit,
@@ -46,7 +45,6 @@ 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');
@@ -56,7 +54,7 @@ var NOOP_CALLBACK = function (error) { if (error) debug(error); };
var MAIL_TEMPLATES_DIR = path.join(__dirname, 'mail_templates');
var gMailQueue = [ ],
gDnsReady = false;
gPaused = false;
function splatchError(error) {
var result = { };
@@ -72,7 +70,7 @@ function splatchError(error) {
function start(callback) {
assert.strictEqual(typeof callback, 'function');
checkDns();
if (process.env.BOX_ENV === 'test') gPaused = true;
callback(null);
}
@@ -94,22 +92,8 @@ 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(gDnsReady);
assert(!gPaused);
sendMails(gMailQueue);
gMailQueue = [ ];
@@ -157,7 +141,7 @@ function enqueue(mailOptions) {
debug('Queued mail for ' + mailOptions.from + ' to ' + mailOptions.to);
gMailQueue.push(mailOptions);
if (gDnsReady) processQueue();
if (!gPaused) processQueue();
}
function render(templateFile, params) {
@@ -419,30 +403,6 @@ 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] Cloudron - Weekly activity 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');
+21 -2
View File
@@ -40,7 +40,9 @@ 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);
@@ -243,9 +245,11 @@ function createMailConfig(callback) {
user.getOwner(function (error, owner) {
var alertsTo = config.provider() === 'caas' ? [ 'support@cloudron.io' ] : [ ];
alertsTo.concat(error ? [] : owner.email).join(',');
alertsTo.concat(error ? [] : owner.email).join(','); // owner may not exist yet
settings.getCatchAllAddress(function (error, address) {
if (error) return callback(error);
var catchAll = address.join(',');
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/mail.ini',
@@ -253,7 +257,22 @@ function createMailConfig(callback) {
return callback(new Error('Could not create mail var file:' + safe.error.message));
}
callback();
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();
});
});
});
}
+33 -4
View File
@@ -24,6 +24,9 @@ exports = module.exports = {
getMailConfig: getMailConfig,
setMailConfig: setMailConfig,
getMailRelay: getMailRelay,
setMailRelay: setMailRelay,
getCatchAllAddress: getCatchAllAddress,
setCatchAllAddress: setCatchAllAddress,
@@ -38,6 +41,7 @@ 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'),
@@ -124,7 +128,32 @@ 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(200));
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));
});
}
@@ -149,7 +178,7 @@ function setCatchAllAddress(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(200, {}));
next(new HttpSuccess(202));
});
}
@@ -162,7 +191,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));
});
}
@@ -179,7 +208,7 @@ function getCloudronAvatar(req, res, next) {
}
function getEmailStatus(req, res, next) {
settings.getEmailStatus(function (error, records) {
email.getStatus(function (error, records) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, records));
+63 -4
View File
@@ -15,9 +15,10 @@ 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');
@@ -299,7 +300,7 @@ describe('Settings API', function () {
.query({ access_token: token })
.send({ enabled: true })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.statusCode).to.equal(202);
done();
});
});
@@ -350,7 +351,7 @@ describe('Settings API', function () {
.query({ access_token: token })
.send({ address: [ "user1" ] })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.statusCode).to.equal(202);
done();
});
});
@@ -757,7 +758,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.outboundPort25).to.be.an('object');
expect(res.body.relay).to.be.an('object');
done();
});
@@ -825,4 +826,62 @@ 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();
});
});
});
});
});
+1 -1
View File
@@ -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=200 "$@"
exec env "DEBUG=box*,connect-lastmile" /usr/bin/node --max_old_space_size=300 "$@"
+4
View File
@@ -209,8 +209,12 @@ 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);
+21 -220
View File
@@ -6,8 +6,6 @@ exports = module.exports = {
initialize: initialize,
uninitialize: uninitialize,
getEmailStatus: getEmailStatus,
getAutoupdatePattern: getAutoupdatePattern,
setAutoupdatePattern: setAutoupdatePattern,
@@ -44,20 +42,19 @@ 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',
@@ -65,13 +62,13 @@ 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'),
@@ -79,13 +76,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'),
@@ -113,8 +109,8 @@ 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;
})();
@@ -159,206 +155,6 @@ 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');
@@ -665,27 +461,32 @@ function setMailConfig(mailConfig, callback) {
});
}
function getEmailDigest(callback) {
function getMailRelay(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.EMAIL_DIGEST, function (error, enabled) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.EMAIL_DIGEST]);
settingsdb.get(exports.MAIL_RELAY_KEY, function (error, value) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.MAIL_RELAY_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
callback(null, !!enabled); // settingsdb holds string values only
callback(null, JSON.parse(value));
});
}
function setEmailDigest(enabled, callback) {
assert.strictEqual(typeof enabled, 'boolean');
function setMailRelay(relay, callback) {
assert.strictEqual(typeof relay, 'object');
assert.strictEqual(typeof callback, 'function');
settingsdb.set(exports.EMAIL_DIGEST, enabled ? 'enabled' : '', function (error) {
email.verifyRelay(relay, function (error) {
if (error && error.reason === EmailError.BAD_FIELD) return callback(new SettingsError(SettingsError.BAD_FIELD, error.message));
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
exports.events.emit(exports.EMAIL_DIGEST, enabled);
settingsdb.set(exports.MAIL_RELAY_KEY, JSON.stringify(relay), function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
callback(null);
exports.events.emit(exports.MAIL_RELAY_KEY, relay);
callback(null);
});
});
}
+23 -1
View File
@@ -9,7 +9,8 @@ var async = require('async'),
config = require('../config.js'),
database = require('../database.js'),
expect = require('expect.js'),
settings = require('../settings.js');
settings = require('../settings.js'),
settingsdb = require('../settingsdb.js');
function setup(done) {
config.set('provider', 'caas');
@@ -181,6 +182,27 @@ describe('Settings', function () {
});
});
it('can get mail relay', function (done) {
settings.getMailRelay(function (error, address) {
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()
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);
+16 -2
View File
@@ -406,7 +406,7 @@ 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 !== 200) return callback(new ClientError(status, data));
if (status !== 202) return callback(new ClientError(status, data));
callback(null);
}).error(defaultErrorHandler(callback));
};
@@ -499,7 +499,21 @@ 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 !== 200) return callback(new ClientError(status, data));
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));
callback(null);
}).error(defaultErrorHandler(callback));
};
+14 -7
View File
@@ -140,16 +140,23 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
if (result.provider === 'caas') return;
// Check if all email DNS records are set up properly only for non external DNS API
Client.getEmailStatus(function (error, result) {
Client.getMailRelay(function (error, result) {
if (error) return console.error(error);
if (!result.dns.spf.status || !result.dns.dkim.status || !result.dns.ptr.status || !result.outboundPort25.status) {
var actionScope = $scope.$new(true);
actionScope.action = '/#/email';
// the email status checks are currently only useful when using Cloudron itself for relaying
if (result.provider !== 'cloudron-smtp') return;
Client.notify('DNS Configuration', 'Please setup all required DNS records to guarantee correct mail delivery', false, 'info', actionScope);
}
// 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);
}
});
});
});
}
+73 -5
View File
@@ -58,6 +58,74 @@
</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>
@@ -93,7 +161,7 @@
<br/><br/>
<div ng-repeat="record in expectedDnsRecordsTypes">
<div class="row" ng-if="mailConfig.enabled || (record.name !== 'DMARC' && record.name !== 'MX')">
<div class="row" ng-if="expectedDnsRecords[record.value] && (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> &nbsp;
@@ -115,15 +183,15 @@
<div class="row">
<div class="col-xs-12">
<p class="text-muted">
<i ng-class="outboundPort25.status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i> &nbsp;
<i ng-class="relay.status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i> &nbsp;
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_dns_port">
Outbound SMTP (Port 25)
Outbound SMTP
</a>
<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>
<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>
</p>
<div id="collapse_dns_port" class="panel-collapse collapse">
<div class="panel-body">
<p><b> {{ outboundPort25.value }} </b> </p>
<p><b> {{ relay.value }} </b> </p>
</div>
</div>
</div>
+67 -3
View File
@@ -7,7 +7,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
$scope.user = Client.getUserInfo();
$scope.config = Client.getConfig();
$scope.dnsConfig = {};
$scope.outboundPort25 = {};
$scope.relay = {};
$scope.expectedDnsRecords = {};
$scope.expectedDnsRecordsTypes = [
{ name: 'MX', value: 'mx' },
@@ -84,6 +84,54 @@ 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);
@@ -92,6 +140,21 @@ 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);
@@ -107,7 +170,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
if (error) return callback(error);
$scope.expectedDnsRecords = result.dns;
$scope.outboundPort25 = result.outboundPort25;
$scope.relay = result.relay;
// open the record details if they are not correct
for (var type in $scope.expectedDnsRecords) {
@@ -116,7 +179,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
}
}
if (!$scope.outboundPort25.status) {
if (!$scope.relay.status) {
$('#collapse_dns_port').collapse('show');
}
@@ -165,6 +228,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
Client.onReady(function () {
getMailConfig();
getMailRelay();
getDnsConfig();
getSubscription();
getUsers();
+2 -4
View File
@@ -70,9 +70,7 @@
<div class="modal-header">
<h4 class="modal-title">New token created</h4>
</div>
<div class="modal-body">
<p><b ng-click-select>{{ tokenAdd.token.accessToken }}</b></p>
</div>
<div class="modal-body"><b ng-click-select>{{ tokenAdd.token.accessToken }}</b></div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Done</button>
</div>
@@ -106,7 +104,7 @@
<div class="section-header">
<div class="text-left">
<h3>Access tokens <button class="btn btn-xs btn-primary btn-outline pull-right" ng-click="tokenAdd.show(apiClient)"><i class="fa fa-plus"></i> New Token</button> </h3>
<h3>Access Tokens <button class="btn btn-xs btn-primary btn-outline pull-right" ng-click="tokenAdd.show(apiClient)"><i class="fa fa-plus"></i> New Token</button> </h3>
</div>
</div>