diff --git a/src/mail.js b/src/mail.js index 0e67638c8..43f002531 100644 --- a/src/mail.js +++ b/src/mail.js @@ -4,6 +4,7 @@ exports = module.exports = { getStatus: getStatus, get: get, + getAll: getAll, setMailFromValidation: setMailFromValidation, setCatchAllAddress: setCatchAllAddress, @@ -154,9 +155,9 @@ function verifyRelay(relay, callback) { }); } -function checkDkim(callback) { +function checkDkim(domain, callback) { var dkim = { - domain: config.dkimSelector() + '._domainkey.' + config.fqdn(), + domain: config.dkimSelector() + '._domainkey.' + domain, type: 'TXT', expected: null, value: null, @@ -181,12 +182,12 @@ function checkDkim(callback) { }); } -function checkSpf(callback) { +function checkSpf(domain, callback) { var spf = { - domain: config.fqdn(), + domain: domain, type: 'TXT', value: null, - expected: '"v=spf1 a:' + config.adminFqdn() + ' ~all"', + expected: '"v=spf1 a:' + config.mailFqdn() + ' ~all"', status: false }; @@ -215,9 +216,9 @@ function checkSpf(callback) { }); } -function checkMx(callback) { +function checkMx(domain, callback) { var mx = { - domain: config.fqdn(), + domain: domain, type: 'MX', value: null, expected: '10 ' + config.mailFqdn() + '.', @@ -237,9 +238,9 @@ function checkMx(callback) { }); } -function checkDmarc(callback) { +function checkDmarc(domain, callback) { var dmarc = { - domain: '_dmarc.' + config.fqdn(), + domain: '_dmarc.' + domain, type: 'TXT', value: null, expected: '"v=DMARC1; p=reject; pct=100"', @@ -346,7 +347,8 @@ const RBL_LIST = [ } ]; -function checkRblStatus(callback) { +// this function currently only looks for black lists based on IP. TODO: also look up by domain +function checkRblStatus(domain, callback) { assert.strictEqual(typeof callback, 'function'); sysinfo.getPublicIp(function (error, ip) { @@ -359,14 +361,14 @@ function checkRblStatus(callback) { dig.resolve(flippedIp + '.' + rblServer.dns, 'A', digOptions, function (error, records) { if (error || !records) return iteratorDone(null, null); // not listed - debug('checkRblStatus: %s (ip: %s) is in the blacklist of %j', config.fqdn(), flippedIp, rblServer); + debug('checkRblStatus: %s (ip: %s) is in the blacklist of %j', domain, flippedIp, rblServer); var result = _.extend({ }, rblServer); dig.resolve(flippedIp + '.' + rblServer.dns, 'TXT', digOptions, function (error, txtRecords) { result.txtRecords = error || !txtRecords ? 'No txt record' : txtRecords; - debug('checkRblStatus: %s (error: %s) (txtRecords: %j)', config.fqdn(), error, txtRecords); + debug('checkRblStatus: %s (error: %s) (txtRecords: %j)', domain, error, txtRecords); return iteratorDone(null, result); }); @@ -374,7 +376,7 @@ function checkRblStatus(callback) { }, function (ignoredError, blacklistedServers) { blacklistedServers = blacklistedServers.filter(function(b) { return b !== null; }); - debug('checkRblStatus: %s (ip: %s) servers: %j', config.fqdn(), ip, blacklistedServers); + debug('checkRblStatus: %s (ip: %s) servers: %j', domain, ip, blacklistedServers); return callback(null, { status: blacklistedServers.length === 0, ip: ip, servers: blacklistedServers }); }); @@ -404,25 +406,25 @@ function getStatus(domain, callback) { }; } - get(domain, function (error, mailConfig) { + get(domain, function (error, result) { if (error) return callback(error); var checks = [ - recordResult('dns.mx', checkMx), - recordResult('dns.dmarc', checkDmarc) + recordResult('dns.mx', checkMx.bind(null, domain)), + recordResult('dns.dmarc', checkDmarc.bind(null, domain)) ]; - if (mailConfig.relay.provider === 'cloudron-smtp') { + if (result.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.spf', checkSpf.bind(null, domain)), + recordResult('dns.dkim', checkDkim.bind(null, domain)), recordResult('dns.ptr', checkPtr), recordResult('relay', checkOutboundPort25), - recordResult('rbl', checkRblStatus) + recordResult('rbl', checkRblStatus.bind(null, domain)) ); } else { - checks.push(recordResult('relay', checkSmtpRelay.bind(null, mailConfig.relay))); + checks.push(recordResult('relay', checkSmtpRelay.bind(null, result.relay))); } async.parallel(checks, function () { @@ -434,28 +436,32 @@ function getStatus(domain, callback) { function createMailConfig(callback) { assert.strictEqual(typeof callback, 'function'); - const fqdn = config.fqdn(); const mailFqdn = config.mailFqdn(); - 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 + maildb.getAll(function (error, mailOutDomains) { + if (error) return callback(error); - get(fqdn, function (error, result) { - if (error) return callback(error); + var mailDomain = mailOutDomains[0]; // mail container can only handle one domain at this point - var catchAll = result.catchAll.map(function (c) { return `${c}@${fqdn}`; }).join(','); - var mailFromValidation = result.mailFromValidation; + const alertsFrom = `no-reply@${mailDomain.domain}`; + + user.getOwner(function (error, owner) { + const alertsTo = config.provider() === 'caas' ? [ 'support@cloudron.io' ] : [ ]; + alertsTo.concat(error ? [] : owner.email).join(','); // owner may not exist yet + + const mailOutDomain = mailDomain.domain; + const mailInDomain = mailDomain.enabled ? mailDomain.domain : ''; + const catchAll = mailDomain.catchAll.map(function (c) { return `${c}@${mailDomain.domain}`; }).join(','); + const mailFromValidation = mailDomain.mailFromValidation; if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/mail.ini', - `mail_in_domains=${fqdn}\nmail_out_domains=${fqdn}\nmail_default_domain=${fqdn}\nmail_server_name=${mailFqdn}\nalerts_from=${alertsFrom}\nalerts_to=${alertsTo}\ncatch_all=${catchAll}\nmail_from_validation=${mailFromValidation}\n`, 'utf8')) { + `mail_in_domains=${mailInDomain}\nmail_out_domains=${mailOutDomain}\nmail_default_domain=${mailDomain.domain}\nmail_server_name=${mailFqdn}\nalerts_from=${alertsFrom}\nalerts_to=${alertsTo}\ncatch_all=${catchAll}\nmail_from_validation=${mailFromValidation}\n`, 'utf8')) { return callback(new Error('Could not create mail var file:' + safe.error.message)); } - var relay = result.relay; + var relay = mailDomain.relay; const enabled = relay.provider !== 'cloudron-smtp' ? true : false, host = relay.host || '', @@ -468,7 +474,7 @@ function createMailConfig(callback) { return callback(new Error('Could not create mail var file:' + safe.error.message)); } - callback(); + callback(null, mailInDomain.length !== 0); }); }); } @@ -499,45 +505,28 @@ function restartMail(callback) { if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/tls_cert.pem', cert)) return callback(new Error('Could not create cert file:' + safe.error.message)); if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/tls_key.pem', key)) return callback(new Error('Could not create key file:' + safe.error.message)); - get(config.fqdn(), function (error, mailConfig) { + shell.execSync('startMail', 'docker rm -f mail || true'); + + createMailConfig(function (error, allowInbound) { if (error) return callback(error); - shell.execSync('startMail', 'docker rm -f mail || true'); + var ports = allowInbound ? '-p 587:2525 -p 993:9993 -p 4190:4190 -p 25:2525' : ''; - createMailConfig(function (error) { - if (error) return callback(error); + const cmd = `docker run --restart=always -d --name="mail" \ + --net cloudron \ + --net-alias mail \ + -m ${memoryLimit}m \ + --memory-swap ${memoryLimit * 2}m \ + --dns 172.18.0.1 \ + --dns-search=. \ + -v "${paths.MAIL_DATA_DIR}:/app/data" \ + -v "${paths.PLATFORM_DATA_DIR}/addons/mail:/etc/mail" \ + ${ports} \ + --read-only -v /run -v /tmp ${tag}`; - var ports = mailConfig.enabled ? '-p 587:2525 -p 993:9993 -p 4190:4190 -p 25:2525' : ''; + shell.execSync('startMail', cmd); - const cmd = `docker run --restart=always -d --name="mail" \ - --net cloudron \ - --net-alias mail \ - -m ${memoryLimit}m \ - --memory-swap ${memoryLimit * 2}m \ - --dns 172.18.0.1 \ - --dns-search=. \ - -v "${paths.MAIL_DATA_DIR}:/app/data" \ - -v "${paths.PLATFORM_DATA_DIR}/addons/mail:/etc/mail" \ - ${ports} \ - --read-only -v /run -v /tmp ${tag}`; - - shell.execSync('startMail', cmd); - - if (!mailConfig.enabled || process.env.BOX_ENV === 'test') return callback(); - - // Add MX and DMARC record. Note that DMARC policy depends on DKIM signing and thus works - // only if we use our internal mail server. - var records = [ - { subdomain: '_dmarc', type: 'TXT', values: [ '"v=DMARC1; p=reject; pct=100"' ] }, - { subdomain: '', type: 'MX', values: [ '10 ' + config.mailFqdn() + '.' ] } - ]; - - async.mapSeries(records, function (record, iteratorCallback) { - domains.upsertDNSRecords(record.subdomain, config.fqdn(), record.type, record.values, iteratorCallback); - }, NOOP_CALLBACK); // do not crash if DNS creds do not work in startup sequence - - callback(); - }); + callback(); }); }); } @@ -554,6 +543,16 @@ function get(domain, callback) { }); } +function getAll(callback) { + assert.strictEqual(typeof callback, 'function'); + + maildb.getAll(function (error, results) { + if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error)); + + return callback(null, results); + }); +} + function setMailFromValidation(domain, enabled, callback) { assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof enabled, 'boolean'); @@ -612,6 +611,19 @@ function setMailEnabled(domain, enabled, callback) { restartMail(NOOP_CALLBACK); + if (!enabled || process.env.BOX_ENV === 'test') return callback(); + + // Add MX and DMARC record. Note that DMARC policy depends on DKIM signing and thus works + // only if we use our internal mail server. + var records = [ + { subdomain: '_dmarc', type: 'TXT', values: [ '"v=DMARC1; p=reject; pct=100"' ] }, + { subdomain: '', type: 'MX', values: [ '10 ' + config.mailFqdn() + '.' ] } + ]; + + async.mapSeries(records, function (record, iteratorCallback) { + domains.upsertDNSRecords(record.subdomain, domain, record.type, record.values, iteratorCallback); + }, NOOP_CALLBACK); + callback(null); }); } diff --git a/src/maildb.js b/src/maildb.js index f827ae6e7..9f53050cc 100644 --- a/src/maildb.js +++ b/src/maildb.js @@ -4,6 +4,7 @@ exports = module.exports = { add: add, del: del, get: get, + getAll: getAll, update: update, _clear: clear, @@ -82,6 +83,18 @@ function get(domain, callback) { }); } +function getAll(callback) { + assert.strictEqual(typeof callback, 'function'); + + database.query('SELECT ' + MAILDB_FIELDS + ' FROM mail ORDER BY domain', function (error, results) { + if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error)); + + results.forEach(function (result) { postProcess(result); }); + + callback(null, results); + }); +} + function update(domain, data, callback) { assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof data, 'object'); diff --git a/src/mailer.js b/src/mailer.js index 23d002af7..c4c8ccdcd 100644 --- a/src/mailer.js +++ b/src/mailer.js @@ -31,8 +31,8 @@ var assert = require('assert'), config = require('./config.js'), debug = require('debug')('box:mailer'), docker = require('./docker.js').connection, - domains = require('./domains.js'), ejs = require('ejs'), + mail = require('./mail.js'), nodemailer = require('nodemailer'), path = require('path'), safe = require('safetydance'), @@ -74,15 +74,17 @@ function getMailConfig(callback) { cloudronName = 'Cloudron'; } - domains.getAll(function (error, domains) { + mail.getAll(function (error, domains) { if (error) return callback(error); if (domains.length === 0) return callback('No domains configured'); + const defaultDomain = domains[0]; + callback(null, { adminEmails: adminEmails, cloudronName: cloudronName, - notificationDomain: domains[0].domain, - notificationFrom: '"' + cloudronName + '" ' + notificationDomain: defaultDomain.domain, + notificationFrom: '"' + cloudronName + '" ' }); }); });