diff --git a/src/cloudron.js b/src/cloudron.js index 74a74a9ee..edd679be9 100644 --- a/src/cloudron.js +++ b/src/cloudron.js @@ -21,6 +21,8 @@ exports = module.exports = { checkDiskSpace: checkDiskSpace, + readDkimPublicKeySync: readDkimPublicKeySync, + events: new (require('events').EventEmitter)(), EVENT_ACTIVATED: 'activated', diff --git a/src/routes/settings.js b/src/routes/settings.js index 71bdfa56e..edb665507 100644 --- a/src/routes/settings.js +++ b/src/routes/settings.js @@ -10,6 +10,8 @@ exports = module.exports = { getCloudronAvatar: getCloudronAvatar, setCloudronAvatar: setCloudronAvatar, + getExpectedDnsRecords: getExpectedDnsRecords, + getDnsConfig: getDnsConfig, setDnsConfig: setDnsConfig, @@ -147,6 +149,14 @@ function getCloudronAvatar(req, res, next) { }); } +function getExpectedDnsRecords(req, res, next) { + settings.getExpectedDnsRecords(function (error, records) { + if (error) return next(new HttpError(500, error)); + + next(new HttpSuccess(200, records)); + }); +} + function getDnsConfig(req, res, next) { settings.getDnsConfig(function (error, config) { if (error) return next(new HttpError(500, error)); diff --git a/src/server.js b/src/server.js index 4ea61dd69..34542ce66 100644 --- a/src/server.js +++ b/src/server.js @@ -178,6 +178,7 @@ function initializeExpressSync() { router.post('/api/v1/settings/cloudron_name', settingsScope, routes.user.requireAdmin, routes.settings.setCloudronName); router.get ('/api/v1/settings/cloudron_avatar', settingsScope, routes.user.requireAdmin, routes.settings.getCloudronAvatar); router.post('/api/v1/settings/cloudron_avatar', settingsScope, routes.user.requireAdmin, multipart, routes.settings.setCloudronAvatar); + router.get ('/api/v1/settings/expected_dns_records', settingsScope, routes.user.requireAdmin, routes.settings.getExpectedDnsRecords); router.get ('/api/v1/settings/dns_config', settingsScope, routes.user.requireAdmin, routes.settings.getDnsConfig); router.post('/api/v1/settings/dns_config', settingsScope, routes.user.requireAdmin, routes.settings.setDnsConfig); router.get ('/api/v1/settings/backup_config', settingsScope, routes.user.requireAdmin, routes.settings.getBackupConfig); diff --git a/src/settings.js b/src/settings.js index c3d645b89..441ea15f9 100644 --- a/src/settings.js +++ b/src/settings.js @@ -3,6 +3,8 @@ exports = module.exports = { SettingsError: SettingsError, + getExpectedDnsRecords: getExpectedDnsRecords, + getAutoupdatePattern: getAutoupdatePattern, setAutoupdatePattern: setAutoupdatePattern, @@ -61,12 +63,15 @@ var assert = require('assert'), debug = require('debug')('box:settings'), digitalocean = require('./dns/digitalocean.js'), dns = require('native-dns'), + cloudron = require('./cloudron.js'), + CloudronError = cloudron.CloudronError, moment = require('moment-timezone'), paths = require('./paths.js'), route53 = require('./dns/route53.js'), safe = require('safetydance'), settingsdb = require('./settingsdb.js'), - SubdomainError = require('./subdomains.js').SubdomainError, + subdomains = require('./subdomains.js'), + SubdomainError = subdomains.SubdomainError, superagent = require('superagent'), sysinfo = require('./sysinfo.js'), util = require('util'), @@ -121,6 +126,50 @@ SettingsError.EXTERNAL_ERROR = 'External Error'; SettingsError.NOT_FOUND = 'Not Found'; SettingsError.BAD_FIELD = 'Bad Field'; +function getExpectedDnsRecords(callback) { + assert.strictEqual(typeof callback, 'function'); + + var records = {}; + + // DKIM + var DKIM_SELECTOR = 'cloudron'; + var dkimKey = cloudron.readDkimPublicKeySync(); + if (!dkimKey) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, new Error('Failed to read dkim public key'))); + records.dkim = {subdomain: DKIM_SELECTOR + '._domainkey', type: 'TXT', expected: 'v=DKIM1; t=s; p=' + dkimKey, value: null, status: false}; + + dns.resolveTxt(records.dkim.subdomain + '.' + config.fqdn(), function (error, txtRecords) { + if (error) return callback(error); + for (var i = 0; i < txtRecords.length; i++) { + records.dkim.value = txtRecords[i].join(" "); + records.dkim.status = (records.dkim.value == records.dkim.value); + break; + } + + // SPF + records.spf = {subdomain: '', type: 'TXT', value: null, expected: null, status: false}; + dns.resolveTxt(config.fqdn(), function (error, txtRecords) { + if (error) return callback(error); + var i; + for (i = 0; i < txtRecords.length; i++) { + if (txtRecords[i].join(" ").indexOf('v=spf1 ') !== 0) continue; // not SPF + records.spf.value = txtRecords[i].join(" "); + 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() + ' ~all'; + } else { + records.spf.expected = 'v=spf1 a:' + config.adminFqdn() + ' ' + records.spf.value.slice('v=spf1 '.length); + } + return callback(null, records); + }); + + }); +} + function setAutoupdatePattern(pattern, callback) { assert.strictEqual(typeof pattern, 'string'); assert.strictEqual(typeof callback, 'function'); diff --git a/webadmin/src/js/client.js b/webadmin/src/js/client.js index f79503bfe..211496457 100644 --- a/webadmin/src/js/client.js +++ b/webadmin/src/js/client.js @@ -442,6 +442,13 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification', }).error(defaultErrorHandler(callback)); }; + Client.prototype.getExpectedDnsRecords = function (callback) { + get('/api/v1/settings/expected_dns_records').success(function(data, status) { + if (status !== 200) return callback(new ClientError(status, data)); + callback(null, data); + }).error(defaultErrorHandler(callback)); + }; + Client.prototype.setAppstoreConfig = function (config, callback) { var data = config; diff --git a/webadmin/src/js/main.js b/webadmin/src/js/main.js index 7c54b5551..c5c1fa82c 100644 --- a/webadmin/src/js/main.js +++ b/webadmin/src/js/main.js @@ -117,6 +117,18 @@ angular.module('Application').controller('MainController', ['$scope', '$route', Client.notify('Backup Configuration', 'Please setup an external backup storage to avoid data loss', true, 'info', actionScope); } }); + + // Check if all DNS records are set up properly + Client.getExpectedDnsRecords(function (error, result) { + if (error) return console.error(error); + + if (!result.spf.status || !result.dkim.status) { + var actionScope = $scope.$new(true); + actionScope.action = '/#/settings'; + + Client.notify('DNS Configuration', 'Please setup all required DNS records to guarantee correct mail delivery', true, 'info', actionScope); + } + }); } Client.getStatus(function (error, status) { diff --git a/webadmin/src/views/settings.html b/webadmin/src/views/settings.html index 8392667a9..a87b096e9 100644 --- a/webadmin/src/views/settings.html +++ b/webadmin/src/views/settings.html @@ -282,18 +282,39 @@
Cloudron has a built-in email server that allows users to send and receive email for your domain. Apps can still send email regardless of this setting.
+ Cloudron has a built-in email server that allows users to send and receive email for your domain. Apps can still send email regardless of this setting. When enabled, your DNS will be configured automatically.The User manual has information on how to setup your mail client.
+ Please make sure to set the following DNS records for {{ config.fqdn }} to guarentee proper email functionality.
Subdomain: {{ expectedDnsRecords[record.value].subdomain }}
+Record type: {{ expectedDnsRecords[record.value].type }}
+Expected value: {{ expectedDnsRecords[record.value].expected }}
+Current value: {{ expectedDnsRecords[record.value].value ? expectedDnsRecords[record.value].value : '[not set]' }}
+