Files
cloudron-box/src/mail.js

1312 lines
47 KiB
JavaScript
Raw Normal View History

2017-06-28 17:06:12 -05:00
'use strict';
exports = module.exports = {
getStatus: getStatus,
checkConfiguration: checkConfiguration,
2017-06-28 17:06:12 -05:00
2018-04-03 14:37:52 -07:00
getDomains: getDomains,
2018-04-03 14:37:52 -07:00
getDomain: getDomain,
clearDomains: clearDomains,
onDomainAdded: onDomainAdded,
onDomainRemoved: onDomainRemoved,
2019-02-15 10:55:15 -08:00
removePrivateFields: removePrivateFields,
setDnsRecords: setDnsRecords,
onMailFqdnChanged: onMailFqdnChanged,
2018-03-08 20:08:01 -08:00
validateName: validateName,
setMailFromValidation: setMailFromValidation,
setCatchAllAddress: setCatchAllAddress,
setMailRelay: setMailRelay,
2018-01-20 23:17:39 -08:00
setMailEnabled: setMailEnabled,
startMail: restartMail,
restartMail: restartMail,
handleCertChanged: handleCertChanged,
2019-11-05 19:54:53 -08:00
getMailAuth: getMailAuth,
sendTestMail: sendTestMail,
2018-12-06 10:23:10 -08:00
listMailboxes: listMailboxes,
2018-02-11 01:18:29 -08:00
removeMailboxes: removeMailboxes,
getMailbox: getMailbox,
addMailbox: addMailbox,
2018-12-06 11:41:16 -08:00
updateMailboxOwner: updateMailboxOwner,
removeMailbox: removeMailbox,
getAliases: getAliases,
setAliases: setAliases,
2018-01-26 10:22:50 +01:00
getLists: getLists,
getList: getList,
addList: addList,
updateList: updateList,
2018-01-26 10:22:50 +01:00
removeList: removeList,
2019-11-06 16:45:44 -08:00
resolveList: resolveList,
2018-01-26 10:22:50 +01:00
2019-10-24 13:34:14 -07:00
_readDkimPublicKeySync: readDkimPublicKeySync
2017-06-28 17:06:12 -05:00
};
2020-02-20 09:49:26 -08:00
var assert = require('assert'),
2017-06-28 17:06:12 -05:00
async = require('async'),
2019-10-24 13:34:14 -07:00
BoxError = require('./boxerror.js'),
2019-02-15 10:55:15 -08:00
constants = require('./constants.js'),
debug = require('debug')('box:mail'),
dns = require('./native-dns.js'),
2019-11-05 19:54:53 -08:00
docker = require('./docker.js'),
domains = require('./domains.js'),
2018-11-09 18:45:44 -08:00
eventlog = require('./eventlog.js'),
hat = require('./hat.js'),
infra = require('./infra_version.js'),
mailboxdb = require('./mailboxdb.js'),
maildb = require('./maildb.js'),
mailer = require('./mailer.js'),
2017-06-28 17:06:12 -05:00
net = require('net'),
nodemailer = require('nodemailer'),
path = require('path'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
shell = require('./shell.js'),
2017-06-28 17:06:12 -05:00
smtpTransport = require('nodemailer-smtp-transport'),
sysinfo = require('./sysinfo.js'),
2018-04-29 10:58:45 -07:00
users = require('./users.js'),
validator = require('validator'),
2017-06-28 17:06:12 -05:00
_ = require('underscore');
2019-03-25 11:43:01 -07:00
const DNS_OPTIONS = { timeout: 5000 };
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
2018-04-03 09:36:41 -07:00
function validateName(name) {
assert.strictEqual(typeof name, 'string');
2019-10-24 13:34:14 -07:00
if (name.length < 1) return new BoxError(BoxError.BAD_FIELD, 'mailbox name must be atleast 1 char');
if (name.length >= 200) return new BoxError(BoxError.BAD_FIELD, 'mailbox name too long');
// also need to consider valid LDAP characters here (e.g '+' is reserved)
2019-10-24 13:34:14 -07:00
if (/[^a-zA-Z0-9.-]/.test(name)) return new BoxError(BoxError.BAD_FIELD, 'mailbox name can only contain alphanumerals and dot');
return null;
}
function checkOutboundPort25(callback) {
2017-06-28 17:06:12 -05:00
assert.strictEqual(typeof callback, 'function');
var smtpServer = _.sample([
'smtp.gmail.com',
'smtp.live.com',
'smtp.mail.yahoo.com',
2018-02-08 11:48:55 -08:00
'smtp.1und1.de',
]);
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;
2019-01-25 10:27:44 -08:00
relay.value = `Connect to ${smtpServer} timed out. Check if port 25 (outbound) is blocked`;
client.destroy();
callback(new BoxError(BoxError.TIMEOUT, `Connect to ${smtpServer} timed out.`), relay);
});
client.on('error', function (error) {
relay.status = false;
2019-01-25 10:27:44 -08:00
relay.value = `Connect to ${smtpServer} failed: ${error.message}. Check if port 25 (outbound) is blocked`;
client.destroy();
2019-12-05 09:54:29 -08:00
callback(new BoxError(BoxError.NETWORK_ERROR, `Connect to ${smtpServer} failed.`), relay);
});
}
function checkSmtpRelay(relay, callback) {
var result = {
value: 'OK',
status: false
};
2017-06-28 17:06:12 -05:00
var options = {
2018-07-23 17:05:15 -07:00
connectionTimeout: 5000,
greetingTimeout: 5000,
2017-06-28 17:06:12 -05:00
host: relay.host,
port: relay.port
};
// only set auth if either username or password is provided, some relays auth based on IP (range)
if (relay.username || relay.password) {
options.auth = {
2017-06-28 17:06:12 -05:00
user: relay.username,
pass: relay.password
};
}
if (relay.acceptSelfSignedCerts) options.tls = { rejectUnauthorized: false };
var transporter = nodemailer.createTransport(smtpTransport(options));
2017-06-28 17:06:12 -05:00
transporter.verify(function(error) {
2017-06-29 10:11:55 -05:00
result.status = !error;
if (error) {
result.value = error.message;
return callback(error, result);
}
2017-06-28 17:06:12 -05:00
2017-12-04 17:10:02 +05:30
callback(null, result);
});
2017-06-28 17:06:12 -05:00
}
function verifyRelay(relay, callback) {
assert.strictEqual(typeof relay, 'object');
2017-06-28 17:06:12 -05:00
assert.strictEqual(typeof callback, 'function');
2018-03-08 23:23:02 -08:00
// we used to verify cloudron-smtp with checkOutboundPort25 but that is unreliable given that we just
// randomly select some smtp server
if (relay.provider === 'cloudron-smtp' || relay.provider === 'noop') return callback();
2017-06-28 17:06:12 -05:00
2018-03-08 23:23:02 -08:00
checkSmtpRelay(relay, function (error) {
2019-10-24 13:34:14 -07:00
if (error) return callback(new BoxError(BoxError.BAD_FIELD, error.message));
2017-06-28 17:06:12 -05:00
callback();
});
}
2017-06-28 17:06:12 -05:00
function checkDkim(mailDomain, callback) {
assert.strictEqual(typeof mailDomain, 'object');
assert.strictEqual(typeof callback, 'function');
const domain = mailDomain.domain;
let dkim = {
domain: `${mailDomain.dkimSelector}._domainkey.${domain}`,
name: `${mailDomain.dkimSelector}._domainkey`,
type: 'TXT',
expected: null,
value: null,
status: false
};
2017-06-28 17:06:12 -05:00
2018-01-25 14:51:07 -08:00
var dkimKey = readDkimPublicKeySync(domain);
2019-12-12 20:36:27 -08:00
if (!dkimKey) return callback(new BoxError(BoxError.FS_ERROR, `Failed to read dkim public key of ${domain}`), dkim);
2017-06-28 17:06:12 -05:00
dkim.expected = 'v=DKIM1; t=s; p=' + dkimKey;
2017-06-28 17:06:12 -05:00
dns.resolve(dkim.domain, dkim.type, DNS_OPTIONS, function (error, txtRecords) {
if (error) return callback(error, dkim);
2017-06-28 17:06:12 -05:00
if (txtRecords.length !== 0) {
dkim.value = txtRecords[0].join('');
const actual = txtToDict(dkim.value);
dkim.status = actual.p === dkimKey;
}
2017-06-28 17:06:12 -05:00
callback(null, dkim);
});
}
2017-06-28 17:06:12 -05:00
2019-01-31 15:08:14 -08:00
function checkSpf(domain, mailFqdn, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof mailFqdn, 'string');
assert.strictEqual(typeof callback, 'function');
var spf = {
2018-01-24 11:33:09 -08:00
domain: domain,
2018-07-24 14:03:39 -07:00
name: '@',
type: 'TXT',
value: null,
2019-01-31 15:08:14 -08:00
expected: 'v=spf1 a:' + mailFqdn + ' ~all',
status: false
};
dns.resolve(spf.domain, spf.type, DNS_OPTIONS, function (error, txtRecords) {
if (error) return callback(error, spf);
var i;
for (i = 0; i < txtRecords.length; i++) {
let txtRecord = txtRecords[i].join(''); // https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
if (txtRecord.indexOf('v=spf1 ') !== 0) continue; // not SPF
spf.value = txtRecord;
spf.status = spf.value.indexOf(' a:' + settings.adminFqdn()) !== -1;
break;
}
2017-06-28 17:06:12 -05:00
if (spf.status) {
spf.expected = spf.value;
} else if (i !== txtRecords.length) {
spf.expected = 'v=spf1 a:' + settings.adminFqdn() + ' ' + spf.value.slice('v=spf1 '.length);
}
2017-06-28 17:06:12 -05:00
callback(null, spf);
});
}
2017-06-28 17:06:12 -05:00
2019-01-31 15:08:14 -08:00
function checkMx(domain, mailFqdn, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof mailFqdn, 'string');
assert.strictEqual(typeof callback, 'function');
var mx = {
2018-01-24 11:33:09 -08:00
domain: domain,
2018-07-24 14:03:39 -07:00
name: '@',
type: 'MX',
value: null,
2019-01-31 15:08:14 -08:00
expected: '10 ' + mailFqdn + '.',
status: false
};
dns.resolve(mx.domain, mx.type, DNS_OPTIONS, function (error, mxRecords) {
if (error) return callback(error, mx);
if (mxRecords.length === 0) return callback(null, mx);
2020-04-22 17:16:52 -07:00
mx.status = mxRecords.some(mx => mx.exchange === mailFqdn); // this lets use change priority and/or setup backup MX
mx.value = mxRecords.map(function (r) { return r.priority + ' ' + r.exchange + '.'; }).join(' ');
if (mx.status) return callback(null, mx); // MX record is "my."
// cloudflare might create a conflict subdomain (https://support.cloudflare.com/hc/en-us/articles/360020296512-DNS-Troubleshooting-FAQ)
dns.resolve(mxRecords[0].exchange, 'A', DNS_OPTIONS, function (error, mxIps) {
if (error || mxIps.length !== 1) return callback(null, mx);
2019-10-29 15:46:33 -07:00
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(null, mx);
mx.status = mxIps[0] === ip;
2017-06-28 17:06:12 -05:00
callback(null, mx);
});
});
});
}
2017-06-28 17:06:12 -05:00
function txtToDict(txt) {
var dict = {};
txt.split(';').forEach(function(v) {
var p = v.trim().split('=');
dict[p[0]]=p[1];
});
return dict;
}
2018-01-24 11:33:09 -08:00
function checkDmarc(domain, callback) {
var dmarc = {
2018-01-24 11:33:09 -08:00
domain: '_dmarc.' + domain,
2018-07-24 14:03:39 -07:00
name: '_dmarc',
type: 'TXT',
value: null,
expected: 'v=DMARC1; p=reject; pct=100',
status: false
};
dns.resolve(dmarc.domain, dmarc.type, DNS_OPTIONS, function (error, txtRecords) {
if (error) return callback(error, dmarc);
if (txtRecords.length !== 0) {
dmarc.value = txtRecords[0].join('');
const actual = txtToDict(dmarc.value);
dmarc.status = actual.v === 'DMARC1'; // see box#666
}
2017-06-28 17:06:12 -05:00
callback(null, dmarc);
});
}
2017-06-28 17:06:12 -05:00
2019-01-31 15:08:14 -08:00
function checkPtr(mailFqdn, callback) {
assert.strictEqual(typeof mailFqdn, 'string');
assert.strictEqual(typeof callback, 'function');
var ptr = {
domain: null,
name: null,
type: 'PTR',
value: null,
2019-01-31 15:08:14 -08:00
expected: mailFqdn, // any trailing '.' is added by client software (https://lists.gt.net/spf/devel/7918)
status: false
};
2017-06-28 17:06:12 -05:00
2019-10-29 15:46:33 -07:00
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(error, ptr);
2017-06-28 17:06:12 -05:00
ptr.domain = ip.split('.').reverse().join('.') + '.in-addr.arpa';
ptr.name = ip;
2017-06-28 17:06:12 -05:00
dns.resolve(ptr.domain, 'PTR', DNS_OPTIONS, function (error, ptrRecords) {
if (error) return callback(error, ptr);
2017-06-28 17:06:12 -05:00
if (ptrRecords.length !== 0) {
ptr.value = ptrRecords.join(' ');
ptr.status = ptrRecords.some(function (v) { return v === ptr.expected; });
}
2017-06-28 17:06:12 -05:00
return callback(null, ptr);
2017-06-28 17:06:12 -05:00
});
});
}
2017-06-28 17:06:12 -05:00
2017-09-08 11:50:11 -07:00
// https://raw.githubusercontent.com/jawsome/node-dnsbl/master/list.json
const RBL_LIST = [
2018-03-05 14:26:53 -08:00
{
'name': 'Abuse.ch',
'dns': 'spam.abuse.ch',
'site': 'http://abuse.ch/'
},
2017-09-08 11:50:11 -07:00
{
2017-12-04 17:10:02 +05:30
'name': 'Barracuda',
'dns': 'b.barracudacentral.org',
'site': 'http://www.barracudacentral.org/rbl/removal-request'
2017-09-08 11:50:11 -07:00
},
{
2018-03-05 14:26:53 -08:00
'name': 'Composite Blocking List',
'dns': 'cbl.abuseat.org',
'site': 'http://www.abuseat.org'
},
{
'name': 'Multi SURBL',
'dns': 'multi.surbl.org',
'site': 'http://www.surbl.org'
},
{
'name': 'Passive Spam Block List',
'dns': 'psbl.surriel.com',
'site': 'https://psbl.org'
2017-09-08 11:50:11 -07:00
},
{
2017-12-04 17:10:02 +05:30
'name': 'Sorbs Aggregate Zone',
'dns': 'dnsbl.sorbs.net',
'site': 'http://dnsbl.sorbs.net/'
2017-09-08 11:50:11 -07:00
},
{
2017-12-04 17:10:02 +05:30
'name': 'Sorbs spam.dnsbl Zone',
'dns': 'spam.dnsbl.sorbs.net',
'site': 'http://sorbs.net'
2017-09-08 11:50:11 -07:00
},
{
2018-03-05 14:26:53 -08:00
'name': 'SpamCop',
'dns': 'bl.spamcop.net',
'site': 'http://spamcop.net'
},
{
'name': 'SpamHaus Zen',
'dns': 'zen.spamhaus.org',
'site': 'http://spamhaus.org'
2017-09-08 11:50:11 -07:00
},
{
2017-12-04 17:10:02 +05:30
'name': 'The Unsubscribe Blacklist(UBL)',
'dns': 'ubl.unsubscore.com ',
'site': 'http://www.lashback.com/blacklist/'
2017-09-08 11:50:11 -07:00
},
{
2017-12-04 17:10:02 +05:30
'name': 'UCEPROTECT Network',
'dns': 'dnsbl-1.uceprotect.net',
'site': 'http://www.uceprotect.net/en'
2017-09-08 11:50:11 -07:00
}
];
2018-01-24 11:33:09 -08:00
// this function currently only looks for black lists based on IP. TODO: also look up by domain
function checkRblStatus(domain, callback) {
2017-09-13 22:39:42 -07:00
assert.strictEqual(typeof callback, 'function');
2017-09-08 11:50:11 -07:00
2019-10-29 15:46:33 -07:00
sysinfo.getServerIp(function (error, ip) {
2017-09-08 11:50:11 -07:00
if (error) return callback(error, ip);
var flippedIp = ip.split('.').reverse().join('.');
// https://tools.ietf.org/html/rfc5782
async.map(RBL_LIST, function (rblServer, iteratorDone) {
dns.resolve(flippedIp + '.' + rblServer.dns, 'A', DNS_OPTIONS, function (error, records) {
2017-09-08 11:50:11 -07:00
if (error || !records) return iteratorDone(null, null); // not listed
2018-01-24 11:33:09 -08:00
debug('checkRblStatus: %s (ip: %s) is in the blacklist of %j', domain, flippedIp, rblServer);
2017-09-08 11:50:11 -07:00
var result = _.extend({ }, rblServer);
dns.resolve(flippedIp + '.' + rblServer.dns, 'TXT', DNS_OPTIONS, function (error, txtRecords) {
result.txtRecords = error || !txtRecords ? 'No txt record' : txtRecords.map(x => x.join(''));
2017-09-08 11:50:11 -07:00
2018-01-24 11:33:09 -08:00
debug('checkRblStatus: %s (error: %s) (txtRecords: %j)', domain, error, txtRecords);
2017-09-08 11:50:11 -07:00
return iteratorDone(null, result);
});
});
}, function (ignoredError, blacklistedServers) {
blacklistedServers = blacklistedServers.filter(function(b) { return b !== null; });
2018-01-24 11:33:09 -08:00
debug('checkRblStatus: %s (ip: %s) servers: %j', domain, ip, blacklistedServers);
2017-09-13 22:39:42 -07:00
return callback(null, { status: blacklistedServers.length === 0, ip: ip, servers: blacklistedServers });
});
});
}
2018-01-21 00:40:30 -08:00
function getStatus(domain, callback) {
assert.strictEqual(typeof domain, 'string');
2017-09-13 22:39:42 -07:00
assert.strictEqual(typeof callback, 'function');
// ensure we always have a valid toplevel properties for the api
var results = {
2020-02-27 10:36:35 -08:00
dns: {}, // { mx/dmarc/dkim/spf/ptr: { expected, value, name, domain, type, status } }
2019-03-06 19:48:18 -08:00
rbl: {}, // { status, ip, servers: [{name,site,dns}]} optional. only for cloudron-smtp
relay: {} // { status, value } always checked
};
2017-09-13 22:39:42 -07:00
function recordResult(what, func) {
return function (callback) {
func(function (error, result) {
if (error) debug('Ignored error - ' + what + ':', error);
2019-12-12 20:36:27 -08:00
safe.set(results, what, result || {});
2017-09-13 22:39:42 -07:00
callback();
});
};
}
2017-09-08 11:50:11 -07:00
const mailFqdn = settings.mailFqdn();
2019-01-31 15:08:14 -08:00
getDomain(domain, function (error, mailDomain) {
2017-09-13 22:39:42 -07:00
if (error) return callback(error);
2017-09-08 11:50:11 -07:00
let checks = [];
if (mailDomain.enabled) {
checks.push(
recordResult('dns.mx', checkMx.bind(null, domain, mailFqdn)),
recordResult('dns.dmarc', checkDmarc.bind(null, domain))
);
}
2017-09-13 22:39:42 -07:00
if (mailDomain.relay.provider === 'cloudron-smtp') {
2017-09-13 22:39:42 -07:00
// these tests currently only make sense when using Cloudron's SMTP server at this point
checks.push(
2019-01-31 15:08:14 -08:00
recordResult('dns.spf', checkSpf.bind(null, domain, mailFqdn)),
recordResult('dns.dkim', checkDkim.bind(null, mailDomain)),
2019-01-31 15:08:14 -08:00
recordResult('dns.ptr', checkPtr.bind(null, mailFqdn)),
2017-09-13 22:39:42 -07:00
recordResult('relay', checkOutboundPort25),
2018-01-24 11:33:09 -08:00
recordResult('rbl', checkRblStatus.bind(null, domain))
2017-09-13 22:39:42 -07:00
);
} else if (mailDomain.relay.provider !== 'noop') {
checks.push(recordResult('relay', checkSmtpRelay.bind(null, mailDomain.relay)));
2017-09-13 22:39:42 -07:00
}
async.parallel(checks, function () {
callback(null, results);
2017-09-08 11:50:11 -07:00
});
});
}
function checkConfiguration(callback) {
assert.strictEqual(typeof callback, 'function');
let messages = {};
domains.getAll(function (error, allDomains) {
if (error) return callback(error);
async.eachSeries(allDomains, function (domainObject, iteratorCallback) {
getStatus(domainObject.domain, function (error, result) {
if (error) return iteratorCallback(error);
let message = [];
Object.keys(result.dns).forEach((type) => {
const record = result.dns[type];
if (!record.status) message.push(`${type.toUpperCase()} DNS record (${record.type}) did not match.\n * Hostname: \`${record.name}\`\n * Expected: \`${record.expected}\`\n * Actual: \`${record.value}\``);
});
2019-03-17 17:48:14 -07:00
if (result.relay && result.relay.status === false) message.push(`Relay error: ${result.relay.value}`);
if (result.rbl && result.rbl.status === false) { // rbl field contents is optional
const servers = result.rbl.servers.map((bs) => `[${bs.name}](${bs.site})`); // in markdown
message.push(`This server's IP \`${result.rbl.ip}\` is blacklisted in the following servers - ${servers.join(', ')}`);
}
if (message.length) messages[domainObject.domain] = message;
iteratorCallback(null);
});
}, function (error) {
if (error) return callback(error);
// create bulleted list for each domain
let markdownMessage = '';
Object.keys(messages).forEach((domain) => {
markdownMessage += `**${domain}**\n`;
markdownMessage += messages[domain].map((m) => `* ${m}\n`).join('');
markdownMessage += '\n\n';
});
2019-08-04 15:35:42 -07:00
if (markdownMessage) markdownMessage += 'Email Status is checked every 30 minutes.\n See the [troubleshooting docs](https://cloudron.io/documentation/troubleshooting/#mail-dns) for more information.\n';
2019-03-01 11:24:10 -08:00
callback(null, markdownMessage); // empty message means all status checks succeeded
});
});
}
function createMailConfig(mailFqdn, mailDomain, callback) {
2019-01-31 15:08:14 -08:00
assert.strictEqual(typeof mailFqdn, 'string');
assert.strictEqual(typeof mailDomain, 'string');
assert.strictEqual(typeof callback, 'function');
debug('createMailConfig: generating mail config');
2018-04-03 14:37:52 -07:00
getDomains(function (error, mailDomains) {
2018-01-24 11:33:09 -08:00
if (error) return callback(error);
const mailOutDomains = mailDomains.filter(d => d.relay.provider !== 'noop').map(d => d.domain).join(',');
const mailInDomains = mailDomains.filter(function (d) { return d.enabled; }).map(function (d) { return d.domain; }).join(',');
2018-02-05 15:02:34 -08:00
// mail_domain is used for SRS
if (!safe.fs.writeFileSync(path.join(paths.ADDON_CONFIG_DIR, 'mail/mail.ini'),
`mail_in_domains=${mailInDomains}\nmail_out_domains=${mailOutDomains}\nmail_server_name=${mailFqdn}\nmail_domain=${mailDomain}\n\n`, 'utf8')) {
return callback(new BoxError(BoxError.FS_ERROR, 'Could not create mail var file:' + safe.error.message));
}
// enable_outbound makes plugin forward email for relayed mail. non-relayed mail always hits LMTP plugin first
if (!safe.fs.writeFileSync(path.join(paths.ADDON_CONFIG_DIR, 'mail/smtp_forward.ini'), 'enable_outbound=false\ndomain_selector=mail_from\n', 'utf8')) {
return callback(new BoxError(BoxError.FS_ERROR, 'Could not create smtp forward file:' + safe.error.message));
}
// create sections for per-domain configuration
mailDomains.forEach(function (domain) {
const catchAll = domain.catchAll.map(function (c) { return `${c}@${domain.domain}`; }).join(',');
const mailFromValidation = domain.mailFromValidation;
if (!safe.fs.appendFileSync(path.join(paths.ADDON_CONFIG_DIR, 'mail/mail.ini'),
`[${domain.domain}]\ncatch_all=${catchAll}\nmail_from_validation=${mailFromValidation}\n\n`, 'utf8')) {
return callback(new BoxError(BoxError.FS_ERROR, 'Could not create mail var file:' + safe.error.message));
2018-02-05 15:02:34 -08:00
}
const relay = domain.relay;
2018-02-05 15:02:34 -08:00
const enableRelay = relay.provider !== 'cloudron-smtp' && relay.provider !== 'noop',
host = relay.host || '',
port = relay.port || 25,
authType = relay.username ? 'plain' : '',
username = relay.username || '',
password = relay.password || '';
if (!enableRelay) return;
if (!safe.fs.appendFileSync(paths.ADDON_CONFIG_DIR + '/mail/smtp_forward.ini',
`[${domain.domain}]\nenable_outbound=true\nhost=${host}\nport=${port}\nenable_tls=true\nauth_type=${authType}\nauth_user=${username}\nauth_pass=${password}\n\n`, 'utf8')) {
return callback(new BoxError(BoxError.FS_ERROR, 'Could not create mail var file:' + safe.error.message));
}
});
callback(null, mailInDomains.length !== 0 /* allowInbound */);
});
}
2019-02-04 17:10:07 -08:00
function configureMail(mailFqdn, mailDomain, callback) {
assert.strictEqual(typeof mailFqdn, 'string');
assert.strictEqual(typeof mailDomain, 'string');
assert.strictEqual(typeof callback, 'function');
// mail (note: 2525 is hardcoded in mail container and app use this port)
// MAIL_SERVER_NAME is the hostname of the mailserver i.e server uses these certs
// MAIL_DOMAIN is the domain for which this server is relaying mails
// mail container uses /app/data for backed up data and /run for restart-able data
const tag = infra.images.mail.tag;
const memoryLimit = 4 * 256;
2018-12-28 13:32:37 -08:00
const cloudronToken = hat(8 * 128), relayToken = hat(8 * 128);
2019-02-04 17:10:07 -08:00
reverseProxy.getCertificate(mailFqdn, mailDomain, function (error, bundle) {
if (error) return callback(error);
// the setup script copies dhparams.pem to /addons/mail
const mailCertFilePath = path.join(paths.ADDON_CONFIG_DIR, 'mail/tls_cert.pem');
2018-01-31 22:31:14 -08:00
const mailKeyFilePath = path.join(paths.ADDON_CONFIG_DIR, 'mail/tls_key.pem');
if (!safe.child_process.execSync(`cp ${bundle.certFilePath} ${mailCertFilePath}`)) return callback(new BoxError(BoxError.FS_ERROR, 'Could not create cert file:' + safe.error.message));
if (!safe.child_process.execSync(`cp ${bundle.keyFilePath} ${mailKeyFilePath}`)) return callback(new BoxError(BoxError.FS_ERROR, 'Could not create key file:' + safe.error.message));
shell.exec('startMail', 'docker rm -f mail || true', function (error) {
2018-01-24 11:33:09 -08:00
if (error) return callback(error);
createMailConfig(mailFqdn, mailDomain, function (error, allowInbound) {
if (error) return callback(error);
var ports = allowInbound ? '-p 587:2525 -p 993:9993 -p 4190:4190 -p 25:2525' : '';
const cmd = `docker run --restart=always -d --name="mail" \
--net cloudron \
--net-alias mail \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=mail \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_MAIL_TOKEN="${cloudronToken}" \
2018-12-28 13:32:37 -08:00
-e CLOUDRON_RELAY_TOKEN="${relayToken}" \
-v "${paths.MAIL_DATA_DIR}:/app/data" \
-v "${paths.PLATFORM_DATA_DIR}/addons/mail:/etc/mail" \
${ports} \
--label isCloudronManaged=true \
--read-only -v /run -v /tmp ${tag}`;
shell.exec('startMail', cmd, callback);
});
});
});
}
2019-11-05 19:54:53 -08:00
function getMailAuth(callback) {
assert.strictEqual(typeof callback, 'function');
docker.inspect('mail', function (error, data) {
if (error) return callback(error);
const ip = safe.query(data, 'NetworkSettings.Networks.cloudron.IPAddress');
if (!ip) return callback(new BoxError(BoxError.MAIL_ERROR, 'Error querying mail server IP'));
// extract the relay token for auth
const env = safe.query(data, 'Config.Env', null);
if (!env) return callback(new BoxError(BoxError.MAIL_ERROR, 'Error getting mail env'));
const tmp = env.find(function (e) { return e.indexOf('CLOUDRON_RELAY_TOKEN') === 0; });
if (!tmp) return callback(new BoxError(BoxError.MAIL_ERROR, 'Error getting CLOUDRON_RELAY_TOKEN env var'));
const relayToken = tmp.slice('CLOUDRON_RELAY_TOKEN'.length + 1); // +1 for the = sign
if (!relayToken) return callback(new BoxError(BoxError.MAIL_ERROR, 'Error parsing CLOUDRON_RELAY_TOKEN'));
callback(null, {
ip,
port: constants.INTERNAL_SMTP_PORT,
relayToken
});
});
}
2019-02-04 17:10:07 -08:00
function restartMail(callback) {
assert.strictEqual(typeof callback, 'function');
if (process.env.BOX_ENV === 'test' && !process.env.TEST_CREATE_INFRA) return callback();
debug(`restartMail: restarting mail container with ${settings.mailFqdn()} ${settings.adminDomain()}`);
configureMail(settings.mailFqdn(), settings.adminDomain(), callback);
2019-02-04 17:10:07 -08:00
}
function restartMailIfActivated(callback) {
assert.strictEqual(typeof callback, 'function');
users.isActivated(function (error, activated) {
2019-10-24 13:34:14 -07:00
if (error) return callback(error);
if (!activated) {
debug('restartMailIfActivated: skipping restart of mail container since Cloudron is not activated yet');
return callback(); // not provisioned yet, do not restart container after dns setup
}
restartMail(callback);
});
}
function handleCertChanged(callback) {
2019-03-04 18:11:07 -08:00
assert.strictEqual(typeof callback, 'function');
debug('handleCertChanged: will restart if activated');
restartMailIfActivated(callback);
}
2018-04-03 14:37:52 -07:00
function getDomain(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
maildb.get(domain, function (error, result) {
2019-10-24 13:34:14 -07:00
if (error) return callback(error);
return callback(null, result);
});
}
2018-04-03 14:37:52 -07:00
function getDomains(callback) {
2018-01-24 11:33:09 -08:00
assert.strictEqual(typeof callback, 'function');
2018-12-06 11:41:16 -08:00
maildb.list(function (error, results) {
2019-10-24 13:34:14 -07:00
if (error) return callback(error);
2018-01-24 11:33:09 -08:00
return callback(null, results);
});
}
// https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
2019-01-31 15:08:14 -08:00
function txtRecordsWithSpf(domain, mailFqdn, callback) {
assert.strictEqual(typeof domain, 'string');
2019-01-31 15:08:14 -08:00
assert.strictEqual(typeof mailFqdn, 'string');
assert.strictEqual(typeof callback, 'function');
2018-02-08 12:05:29 -08:00
domains.getDnsRecords('', domain, 'TXT', function (error, txtRecords) {
2019-10-24 13:34:14 -07:00
if (error) return error;
debug('txtRecordsWithSpf: current txt records - %j', txtRecords);
var i, matches, validSpf;
for (i = 0; i < txtRecords.length; i++) {
matches = txtRecords[i].match(/^("?v=spf1) /); // DO backend may return without quotes
if (matches === null) continue;
// this won't work if the entry is arbitrarily "split" across quoted strings
2019-01-31 15:08:14 -08:00
validSpf = txtRecords[i].indexOf('a:' + mailFqdn) !== -1;
break; // there can only be one SPF record
}
if (validSpf) return callback(null, null);
if (!matches) { // no spf record was found, create one
2019-01-31 15:08:14 -08:00
txtRecords.push('"v=spf1 a:' + mailFqdn + ' ~all"');
debug('txtRecordsWithSpf: adding txt record');
} else { // just add ourself
2019-01-31 15:08:14 -08:00
txtRecords[i] = matches[1] + ' a:' + mailFqdn + txtRecords[i].slice(matches[1].length);
debug('txtRecordsWithSpf: inserting txt record');
}
return callback(null, txtRecords);
});
}
function ensureDkimKeySync(mailDomain) {
assert.strictEqual(typeof mailDomain, 'object');
const domain = mailDomain.domain;
const dkimPath = path.join(paths.MAIL_DATA_DIR, `dkim/${domain}`);
const dkimPrivateKeyFile = path.join(dkimPath, 'private');
const dkimPublicKeyFile = path.join(dkimPath, 'public');
const dkimSelectorFile = path.join(dkimPath, 'selector');
2018-03-08 15:29:18 -08:00
if (safe.fs.existsSync(dkimPublicKeyFile) &&
safe.fs.existsSync(dkimPublicKeyFile) &&
safe.fs.existsSync(dkimPublicKeyFile)) {
debug(`Reusing existing DKIM keys for ${domain}`);
return null;
}
debug(`Generating new DKIM keys for ${domain}`);
if (!safe.fs.mkdirSync(dkimPath) && safe.error.code !== 'EEXIST') {
debug('Error creating dkim.', safe.error);
2019-10-24 13:34:14 -07:00
return new BoxError(BoxError.FS_ERROR, safe.error);
}
2020-04-17 10:29:12 -07:00
// https://www.unlocktheinbox.com/dkim-key-length-statistics/ and https://docs.aws.amazon.com/ses/latest/DeveloperGuide/send-email-authentication-dkim-easy.html for key size
2019-10-24 13:34:14 -07:00
if (!safe.child_process.execSync('openssl genrsa -out ' + dkimPrivateKeyFile + ' 1024')) return new BoxError(BoxError.OPENSSL_ERROR, safe.error);
if (!safe.child_process.execSync('openssl rsa -in ' + dkimPrivateKeyFile + ' -out ' + dkimPublicKeyFile + ' -pubout -outform PEM')) return new BoxError(BoxError.OPENSSL_ERROR, safe.error);
2019-10-24 13:34:14 -07:00
if (!safe.fs.writeFileSync(dkimSelectorFile, mailDomain.dkimSelector, 'utf8')) return new BoxError(BoxError.FS_ERROR, safe.error);
// if the 'yellowtent' user of OS and the 'cloudron' user of mail container don't match, the keys become inaccessible by mail code
2019-10-24 13:34:14 -07:00
if (!safe.fs.chmodSync(dkimPrivateKeyFile, 0o644)) return new BoxError(BoxError.FS_ERROR, safe.error);
return null;
}
2018-01-25 14:51:07 -08:00
function readDkimPublicKeySync(domain) {
assert.strictEqual(typeof domain, 'string');
var dkimPath = path.join(paths.MAIL_DATA_DIR, `dkim/${domain}`);
var dkimPublicKeyFile = path.join(dkimPath, 'public');
var publicKey = safe.fs.readFileSync(dkimPublicKeyFile, 'utf8');
if (publicKey === null) {
debug('Error reading dkim public key.', safe.error);
return null;
}
// remove header, footer and new lines
publicKey = publicKey.split('\n').slice(1, -2).join('');
return publicKey;
}
2019-02-04 20:51:26 -08:00
function upsertDnsRecords(domain, mailFqdn, callback) {
assert.strictEqual(typeof domain, 'string');
2019-01-31 15:08:14 -08:00
assert.strictEqual(typeof mailFqdn, 'string');
assert.strictEqual(typeof callback, 'function');
2019-04-08 12:23:11 -07:00
debug(`upsertDnsRecords: updating mail dns records of domain ${domain} and mail fqdn ${mailFqdn}`);
maildb.get(domain, function (error, mailDomain) {
2019-10-24 13:34:14 -07:00
if (error) return callback(error);
error = ensureDkimKeySync(mailDomain);
if (error) return callback(error);
if (process.env.BOX_ENV === 'test') return callback();
2018-03-08 16:02:13 -08:00
var dkimKey = readDkimPublicKeySync(domain);
if (!dkimKey) return callback(new BoxError(BoxError.FS_ERROR, 'Failed to read dkim public key'));
// t=s limits the domainkey to this domain and not it's subdomains
var dkimRecord = { subdomain: `${mailDomain.dkimSelector}._domainkey`, domain: domain, type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
var records = [ ];
records.push(dkimRecord);
if (mailDomain.enabled) {
2018-07-25 11:41:42 -07:00
records.push({ subdomain: '_dmarc', domain: domain, type: 'TXT', values: [ '"v=DMARC1; p=reject; pct=100"' ] });
2019-01-31 15:08:14 -08:00
records.push({ subdomain: '', domain: domain, type: 'MX', values: [ '10 ' + mailFqdn + '.' ] });
}
2019-02-04 20:24:28 -08:00
txtRecordsWithSpf(domain, mailFqdn, function (error, txtRecords) {
if (error) return callback(error);
if (txtRecords) records.push({ subdomain: '', domain: domain, type: 'TXT', values: txtRecords });
2019-02-04 20:51:26 -08:00
debug('upsertDnsRecords: will update %j', records);
async.mapSeries(records, function (record, iteratorCallback) {
domains.upsertDnsRecords(record.subdomain, record.domain, record.type, record.values, iteratorCallback);
}, function (error, changeIds) {
if (error) {
2019-02-04 20:51:26 -08:00
debug(`upsertDnsRecords: failed to update: ${error}`);
2019-10-24 13:34:14 -07:00
return callback(error);
}
2019-02-04 20:51:26 -08:00
debug('upsertDnsRecords: records %j added with changeIds %j', records, changeIds);
callback(null);
});
});
});
}
2019-02-04 20:51:26 -08:00
function setDnsRecords(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
upsertDnsRecords(domain, settings.mailFqdn(), callback);
2019-02-04 20:51:26 -08:00
}
function onMailFqdnChanged(callback) {
assert.strictEqual(typeof callback, 'function');
const mailFqdn = settings.mailFqdn(),
mailDomain = settings.adminDomain();
domains.getAll(function (error, allDomains) {
2019-10-24 20:48:38 -07:00
if (error) return callback(error);
async.eachOfSeries(allDomains, function (domainObject, idx, iteratorDone) {
2019-02-04 20:51:26 -08:00
upsertDnsRecords(domainObject.domain, mailFqdn, iteratorDone);
}, function (error) {
2019-10-24 13:34:14 -07:00
if (error) return callback(error);
configureMail(mailFqdn, mailDomain, callback);
});
});
}
function onDomainAdded(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
async.series([
upsertDnsRecords.bind(null, domain, settings.mailFqdn()), // do this first to ensure DKIM keys
restartMailIfActivated
], callback);
}
function onDomainRemoved(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
restartMail(callback);
}
function clearDomains(callback) {
assert.strictEqual(typeof callback, 'function');
maildb.clear(function (error) {
2019-10-24 13:34:14 -07:00
if (error) return callback(error);
callback();
});
}
2019-02-15 10:55:15 -08:00
// remove all fields that should never be sent out via REST API
function removePrivateFields(domain) {
let result = _.pick(domain, 'domain', 'enabled', 'mailFromValidation', 'catchAll', 'relay');
2019-02-15 11:44:33 -08:00
if (result.relay.provider !== 'cloudron-smtp') {
if (result.relay.username === result.relay.password) result.relay.username = constants.SECRET_PLACEHOLDER;
result.relay.password = constants.SECRET_PLACEHOLDER;
}
2019-02-15 10:55:15 -08:00
return result;
}
2018-01-20 23:17:39 -08:00
function setMailFromValidation(domain, enabled, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof enabled, 'boolean');
assert.strictEqual(typeof callback, 'function');
2018-01-21 00:06:08 -08:00
maildb.update(domain, { mailFromValidation: enabled }, function (error) {
2019-10-24 13:34:14 -07:00
if (error) return callback(error);
restartMail(NOOP_CALLBACK); // have to restart mail container since haraka cannot watch symlinked config files (mail.ini)
callback(null);
});
}
function setCatchAllAddress(domain, addresses, callback) {
2018-01-20 23:17:39 -08:00
assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(addresses));
assert.strictEqual(typeof callback, 'function');
maildb.update(domain, { catchAll: addresses }, function (error) {
2019-10-24 13:34:14 -07:00
if (error) return callback(error);
restartMail(NOOP_CALLBACK); // have to restart mail container since haraka cannot watch symlinked config files (mail.ini)
callback(null);
});
}
2018-01-20 23:17:39 -08:00
function setMailRelay(domain, relay, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof relay, 'object');
assert.strictEqual(typeof callback, 'function');
2019-02-15 10:55:15 -08:00
getDomain(domain, function (error, result) {
if (error) return callback(error);
2019-02-15 11:44:33 -08:00
// inject current username/password
if (result.relay.provider === relay.provider) {
if (relay.username === constants.SECRET_PLACEHOLDER) relay.username = result.relay.username;
if (relay.password === constants.SECRET_PLACEHOLDER) relay.password = result.relay.password;
}
2019-02-15 10:55:15 -08:00
verifyRelay(relay, function (error) {
if (error) return callback(error);
2019-02-15 10:55:15 -08:00
maildb.update(domain, { relay: relay }, function (error) {
2019-10-24 13:34:14 -07:00
if (error) return callback(error);
2019-02-15 10:55:15 -08:00
restartMail(NOOP_CALLBACK);
callback(null);
});
});
});
}
function setMailEnabled(domain, enabled, auditSource, callback) {
2018-01-20 23:17:39 -08:00
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof enabled, 'boolean');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
2018-06-09 18:26:00 -07:00
maildb.update(domain, { enabled: enabled }, function (error) {
2019-10-24 13:34:14 -07:00
if (error) return callback(error);
2018-01-24 11:33:09 -08:00
2018-06-09 18:26:00 -07:00
restartMail(NOOP_CALLBACK);
2018-01-24 11:33:09 -08:00
2018-11-10 00:32:27 -08:00
eventlog.add(enabled ? eventlog.ACTION_MAIL_ENABLED : eventlog.ACTION_MAIL_DISABLED, auditSource, { domain });
callback(null);
});
}
function sendTestMail(domain, to, callback) {
assert.strictEqual(typeof domain, 'string');
2018-02-03 18:27:55 -08:00
assert.strictEqual(typeof to, 'string');
assert.strictEqual(typeof callback, 'function');
2018-04-03 14:37:52 -07:00
getDomain(domain, function (error, result) {
if (error) return callback(error);
2019-04-15 16:47:43 -07:00
mailer.sendTestMail(result.domain, to, function (error) {
2019-10-24 13:34:14 -07:00
if (error) return callback(error);
2019-04-15 16:47:43 -07:00
callback();
});
});
}
2019-10-22 10:11:35 -07:00
function listMailboxes(domain, page, perPage, callback) {
assert.strictEqual(typeof domain, 'string');
2019-10-22 10:11:35 -07:00
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
2019-10-22 10:11:35 -07:00
mailboxdb.listMailboxes(domain, page, perPage, function (error, result) {
2019-10-24 13:34:14 -07:00
if (error) return callback(error);
callback(null, result);
});
}
2018-02-11 01:18:29 -08:00
function removeMailboxes(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.delByDomain(domain, function (error) {
2019-10-24 13:34:14 -07:00
if (error) return callback(error);
2018-02-11 01:18:29 -08:00
callback();
});
}
function getMailbox(name, domain, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.getMailbox(name, domain, function (error, result) {
2019-10-24 13:34:14 -07:00
if (error) return callback(error);
callback(null, result);
});
}
2018-11-09 18:45:44 -08:00
function addMailbox(name, domain, userId, auditSource, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof userId, 'string');
2018-11-09 18:45:44 -08:00
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
2018-04-05 16:07:51 -07:00
name = name.toLowerCase();
2018-04-05 16:07:51 -07:00
var error = validateName(name);
if (error) return callback(error);
2018-04-03 14:27:09 -07:00
rework how app mailboxes are allocated Our current setup had a mailbox allocated for an app during app install (into the mailboxes table). This has many issues: * When set to a custom mailbox location, there was no way to access this mailbox even via IMAP. Even when using app credentials, we cannot use IMAP since the ldap logic was testing on the addon type (most of our apps only use sendmail addon and thus cannot recvmail). * The mailboxes table was being used to add hidden 'app' type entries. This made it very hard for the user to understand why a mailbox conflicts. For example, if you set an app to use custom mailbox 'blog', this is hidden from all views. The solution is to let an app send email as whatever mailbox name is allocated to it (which we now track in the apps table. the default is in the db already so that REST response contains it). When not using Cloudron email, it will just send mail as that mailbox and the auth checks the "app password" in the addons table. Any replies to that mailbox will end up in the domain's mail server (not our problem). When using cloudron email, the app can send mail like above. Any responses will not end anywhere and bounce since there is no 'mailbox'. This is the expected behavior. If user wants to access this mailbox name, he can create a concrete mailbox and set himself as owner OR set this as an alias. For apps using the recvmail addon, the workflow is to actually create a mailbox at some point. Currently, we have no UI for this 'flow'. It's fine because we have only meemo using it. Intuitive much!
2018-12-06 21:08:19 -08:00
mailboxdb.addMailbox(name, domain, userId, function (error) {
2019-10-24 13:34:14 -07:00
if (error) return callback(error);
2018-04-05 16:07:51 -07:00
2018-11-09 18:49:55 -08:00
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_ADD, auditSource, { name, domain, userId });
2018-11-09 18:45:44 -08:00
2018-04-05 16:07:51 -07:00
callback(null);
});
}
function updateMailboxOwner(name, domain, userId, auditSource, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
2018-04-05 16:07:51 -07:00
name = name.toLowerCase();
getMailbox(name, domain, function (error, result) {
2019-10-24 13:34:14 -07:00
if (error) return callback(error);
mailboxdb.updateMailboxOwner(name, domain, userId, function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, oldUserId: result.userId, userId });
callback(null);
});
});
}
2018-11-09 18:45:44 -08:00
function removeMailbox(name, domain, auditSource, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof name, 'string');
2018-11-09 18:45:44 -08:00
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
mailboxdb.del(name, domain, function (error) {
2019-10-24 13:34:14 -07:00
if (error) return callback(error);
2018-11-09 18:49:55 -08:00
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_REMOVE, auditSource, { name, domain });
2018-11-09 18:45:44 -08:00
callback(null);
});
}
function getAliases(name, domain, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
getMailbox(name, domain, function (error) {
if (error) return callback(error);
mailboxdb.getAliasesForName(name, domain, function (error, aliases) {
2019-10-24 13:34:14 -07:00
if (error) return callback(error);
callback(null, aliases);
});
});
}
function setAliases(name, domain, aliases, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(aliases));
assert.strictEqual(typeof callback, 'function');
for (var i = 0; i < aliases.length; i++) {
let name = aliases[i].name.toLowerCase();
let domain = aliases[i].domain.toLowerCase();
let error = validateName(name);
if (error) return callback(error);
if (!validator.isEmail(`${name}@${domain}`)) return callback(new BoxError(BoxError.BAD_FIELD, `Invalid email: ${name}@${domain}`));
aliases[i] = { name, domain };
}
mailboxdb.setAliasesForName(name, domain, aliases, function (error) {
2019-10-24 13:34:14 -07:00
if (error) return callback(error);
callback(null);
});
}
2018-01-26 10:22:50 +01:00
function getLists(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
2019-08-23 15:09:06 -07:00
mailboxdb.getLists(domain, function (error, result) {
2019-10-24 13:34:14 -07:00
if (error) return callback(error);
2018-01-26 10:22:50 +01:00
callback(null, result);
});
}
2020-01-24 16:54:14 -08:00
function getList(name, domain, callback) {
assert.strictEqual(typeof name, 'string');
2018-01-26 10:22:50 +01:00
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
2020-01-24 16:54:14 -08:00
mailboxdb.getList(name, domain, function (error, result) {
2019-10-24 13:34:14 -07:00
if (error) return callback(error);
2018-01-26 10:22:50 +01:00
callback(null, result);
2018-01-26 10:22:50 +01:00
});
}
2020-04-17 16:55:23 -07:00
function addList(name, domain, members, membersOnly, auditSource, callback) {
2018-01-26 10:22:50 +01:00
assert.strictEqual(typeof domain, 'string');
2018-04-05 16:07:51 -07:00
assert.strictEqual(typeof name, 'string');
assert(Array.isArray(members));
2020-04-17 16:55:23 -07:00
assert.strictEqual(typeof membersOnly, 'boolean');
2018-11-09 18:49:55 -08:00
assert.strictEqual(typeof auditSource, 'object');
2018-01-26 10:22:50 +01:00
assert.strictEqual(typeof callback, 'function');
2018-04-05 16:07:51 -07:00
name = name.toLowerCase();
var error = validateName(name);
if (error) return callback(error);
for (var i = 0; i < members.length; i++) {
2019-10-24 13:34:14 -07:00
if (!validator.isEmail(members[i])) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid mail member: ' + members[i]));
2018-04-05 16:07:51 -07:00
}
2020-04-17 16:55:23 -07:00
mailboxdb.addList(name, domain, members, membersOnly, function (error) {
2019-10-24 13:34:14 -07:00
if (error) return callback(error);
2018-01-26 10:22:50 +01:00
2020-04-17 16:55:23 -07:00
eventlog.add(eventlog.ACTION_MAIL_LIST_ADD, auditSource, { name, domain, members, membersOnly });
2018-11-09 18:49:55 -08:00
callback();
2018-01-26 10:22:50 +01:00
});
}
2020-04-17 16:55:23 -07:00
function updateList(name, domain, members, membersOnly, auditSource, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(members));
2020-04-17 16:55:23 -07:00
assert.strictEqual(typeof membersOnly, 'boolean');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
2018-04-05 16:07:51 -07:00
name = name.toLowerCase();
var error = validateName(name);
if (error) return callback(error);
for (var i = 0; i < members.length; i++) {
2019-10-24 13:34:14 -07:00
if (!validator.isEmail(members[i])) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid email: ' + members[i]));
2018-04-05 16:07:51 -07:00
}
getList(name, domain, function (error, result) {
2019-10-24 13:34:14 -07:00
if (error) return callback(error);
2020-04-17 16:55:23 -07:00
mailboxdb.updateList(name, domain, members, membersOnly, function (error) {
if (error) return callback(error);
2020-04-17 16:55:23 -07:00
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, oldMembers: result.members, members, membersOnly });
callback(null);
});
});
}
2018-11-09 18:49:55 -08:00
function removeList(name, domain, auditSource, callback) {
assert.strictEqual(typeof name, 'string');
2018-01-26 10:22:50 +01:00
assert.strictEqual(typeof domain, 'string');
2018-11-09 18:49:55 -08:00
assert.strictEqual(typeof auditSource, 'object');
2018-01-26 10:22:50 +01:00
assert.strictEqual(typeof callback, 'function');
2018-11-09 18:49:55 -08:00
mailboxdb.del(name, domain, function (error) {
2019-10-24 13:34:14 -07:00
if (error) return callback(error);
2018-01-26 10:22:50 +01:00
eventlog.add(eventlog.ACTION_MAIL_LIST_REMOVE, auditSource, { name, domain });
2018-11-09 18:49:55 -08:00
callback();
2018-01-26 10:22:50 +01:00
});
}
2019-11-06 16:45:44 -08:00
// resolves the members of a list. i.e the lists and aliases
2019-11-06 16:45:44 -08:00
function resolveList(listName, listDomain, callback) {
assert.strictEqual(typeof listName, 'string');
assert.strictEqual(typeof listDomain, 'string');
assert.strictEqual(typeof callback, 'function');
getDomains(function (error, mailDomains) {
if (error) return callback(error);
const mailInDomains = mailDomains.filter(function (d) { return d.enabled; }).map(function (d) { return d.domain; }).join(',');
mailboxdb.getList(listName, listDomain, function (error, list) {
if (error) return callback(error);
let result = [], toResolve = list.members.slice(), visited = []; // slice creates a copy of array
async.whilst(() => toResolve.length != 0, function (iteratorCallback) {
const toProcess = toResolve.shift();
const parts = toProcess.split('@');
const memberName = parts[0].split('+')[0], memberDomain = parts[1];
if (!mailInDomains.includes(memberDomain)) { result.push(toProcess); return iteratorCallback(); } // external domain
const member =`${memberName}@${memberDomain}`; // cleaned up without any '+' subaddress
if (visited.includes(member)) {
debug(`resolveList: list ${listName}@${listDomain} has a recursion at member ${member}`);
return iteratorCallback();
}
visited.push(member);
mailboxdb.get(memberName, memberDomain, function (error, entry) {
if (error && error.reason == BoxError.NOT_FOUND) { result.push(member); return iteratorCallback(); } // let it bounce
2019-11-06 16:45:44 -08:00
if (error) return iteratorCallback(error);
if (entry.type === mailboxdb.TYPE_MAILBOX) { // concrete mailbox
result.push(member);
} else if (entry.type === mailboxdb.TYPE_ALIAS) { // resolve aliases
toResolve = toResolve.concat(`${entry.aliasName}@${entry.aliasTarget}`);
} else { // resolve list members
toResolve = toResolve.concat(entry.members);
}
2019-11-06 16:45:44 -08:00
iteratorCallback();
});
}, function (error) {
2020-04-17 16:55:23 -07:00
callback(error, result, list);
2019-11-06 16:45:44 -08:00
});
});
});
}