diff --git a/src/routes/test/settings-test.js b/src/routes/test/settings-test.js index a468ce0d9..e912041b7 100644 --- a/src/routes/test/settings-test.js +++ b/src/routes/test/settings-test.js @@ -531,7 +531,7 @@ describe('Settings API', function () { describe('email DNS records', function () { var resolve = null; var dnsAnswerQueue = []; - var dkimDomain, spfDomain; + var dkimDomain, spfDomain, mxDomain, dmarcDomain; before(function (done) { var dns = require('native-dns'); @@ -542,13 +542,15 @@ describe('Settings API', function () { expect(hostname).to.be.a('string'); expect(callback).to.be.a('function'); - if (!dnsAnswerQueue[hostname] || !dnsAnswerQueue[hostname][type]) return callback(new Error('no mock answer')); + if (!dnsAnswerQueue[hostname] || !(type in dnsAnswerQueue[hostname])) return callback(new Error('no mock answer')); callback(null, dnsAnswerQueue[hostname][type]); }; dkimDomain = 'cloudron._domainkey.' + config.fqdn(); spfDomain = config.fqdn(); + mxDomain = config.fqdn(); + dmarcDomain = '_dmarc.' + config.fqdn(); done(); }); @@ -571,152 +573,167 @@ describe('Settings API', function () { }); function clearDnsAnswerQueue() { - dnsAnswerQueue = { }; - dnsAnswerQueue[dkimDomain] = { }; - dnsAnswerQueue[spfDomain] = { }; + dnsAnswerQueue = { }; + dnsAnswerQueue[dkimDomain] = { }; + dnsAnswerQueue[spfDomain] = { }; + dnsAnswerQueue[mxDomain] = { }; + dnsAnswerQueue[dmarcDomain] = { }; } - it('succeeds without existing answers', function (done) { + it('succeeds with dns errors', function (done) { clearDnsAnswerQueue(); superagent.get(SERVER_URL + '/api/v1/settings/email_dns_records') .query({ access_token: token }) .end(function (err, res) { expect(res.statusCode).to.equal(200); - expect(res.body.dkim).to.be.an('object'); - expect(res.body.spf).to.be.an('object'); + expect(res.body.dkim).to.be.an('object'); expect(res.body.dkim.domain).to.eql(dkimDomain); expect(res.body.dkim.type).to.eql('TXT'); expect(res.body.dkim.value).to.eql(null); expect(res.body.dkim.expected).to.eql('v=DKIM1; t=s; p=' + cloudron.readDkimPublicKeySync()); expect(res.body.dkim.status).to.eql(false); + expect(res.body.spf).to.be.an('object'); expect(res.body.spf.domain).to.eql(spfDomain); expect(res.body.spf.type).to.eql('TXT'); expect(res.body.spf.value).to.eql(null); expect(res.body.spf.expected).to.eql('v=spf1 a:' + config.adminFqdn() + ' ~all'); expect(res.body.spf.status).to.eql(false); + expect(res.body.dmarc).to.be.an('object'); + expect(res.body.dmarc.type).to.eql('TXT'); + expect(res.body.dmarc.value).to.eql(null); + expect(res.body.dmarc.expected).to.eql('v=DMARC1; p=reject; pct=100'); + expect(res.body.dmarc.status).to.eql(false); + + expect(res.body.mx).to.be.an('object'); + expect(res.body.mx.type).to.eql('MX'); + expect(res.body.mx.value).to.eql(null); + expect(res.body.mx.expected).to.eql('10 ' + config.mailFqdn()); + expect(res.body.mx.status).to.eql(false); + + expect(res.body.ptr).to.be.an('object'); + expect(res.body.ptr.type).to.eql('PTR'); + // expect(res.body.ptr.value).to.eql(null); this will be anything random + expect(res.body.ptr.expected).to.eql(config.mailFqdn()); + expect(res.body.ptr.status).to.eql(false); + done(); }); }); - it('succeeds with existing dkim and no spf', function (done) { + it('succeeds with "undefined" spf, dkim, dmarc, mx, ptr records', function (done) { clearDnsAnswerQueue(); - dnsAnswerQueue[dkimDomain].TXT = [['v=DKIM1;', 't=s;', 'p=' + cloudron.readDkimPublicKeySync()]]; + dnsAnswerQueue[dkimDomain].TXT = null; + dnsAnswerQueue[spfDomain].TXT = null; + dnsAnswerQueue[mxDomain].MX = null; + dnsAnswerQueue[dmarcDomain].TXT = null; superagent.get(SERVER_URL + '/api/v1/settings/email_dns_records') .query({ access_token: token }) .end(function (err, res) { expect(res.statusCode).to.equal(200); - expect(res.body.dkim).to.be.an('object'); + expect(res.body.spf).to.be.an('object'); - - expect(res.body.dkim.domain).to.eql('cloudron._domainkey.' + config.fqdn()); - expect(res.body.dkim.type).to.eql('TXT'); - expect(res.body.dkim.value).to.eql('v=DKIM1; t=s; p=' + cloudron.readDkimPublicKeySync()); - expect(res.body.dkim.expected).to.eql('v=DKIM1; t=s; p=' + cloudron.readDkimPublicKeySync()); - expect(res.body.dkim.status).to.eql(true); - - expect(res.body.spf.domain).to.eql(config.fqdn()); - expect(res.body.spf.type).to.eql('TXT'); - expect(res.body.spf.value).to.eql(null); expect(res.body.spf.expected).to.eql('v=spf1 a:' + config.adminFqdn() + ' ~all'); expect(res.body.spf.status).to.eql(false); + expect(res.body.spf.value).to.eql(null); + + expect(res.body.dkim).to.be.an('object'); + expect(res.body.dkim.expected).to.eql('v=DKIM1; t=s; p=' + cloudron.readDkimPublicKeySync()); + expect(res.body.dkim.status).to.eql(false); + expect(res.body.dkim.value).to.eql(null); + + expect(res.body.dmarc).to.be.an('object'); + expect(res.body.dmarc.expected).to.eql('v=DMARC1; p=reject; pct=100'); + expect(res.body.dmarc.status).to.eql(false); + expect(res.body.dmarc.value).to.eql(null); + + expect(res.body.mx).to.be.an('object'); + expect(res.body.mx.status).to.eql(false); + expect(res.body.mx.expected).to.eql('10 ' + config.mailFqdn()); + expect(res.body.mx.value).to.eql(null); + + expect(res.body.ptr).to.be.an('object'); + expect(res.body.ptr.expected).to.eql(config.mailFqdn()); + expect(res.body.ptr.status).to.eql(false); + // expect(res.body.ptr.value).to.eql(null); this will be anything random done(); }); }); - it('succeeds with no dkim and existing spf', function (done) { + it('succeeds with all different spf, dkim, dmarc, mx, ptr records', function (done) { clearDnsAnswerQueue(); - dnsAnswerQueue[spfDomain].TXT = [['v=spf1', 'a:' + config.adminFqdn(), '~all']]; + dnsAnswerQueue[mxDomain].MX = [ { priority: '20', exchange: config.mailFqdn() }, { priority: '30', exchange: config.mailFqdn() } ]; + dnsAnswerQueue[dmarcDomain].TXT = [['v=DMARC2; p=reject; pct=100']]; + dnsAnswerQueue[dkimDomain].TXT = [['v=DKIM2; t=s; p=' + cloudron.readDkimPublicKeySync()]]; + dnsAnswerQueue[spfDomain].TXT = [['v=spf1 a:random.com ~all']]; superagent.get(SERVER_URL + '/api/v1/settings/email_dns_records') .query({ access_token: token }) .end(function (err, res) { expect(res.statusCode).to.equal(200); - expect(res.body.dkim).to.be.an('object'); - expect(res.body.spf).to.be.an('object'); - expect(res.body.dkim.domain).to.eql('cloudron._domainkey.' + config.fqdn()); - expect(res.body.dkim.type).to.eql('TXT'); - expect(res.body.dkim.value).to.eql(null); + expect(res.body.spf).to.be.an('object'); + expect(res.body.spf.expected).to.eql('v=spf1 a:' + config.adminFqdn() + ' a:random.com ~all'); + expect(res.body.spf.status).to.eql(false); + expect(res.body.spf.value).to.eql('v=spf1 a:random.com ~all'); + + expect(res.body.dkim).to.be.an('object'); expect(res.body.dkim.expected).to.eql('v=DKIM1; t=s; p=' + cloudron.readDkimPublicKeySync()); expect(res.body.dkim.status).to.eql(false); + expect(res.body.dkim.value).to.eql('v=DKIM2; t=s; p=' + cloudron.readDkimPublicKeySync()); - expect(res.body.spf.domain).to.eql(config.fqdn()); + expect(res.body.dmarc).to.be.an('object'); + expect(res.body.dmarc.expected).to.eql('v=DMARC1; p=reject; pct=100'); + expect(res.body.dmarc.status).to.eql(false); + expect(res.body.dmarc.value).to.eql('v=DMARC2; p=reject; pct=100'); + + expect(res.body.mx).to.be.an('object'); + expect(res.body.mx.status).to.eql(false); + expect(res.body.mx.expected).to.eql('10 ' + config.mailFqdn()); + expect(res.body.mx.value).to.eql('20 ' + config.mailFqdn() + ' 30 ' + config.mailFqdn()); + + expect(res.body.ptr).to.be.an('object'); + expect(res.body.ptr.expected).to.eql(config.mailFqdn()); + expect(res.body.ptr.status).to.eql(false); + // expect(res.body.ptr.value).to.eql(null); this will be anything random + + done(); + }); + }); + + it('succeeds with existing embedded spf', function (done) { + clearDnsAnswerQueue(); + + dnsAnswerQueue[spfDomain].TXT = [['v=spf1 a:example.com a:' + config.mailFqdn() + ' ~all']]; + + superagent.get(SERVER_URL + '/api/v1/settings/email_dns_records') + .query({ access_token: token }) + .end(function (err, res) { + expect(res.statusCode).to.equal(200); + + expect(res.body.spf).to.be.an('object'); + expect(res.body.spf.domain).to.eql(spfDomain); expect(res.body.spf.type).to.eql('TXT'); - expect(res.body.spf.value).to.eql('v=spf1 a:' + config.adminFqdn() + ' ~all'); - expect(res.body.spf.expected).to.eql('v=spf1 a:' + config.adminFqdn() + ' ~all'); + expect(res.body.spf.value).to.eql('v=spf1 a:example.com a:' + config.mailFqdn() + ' ~all'); + expect(res.body.spf.expected).to.eql('v=spf1 a:example.com a:' + config.mailFqdn() + ' ~all'); expect(res.body.spf.status).to.eql(true); done(); }); }); - it('succeeds with existing extra spf', function (done) { - clearDnsAnswerQueue(); - - dnsAnswerQueue[spfDomain].TXT = [['v=spf1', 'a:my-example.com', '~all']]; - - superagent.get(SERVER_URL + '/api/v1/settings/email_dns_records') - .query({ access_token: token }) - .end(function (err, res) { - expect(res.statusCode).to.equal(200); - expect(res.body.dkim).to.be.an('object'); - expect(res.body.spf).to.be.an('object'); - - expect(res.body.dkim.domain).to.eql('cloudron._domainkey.' + config.fqdn()); - expect(res.body.dkim.type).to.eql('TXT'); - expect(res.body.dkim.value).to.eql(null); - expect(res.body.dkim.expected).to.eql('v=DKIM1; t=s; p=' + cloudron.readDkimPublicKeySync()); - expect(res.body.dkim.status).to.eql(false); - - expect(res.body.spf.domain).to.eql(config.fqdn()); - expect(res.body.spf.type).to.eql('TXT'); - expect(res.body.spf.value).to.eql('v=spf1 a:my-example.com ~all'); - expect(res.body.spf.expected).to.eql('v=spf1 a:' + config.adminFqdn() + ' a:my-example.com ~all'); - expect(res.body.spf.status).to.eql(false); - - done(); - }); - }); - - it('succeeds with wrong dkim', function (done) { - clearDnsAnswerQueue(); - - dnsAnswerQueue[dkimDomain].TXT = [['v=DKIM1;', 't=s;', 'p=foobar']]; - - superagent.get(SERVER_URL + '/api/v1/settings/email_dns_records') - .query({ access_token: token }) - .end(function (err, res) { - expect(res.statusCode).to.equal(200); - expect(res.body.dkim).to.be.an('object'); - expect(res.body.spf).to.be.an('object'); - - expect(res.body.dkim.domain).to.eql('cloudron._domainkey.' + config.fqdn()); - expect(res.body.dkim.type).to.eql('TXT'); - expect(res.body.dkim.value).to.eql('v=DKIM1; t=s; p=foobar'); - expect(res.body.dkim.expected).to.eql('v=DKIM1; t=s; p=' + cloudron.readDkimPublicKeySync()); - expect(res.body.dkim.status).to.eql(false); - - expect(res.body.spf.domain).to.eql(config.fqdn()); - expect(res.body.spf.type).to.eql('TXT'); - expect(res.body.spf.value).to.eql(null); - expect(res.body.spf.expected).to.eql('v=spf1 a:' + config.adminFqdn() + ' ~all'); - expect(res.body.spf.status).to.eql(false); - - done(); - }); - }); - - it('succeeds with existing spf and dkim', function (done) { + it('succeeds with all correct records', function (done) { clearDnsAnswerQueue(); + dnsAnswerQueue[mxDomain].MX = [ { priority: '10', exchange: config.mailFqdn() } ]; + dnsAnswerQueue[dmarcDomain].TXT = [['v=DMARC1; p=reject; pct=100']]; dnsAnswerQueue[dkimDomain].TXT = [['v=DKIM1;', 't=s;', 'p=' + cloudron.readDkimPublicKeySync()]]; dnsAnswerQueue[spfDomain].TXT = [['v=spf1', 'a:' + config.adminFqdn(), '~all']]; @@ -724,21 +741,31 @@ describe('Settings API', function () { .query({ access_token: token }) .end(function (err, res) { expect(res.statusCode).to.equal(200); - expect(res.body.dkim).to.be.an('object'); - expect(res.body.spf).to.be.an('object'); - expect(res.body.dkim.domain).to.eql('cloudron._domainkey.' + config.fqdn()); + expect(res.body.dkim).to.be.an('object'); + expect(res.body.dkim.domain).to.eql(dkimDomain); expect(res.body.dkim.type).to.eql('TXT'); expect(res.body.dkim.value).to.eql('v=DKIM1; t=s; p=' + cloudron.readDkimPublicKeySync()); expect(res.body.dkim.expected).to.eql('v=DKIM1; t=s; p=' + cloudron.readDkimPublicKeySync()); expect(res.body.dkim.status).to.eql(true); - expect(res.body.spf.domain).to.eql(config.fqdn()); + expect(res.body.spf).to.be.an('object'); + expect(res.body.spf.domain).to.eql(spfDomain); expect(res.body.spf.type).to.eql('TXT'); expect(res.body.spf.value).to.eql('v=spf1 a:' + config.adminFqdn() + ' ~all'); expect(res.body.spf.expected).to.eql('v=spf1 a:' + config.adminFqdn() + ' ~all'); expect(res.body.spf.status).to.eql(true); + expect(res.body.dmarc).to.be.an('object'); + expect(res.body.dmarc.expected).to.eql('v=DMARC1; p=reject; pct=100'); + expect(res.body.dmarc.status).to.eql(true); + expect(res.body.dmarc.value).to.eql('v=DMARC1; p=reject; pct=100'); + + expect(res.body.mx).to.be.an('object'); + expect(res.body.mx.status).to.eql(true); + expect(res.body.mx.expected).to.eql('10 ' + config.mailFqdn()); + expect(res.body.mx.value).to.eql('10 ' + config.mailFqdn()); + done(); }); }); diff --git a/src/settings.js b/src/settings.js index e1612833e..3e2015b07 100644 --- a/src/settings.js +++ b/src/settings.js @@ -164,11 +164,10 @@ function getEmailDnsRecords(callback) { if (error && error.code === 'ENOTFOUND') return callback(null); // not setup if (error) return callback(error); - // ensure this is an array resolveTxt() returns undefined if no records are found - txtRecords = txtRecords || []; - - records.dkim.value = txtRecords[0].join(' '); - records.dkim.status = (records.dkim.value === records.dkim.expected); + if (Array.isArray(txtRecords) && txtRecords.length !== 0) { + records.dkim.value = txtRecords[0].join(' '); + records.dkim.status = (records.dkim.value === records.dkim.expected); + } callback(); }); @@ -183,13 +182,11 @@ function getEmailDnsRecords(callback) { status: false }; - // check if SPF is already setup dns.resolve(records.spf.domain, records.spf.type, function (error, txtRecords) { if (error && error.code === 'ENOTFOUND') return callback(null); // not setup if (error) return callback(error); - // ensure this is an array resolve() returns undefined if no records are found - txtRecords = txtRecords || []; + if (!Array.isArray(txtRecords)) return callback(); var i; for (i = 0; i < txtRecords.length; i++) { @@ -222,11 +219,10 @@ function getEmailDnsRecords(callback) { if (error && error.code === 'ENOTFOUND') return callback(null); // not setup if (error) return callback(error); - // ensure this is an array resolve() returns undefined if no records are found - mxRecords = mxRecords || []; - - records.mx.status = mxRecords.length == 1 && mxRecords[0].exchange === config.mailFqdn(); - records.mx.value = mxRecords.map(function (r) { return r.priority + ' ' + r.exchange; }).join(' '); + 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(); }); @@ -245,7 +241,7 @@ function getEmailDnsRecords(callback) { if (error && error.code === 'ENOTFOUND') return callback(null); // not setup if (error) return callback(error); - if (txtRecords && txtRecords[0]) { + if (Array.isArray(txtRecords) && txtRecords.length !== 0) { records.dmarc.value = txtRecords[0].join(' '); records.dmarc.status = (records.dmarc.value === records.dmarc.expected); } @@ -272,8 +268,10 @@ function getEmailDnsRecords(callback) { if (error && error.code === 'ENOTFOUND') return callback(null); // not setup if (error) return callback(error); - records.ptr.value = ptrRecords.join(' '); - records.ptr.status = records.ptr.value === config.mailFqdn(); + if (Array.isArray(ptrRecords) && ptrRecords.length !== 0) { + records.ptr.value = ptrRecords.join(' '); + records.ptr.status = ptrRecords.some(function (v) { return v === config.mailFqdn(); }); + } return callback(); });