Files
cloudron-box/src/mail.js

1463 lines
57 KiB
JavaScript
Raw Normal View History

2017-06-28 17:06:12 -05:00
'use strict';
exports = module.exports = {
2020-07-15 15:33:53 -07:00
getStatus,
checkConfiguration,
2017-06-28 17:06:12 -05:00
getLocation,
setLocation, // triggers the change task
changeLocation, // does the actual changing
2021-06-29 14:26:34 -07:00
listDomains,
2020-07-15 15:33:53 -07:00
getDomain,
clearDomains,
2021-10-11 19:51:29 -07:00
generateDkimKey,
2020-07-15 15:33:53 -07:00
onDomainAdded,
onDomainRemoved,
2020-07-15 15:33:53 -07:00
removePrivateFields,
2019-02-15 10:55:15 -08:00
2020-07-15 15:33:53 -07:00
setDnsRecords,
2018-03-08 20:08:01 -08:00
2020-07-15 15:33:53 -07:00
validateName,
2022-05-31 17:53:09 -07:00
validateDisplayName,
2020-07-15 15:33:53 -07:00
setMailFromValidation,
setCatchAllAddress,
setMailRelay,
setMailEnabled,
setBanner,
2021-09-26 22:48:14 -07:00
startMail,
2020-07-15 15:33:53 -07:00
restartMail,
handleCertChanged,
getMailAuth,
sendTestMail,
getMailboxCount,
listMailboxes,
2021-08-17 15:45:57 -07:00
listAllMailboxes,
2020-07-15 15:33:53 -07:00
getMailbox,
addMailbox,
updateMailbox,
2021-08-17 15:45:57 -07:00
delMailbox,
2020-07-15 15:33:53 -07:00
2021-08-17 15:45:57 -07:00
getAlias,
2020-07-15 15:33:53 -07:00
getAliases,
setAliases,
getLists,
getList,
addList,
updateList,
2021-08-17 15:45:57 -07:00
delList,
2020-07-15 15:33:53 -07:00
resolveList,
2018-01-26 10:22:50 +01:00
2020-11-12 23:25:33 -08:00
OWNERTYPE_USER: 'user',
OWNERTYPE_GROUP: 'group',
OWNERTYPE_APP: 'app',
2020-11-12 23:25:33 -08:00
DEFAULT_MEMORY_LIMIT: 512 * 1024 * 1024,
2021-08-17 15:45:57 -07:00
TYPE_MAILBOX: 'mailbox',
TYPE_LIST: 'list',
TYPE_ALIAS: 'alias',
_delByDomain: delByDomain,
2021-07-07 12:59:17 -07:00
_updateDomain: updateDomain
2017-06-28 17:06:12 -05:00
};
const assert = require('assert'),
2019-10-24 13:34:14 -07:00
BoxError = require('./boxerror.js'),
cloudron = require('./cloudron.js'),
2019-02-15 10:55:15 -08:00
constants = require('./constants.js'),
2021-10-11 19:51:29 -07:00
crypto = require('crypto'),
2021-06-29 14:26:34 -07:00
database = require('./database.js'),
debug = require('debug')('box:mail'),
dig = require('./dig.js'),
2021-08-13 17:22:28 -07:00
dns = require('./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'),
mailer = require('./mailer.js'),
2021-08-17 15:45:57 -07:00
mysql = require('mysql'),
2017-06-28 17:06:12 -05:00
net = require('net'),
nodemailer = require('nodemailer'),
2021-10-11 19:51:29 -07:00
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
services = require('./services.js'),
settings = require('./settings.js'),
shell = require('./shell.js'),
2017-06-28 17:06:12 -05:00
smtpTransport = require('nodemailer-smtp-transport'),
2021-08-25 19:41:46 -07:00
superagent = require('superagent'),
2017-06-28 17:06:12 -05:00
sysinfo = require('./sysinfo.js'),
system = require('./system.js'),
tasks = require('./tasks.js'),
2018-04-29 10:58:45 -07:00
users = require('./users.js'),
2021-06-29 14:26:34 -07:00
util = require('util'),
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 };
2021-08-17 15:45:57 -07:00
const REMOVE_MAILBOX_CMD = path.join(__dirname, 'scripts/rmmailbox.sh');
const OWNERTYPES = [ exports.OWNERTYPE_USER, exports.OWNERTYPE_GROUP, exports.OWNERTYPE_APP ];
// if you add a field here, listMailboxes has to be updated
2022-08-17 23:18:38 +02:00
const MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'ownerType', 'aliasName', 'aliasDomain', 'creationTime', 'membersJson', 'membersOnly', 'domain', 'active', 'enablePop3', 'storageQuota', 'messagesQuota' ].join(',');
2021-10-11 19:51:29 -07:00
const MAILDB_FIELDS = [ 'domain', 'enabled', 'mailFromValidation', 'catchAllJson', 'relayJson', 'dkimKeyJson', 'dkimSelector', 'bannerJson' ].join(',');
2021-06-29 14:26:34 -07:00
2021-08-17 15:45:57 -07:00
function postProcessMailbox(data) {
data.members = safe.JSON.parse(data.membersJson) || [ ];
delete data.membersJson;
data.membersOnly = !!data.membersOnly;
data.active = !!data.active;
data.enablePop3 = !!data.enablePop3;
2021-08-17 15:45:57 -07:00
return data;
}
function postProcessAliases(data) {
const aliasNames = JSON.parse(data.aliasNames), aliasDomains = JSON.parse(data.aliasDomains);
delete data.aliasNames;
delete data.aliasDomains;
data.aliases = [];
for (let i = 0; i < aliasNames.length; i++) { // NOTE: aliasNames is [ null ] when no aliases
if (aliasNames[i]) data.aliases[i] = { name: aliasNames[i], domain: aliasDomains[i] };
}
return data;
}
function postProcessDomain(data) {
2021-06-29 14:26:34 -07:00
data.enabled = !!data.enabled; // int to boolean
data.mailFromValidation = !!data.mailFromValidation; // int to boolean
data.catchAll = safe.JSON.parse(data.catchAllJson) || [ ];
delete data.catchAllJson;
data.relay = safe.JSON.parse(data.relayJson) || { provider: 'cloudron-smtp' };
delete data.relayJson;
data.banner = safe.JSON.parse(data.bannerJson) || { text: null, html: null };
delete data.bannerJson;
2021-10-11 19:51:29 -07:00
data.dkimKey = safe.JSON.parse(data.dkimKeyJson) || null;
delete data.dkimKeyJson;
2021-06-29 14:26:34 -07:00
return data;
}
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). keep hyphen at the end so it doesn't become a range.
if (/[^a-zA-Z0-9._-]/.test(name)) return new BoxError(BoxError.BAD_FIELD, 'mailbox name can only contain alphanumerals, dot, hyphen or underscore');
return null;
}
2022-05-31 17:53:09 -07:00
function validateDisplayName(name) {
assert.strictEqual(typeof name, 'string');
if (name.length < 1) return new BoxError(BoxError.BAD_FIELD, 'mailbox display name must be atleast 1 char');
if (name.length >= 100) return new BoxError(BoxError.BAD_FIELD, 'mailbox display name too long');
if (/["<>@]/.test(name)) return new BoxError(BoxError.BAD_FIELD, 'mailbox display name is not valid');
2022-05-31 17:53:09 -07:00
return null;
}
async function checkOutboundPort25() {
const relay = {
value: 'OK',
status: false,
errorMessage: ''
};
return await new Promise((resolve) => {
const client = new net.Socket();
client.setTimeout(5000);
client.connect(25, constants.PORT25_CHECK_SERVER);
client.on('connect', function () {
relay.status = true;
relay.value = 'OK';
client.destroy(); // do not use end() because it still triggers timeout
resolve(relay);
});
client.on('timeout', function () {
relay.status = false;
relay.value = `Connect to ${constants.PORT25_CHECK_SERVER} timed out. Check if port 25 (outbound) is blocked`;
client.destroy();
relay.errorMessage = `Connect to ${constants.PORT25_CHECK_SERVER} timed out.`;
resolve(relay);
});
client.on('error', function (error) {
relay.status = false;
relay.value = `Connect to ${constants.PORT25_CHECK_SERVER} failed: ${error.message}. Check if port 25 (outbound) is blocked`;
client.destroy();
2022-03-03 09:58:58 -08:00
relay.errorMessage = `Connect to ${constants.PORT25_CHECK_SERVER} failed: ${error.message}`;
resolve(relay);
});
});
}
async function checkSmtpRelay(relay) {
const result = {
value: 'OK',
status: false,
errorMessage: ''
};
2017-06-28 17:06:12 -05:00
const 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 };
const transporter = nodemailer.createTransport(smtpTransport(options));
2017-06-28 17:06:12 -05:00
const [error] = await safe(util.promisify(transporter.verify)());
result.status = !error;
if (error) {
result.value = result.errorMessage = error.message;
return result;
}
2017-06-28 17:06:12 -05:00
return result;
2017-06-28 17:06:12 -05:00
}
2021-06-29 14:26:34 -07:00
async function verifyRelay(relay) {
assert.strictEqual(typeof relay, 'object');
2017-06-28 17:06:12 -05:00
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 null;
2017-06-28 17:06:12 -05:00
const result = await checkSmtpRelay(relay);
if (result.errorMessage) return new BoxError(BoxError.BAD_FIELD, result.errorMessage);
}
2017-06-28 17:06:12 -05:00
async function checkDkim(mailDomain) {
assert.strictEqual(typeof mailDomain, 'object');
const domain = mailDomain.domain;
const dkim = {
domain: `${mailDomain.dkimSelector}._domainkey.${domain}`,
name: `${mailDomain.dkimSelector}._domainkey`,
type: 'TXT',
expected: null,
value: null,
status: false,
errorMessage: ''
};
2017-06-28 17:06:12 -05:00
2021-10-11 19:51:29 -07:00
const publicKey = mailDomain.dkimKey.publicKey.split('\n').slice(1, -2).join(''); // remove header, footer and new lines
2017-06-28 17:06:12 -05:00
2021-10-11 19:51:29 -07:00
dkim.expected = `v=DKIM1; t=s; p=${publicKey}`;
2017-06-28 17:06:12 -05:00
const [error, txtRecords] = await safe(dig.resolve(dkim.domain, dkim.type, DNS_OPTIONS));
if (error) {
dkim.errorMessage = error.message;
return dkim;
}
2017-06-28 17:06:12 -05:00
if (txtRecords.length !== 0) {
dkim.value = txtRecords[0].join('');
const actual = txtToDict(dkim.value);
2021-10-11 19:51:29 -07:00
dkim.status = actual.p === publicKey;
}
2017-06-28 17:06:12 -05:00
return dkim;
}
2017-06-28 17:06:12 -05:00
async function checkSpf(domain, mailFqdn) {
2019-01-31 15:08:14 -08:00
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof mailFqdn, 'string');
const 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,
errorMessage: ''
};
const [error, txtRecords] = await safe(dig.resolve(spf.domain, spf.type, DNS_OPTIONS));
if (error) {
spf.errorMessage = error.message;
return spf;
}
let 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.mailFqdn()) !== -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.mailFqdn() + ' ' + spf.value.slice('v=spf1 '.length);
}
2017-06-28 17:06:12 -05:00
return spf;
}
2017-06-28 17:06:12 -05:00
async function checkMx(domain, mailFqdn) {
2019-01-31 15:08:14 -08:00
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof mailFqdn, 'string');
const 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,
errorMessage: ''
};
const [error, mxRecords] = await safe(dig.resolve(mx.domain, mx.type, DNS_OPTIONS));
if (error) {
mx.errorMessage = error.message;
return mx;
}
if (mxRecords.length === 0) return mx;
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 mx; // MX record is "my."
// cloudflare might create a conflict subdomain (https://support.cloudflare.com/hc/en-us/articles/360020296512-DNS-Troubleshooting-FAQ)
const [error2, mxIps] = await safe(dig.resolve(mxRecords[0].exchange, 'A', DNS_OPTIONS));
if (error2 || mxIps.length !== 1) return mx;
const [error3, ip] = await safe(sysinfo.getServerIPv4());
if (error3) return mx;
mx.status = mxIps[0] === ip;
2017-06-28 17:06:12 -05:00
return mx;
}
2017-06-28 17:06:12 -05:00
function txtToDict(txt) {
2022-04-14 17:41:41 -05:00
const dict = {};
txt.split(';').forEach(function(v) {
2022-04-14 17:41:41 -05:00
const p = v.trim().split('=');
dict[p[0]]=p[1];
});
return dict;
}
async function checkDmarc(domain) {
const 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,
errorMessage: ''
};
const [error, txtRecords] = await safe(dig.resolve(dmarc.domain, dmarc.type, DNS_OPTIONS));
if (error) {
dmarc.errorMessage = error.message;
return dmarc;
}
2017-06-28 17:06:12 -05:00
if (txtRecords.length !== 0) {
dmarc.value = txtRecords[0].join('');
const actual = txtToDict(dmarc.value);
dmarc.status = actual.v === 'DMARC1'; // see box#666
}
return dmarc;
}
2017-06-28 17:06:12 -05:00
async function checkPtr(mailFqdn) {
2019-01-31 15:08:14 -08:00
assert.strictEqual(typeof mailFqdn, 'string');
const 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,
errorMessage: ''
};
2017-06-28 17:06:12 -05:00
const [error, ip] = await safe(sysinfo.getServerIPv4());
if (error) {
ptr.errorMessage = error.message;
return 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
const [error2, ptrRecords] = await safe(dig.resolve(ptr.domain, 'PTR', DNS_OPTIONS));
if (error2) {
ptr.errorMessage = error2.message;
return 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 ptr;
}
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
async function checkRblStatus(domain) {
const ip = await sysinfo.getServerIPv4();
2017-09-08 11:50:11 -07:00
const flippedIp = ip.split('.').reverse().join('.');
2017-09-08 11:50:11 -07:00
// https://tools.ietf.org/html/rfc5782
const blacklistedServers = [];
for (const rblServer of RBL_LIST) {
const [error, records] = await safe(dig.resolve(flippedIp + '.' + rblServer.dns, 'A', DNS_OPTIONS));
if (error || !records) continue; // not listed
2017-09-08 11:50:11 -07:00
debug(`checkRblStatus: ${domain} (ip: ${flippedIp}) is in the blacklist of ${JSON.stringify(rblServer)}`);
2017-09-08 11:50:11 -07:00
const result = _.extend({ }, rblServer);
2017-09-08 11:50:11 -07:00
const [error2, txtRecords] = await safe(dig.resolve(flippedIp + '.' + rblServer.dns, 'TXT', DNS_OPTIONS));
result.txtRecords = error2 || !txtRecords ? 'No txt record' : txtRecords.map(x => x.join(''));
2017-09-08 11:50:11 -07:00
debug(`checkRblStatus: ${domain} (error: ${error2.message}) (txtRecords: ${JSON.stringify(txtRecords)})`);
2017-09-08 11:50:11 -07:00
blacklistedServers.push(result);
}
2017-09-08 11:50:11 -07:00
2022-03-03 10:08:34 -08:00
debug(`checkRblStatus: ${domain} (ip: ${ip}) blacklistedServers: ${JSON.stringify(blacklistedServers)})`);
2017-09-13 22:39:42 -07:00
return { status: blacklistedServers.length === 0, ip, servers: blacklistedServers };
2017-09-13 22:39:42 -07:00
}
async function getStatus(domain) {
2018-01-21 00:40:30 -08:00
assert.strictEqual(typeof domain, 'string');
2017-09-13 22:39:42 -07:00
// ensure we always have a valid toplevel properties for the api
const 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
const mailFqdn = settings.mailFqdn();
2017-09-13 22:39:42 -07:00
const mailDomain = await getDomain(domain);
if (!mailDomain) throw new BoxError(BoxError.NOT_FOUND, 'mail domain not found');
2017-09-13 22:39:42 -07:00
const checks = [];
if (mailDomain.enabled) {
checks.push(
{ what: 'dns.mx', promise: checkMx(domain, mailFqdn) },
{ what: 'dns.dmarc', promise: checkDmarc(domain) }
);
2017-09-13 22:39:42 -07:00
}
2017-09-08 11:50:11 -07:00
if (mailDomain.relay.provider === 'cloudron-smtp') {
// these tests currently only make sense when using Cloudron's SMTP server at this point
checks.push(
{ what: 'dns.spf', promise: checkSpf(domain, mailFqdn) },
{ what: 'dns.dkim', promise: checkDkim(mailDomain) },
{ what: 'dns.ptr', promise: checkPtr(mailFqdn) },
{ what: 'relay', promise: checkOutboundPort25() },
{ what: 'rbl', promise: checkRblStatus(domain) },
);
} else if (mailDomain.relay.provider !== 'noop') {
checks.push({ what: 'relay', promise: checkSmtpRelay(mailDomain.relay) });
}
2017-09-13 22:39:42 -07:00
// wait for all the checks and record the result
const responses = await Promise.allSettled(checks.map(c => c.promise));
for (let i = 0; i < checks.length; i++) {
const response = responses[i], check = checks[i];
if (response.status !== 'fulfilled') {
debug(`check ${check.what} was rejected. This is not expected`);
continue;
2017-09-13 22:39:42 -07:00
}
if (response.value.errorMessage) debug(`Ignored error - ${check.what} : ${response.value.errorMessage}`);
safe.set(results, checks[i].what, response.value || {});
}
return results;
2017-09-08 11:50:11 -07:00
}
2021-08-17 15:45:57 -07:00
async function checkConfiguration() {
let messages = {};
2021-08-17 15:45:57 -07:00
const allDomains = await listDomains();
2021-08-17 15:45:57 -07:00
for (const domainObject of allDomains) {
2021-09-03 11:38:21 -07:00
const result = await getStatus(domainObject.domain);
2021-08-17 15:45:57 -07:00
let message = [];
2021-08-17 15:45:57 -07:00
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}\``);
});
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(', ')}`);
}
2021-08-17 15:45:57 -07:00
if (message.length) messages[domainObject.domain] = message;
}
2021-08-17 15:45:57 -07:00
// 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';
});
2021-08-17 15:45:57 -07:00
if (markdownMessage) markdownMessage += 'Email Status is checked every 30 minutes.\n See the [troubleshooting docs](https://docs.cloudron.io/troubleshooting/#mail-dns) for more information.\n';
2019-03-01 11:24:10 -08:00
2021-08-17 15:45:57 -07:00
return markdownMessage; // empty message means all status checks succeeded
}
async function createMailConfig(mailFqdn) {
2019-01-31 15:08:14 -08:00
assert.strictEqual(typeof mailFqdn, 'string');
debug(`createMailConfig: generating mail config with ${mailFqdn}`);
2021-08-17 15:45:57 -07:00
const mailDomains = await listDomains();
2021-08-17 15:45:57 -07:00
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
2021-08-17 15:45:57 -07:00
// mail_domain is used for SRS
2021-10-11 19:51:29 -07:00
if (!safe.fs.writeFileSync(`${paths.MAIL_CONFIG_DIR}/mail.ini`,
`mail_in_domains=${mailInDomains}\nmail_out_domains=${mailOutDomains}\nmail_server_name=${mailFqdn}\n\n`, 'utf8')) {
2021-08-17 15:45:57 -07:00
throw new BoxError(BoxError.FS_ERROR, `Could not create mail var file: ${safe.error.message}`);
}
2021-08-17 15:45:57 -07:00
// enable_outbound makes plugin forward email for relayed mail. non-relayed mail always hits LMTP plugin first
2021-10-11 19:51:29 -07:00
if (!safe.fs.writeFileSync(`${paths.MAIL_CONFIG_DIR}/smtp_forward.ini`, 'enable_outbound=false\ndomain_selector=mail_from\n', 'utf8')) {
2021-08-17 15:45:57 -07:00
throw new BoxError(BoxError.FS_ERROR, `Could not create smtp forward file: ${safe.error.message}`);
}
2021-08-17 15:45:57 -07:00
// create sections for per-domain configuration
for (const domain of mailDomains) {
const catchAll = domain.catchAll.map(function (c) { return `${c}@${domain.domain}`; }).join(',');
const mailFromValidation = domain.mailFromValidation;
2021-10-11 19:51:29 -07:00
if (!safe.fs.appendFileSync(`${paths.MAIL_CONFIG_DIR}/mail.ini`,
2021-08-17 15:45:57 -07:00
`[${domain.domain}]\ncatch_all=${catchAll}\nmail_from_validation=${mailFromValidation}\n\n`, 'utf8')) {
throw new BoxError(BoxError.FS_ERROR, `Could not create mail var file: ${safe.error.message}`);
}
2020-08-24 10:28:53 -07:00
2021-10-11 19:51:29 -07:00
if (!safe.fs.writeFileSync(`${paths.MAIL_CONFIG_DIR}/banner/${domain.domain}.text`, domain.banner.text || '')) throw new BoxError(BoxError.FS_ERROR, `Could not create text banner file: ${safe.error.message}`);
if (!safe.fs.writeFileSync(`${paths.MAIL_CONFIG_DIR}/banner/${domain.domain}.html`, domain.banner.html || '')) throw new BoxError(BoxError.FS_ERROR, `Could not create html banner file: ${safe.error.message}`);
safe.fs.mkdirSync(`${paths.MAIL_CONFIG_DIR}/dkim/${domain.domain}`, { recursive: true });
if (!safe.fs.writeFileSync(`${paths.MAIL_CONFIG_DIR}/dkim/${domain.domain}/public`, domain.dkimKey.publicKey)) throw new BoxError(BoxError.FS_ERROR, `Could not create public key file: ${safe.error.message}`);
if (!safe.fs.writeFileSync(`${paths.MAIL_CONFIG_DIR}/dkim/${domain.domain}/private`, domain.dkimKey.privateKey)) throw new BoxError(BoxError.FS_ERROR, `Could not create private key file: ${safe.error.message}`);
if (!safe.fs.writeFileSync(`${paths.MAIL_CONFIG_DIR}/dkim/${domain.domain}/selector`, domain.dkimSelector)) throw new BoxError(BoxError.FS_ERROR, `Could not create selector file: ${safe.error.message}`);
// if the 'yellowtent' user of OS and the 'cloudron' user of mail container don't match, the keys become inaccessible by mail code
2021-10-16 16:25:01 -07:00
if (!safe.fs.chmodSync(`${paths.MAIL_CONFIG_DIR}/dkim/${domain.domain}/private`, 0o644)) throw new BoxError(BoxError.FS_ERROR, safe.error);
2018-02-05 15:02:34 -08:00
2021-08-17 15:45:57 -07:00
const relay = domain.relay;
2021-08-17 15:45:57 -07: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 || '',
forceFromAddress = relay.forceFromAddress ? 'true' : 'false';
2021-08-17 15:45:57 -07:00
if (!enableRelay) continue;
const relayData = `[${domain.domain}]\nenable_outbound=true\nhost=${host}\nport=${port}\nenable_tls=true\nauth_type=${authType}\nauth_user=${username}\nauth_pass=${password}\nforce_from_address=${forceFromAddress}\n\n`;
if (!safe.fs.appendFileSync(paths.MAIL_CONFIG_DIR + '/smtp_forward.ini', relayData, 'utf8')) {
2021-08-17 15:45:57 -07:00
throw new BoxError(BoxError.FS_ERROR, `Could not create mail var file: ${safe.error.message}`);
}
}
2021-08-17 15:45:57 -07:00
return mailInDomains.length !== 0 /* allowInbound */;
}
2021-08-17 15:45:57 -07:00
async function configureMail(mailFqdn, mailDomain, serviceConfig) {
2019-02-04 17:10:07 -08:00
assert.strictEqual(typeof mailFqdn, 'string');
assert.strictEqual(typeof mailDomain, 'string');
assert.strictEqual(typeof serviceConfig, 'object');
2019-02-04 17:10:07 -08:00
// mail (note: 2587 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 = serviceConfig.memoryLimit || exports.DEFAULT_MEMORY_LIMIT;
const memory = system.getMemoryAllocation(memoryLimit);
2018-12-28 13:32:37 -08:00
const cloudronToken = hat(8 * 128), relayToken = hat(8 * 128);
const certificatePath = await reverseProxy.getCertificatePath(mailFqdn, mailDomain);
2021-08-17 15:45:57 -07:00
2021-10-11 19:51:29 -07:00
const dhparamsFilePath = `${paths.MAIL_CONFIG_DIR}/dhparams.pem`;
const mailCertFilePath = `${paths.MAIL_CONFIG_DIR}/tls_cert.pem`;
const mailKeyFilePath = `${paths.MAIL_CONFIG_DIR}/tls_key.pem`;
2021-08-17 15:45:57 -07:00
if (!safe.child_process.execSync(`cp ${paths.DHPARAMS_FILE} ${dhparamsFilePath}`)) throw new BoxError(BoxError.FS_ERROR, 'Could not copy dhparams:' + safe.error.message);
if (!safe.child_process.execSync(`cp ${certificatePath.certFilePath} ${mailCertFilePath}`)) throw new BoxError(BoxError.FS_ERROR, 'Could not create cert file:' + safe.error.message);
if (!safe.child_process.execSync(`cp ${certificatePath.keyFilePath} ${mailKeyFilePath}`)) throw new BoxError(BoxError.FS_ERROR, 'Could not create key file:' + safe.error.message);
2021-08-17 15:45:57 -07:00
// if the 'yellowtent' user of OS and the 'cloudron' user of mail container don't match, the keys become inaccessible by mail code
if (!safe.fs.chmodSync(mailKeyFilePath, 0o644)) throw new BoxError(BoxError.FS_ERROR, `Could not chmod key file: ${safe.error.message}`);
2021-08-17 15:45:57 -07:00
await shell.promises.exec('stopMail', 'docker stop mail || true');
await shell.promises.exec('removeMail', 'docker rm -f mail || true');
const allowInbound = await createMailConfig(mailFqdn);
2021-08-17 15:45:57 -07:00
2021-10-07 21:53:43 -07:00
const ports = allowInbound ? '-p 587:2587 -p 993:9993 -p 4190:4190 -p 25:2587 -p 465:2465 -p 995:9995' : '';
2021-10-01 12:09:13 -07:00
const readOnly = !serviceConfig.recoveryMode ? '--read-only' : '';
const cmd = serviceConfig.recoveryMode ? '/bin/bash -c \'echo "Debug mode. Sleeping" && sleep infinity\'' : '';
2021-10-16 16:07:29 -07:00
const logLevel = serviceConfig.recoveryMode ? 'data' : 'info';
2021-08-17 15:45:57 -07:00
2021-10-01 12:09:13 -07:00
const runCmd = `docker run --restart=always -d --name="mail" \
2021-08-17 15:45:57 -07:00
--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 ${memory} \
--memory-swap ${memoryLimit} \
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_MAIL_TOKEN="${cloudronToken}" \
-e CLOUDRON_RELAY_TOKEN="${relayToken}" \
2021-10-16 16:07:29 -07:00
-e LOGLEVEL=${logLevel} \
2021-08-17 15:45:57 -07:00
-v "${paths.MAIL_DATA_DIR}:/app/data" \
-v "${paths.MAIL_CONFIG_DIR}:/etc/mail:ro" \
2021-08-17 15:45:57 -07:00
${ports} \
--label isCloudronManaged=true \
2021-10-01 12:09:13 -07:00
${readOnly} -v /run -v /tmp ${tag} ${cmd}`;
2021-08-17 15:45:57 -07:00
2021-10-01 12:09:13 -07:00
await shell.promises.exec('startMail', runCmd);
}
async function getMailAuth() {
2021-08-25 19:41:46 -07:00
const data = await docker.inspect('mail');
const ip = safe.query(data, 'NetworkSettings.Networks.cloudron.IPAddress');
if (!ip) throw 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) throw new BoxError(BoxError.MAIL_ERROR, 'Error getting mail env');
const tmp = env.find(function (e) { return e.indexOf('CLOUDRON_RELAY_TOKEN') === 0; });
if (!tmp) throw 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) throw new BoxError(BoxError.MAIL_ERROR, 'Error parsing CLOUDRON_RELAY_TOKEN');
return {
ip,
port: constants.INTERNAL_SMTP_PORT,
relayToken
};
2019-11-05 19:54:53 -08:00
}
2021-08-25 19:41:46 -07:00
async function restartMail() {
if (process.env.BOX_ENV === 'test' && !process.env.TEST_CREATE_INFRA) return;
2019-02-04 17:10:07 -08:00
2021-08-25 19:41:46 -07:00
const servicesConfig = await settings.getServicesConfig();
const mailConfig = servicesConfig['mail'] || {};
debug(`restartMail: restarting mail container with mailFqdn:${settings.mailFqdn()} mailDomain:${settings.mailDomain()}`);
await configureMail(settings.mailFqdn(), settings.mailDomain(), mailConfig);
2019-02-04 17:10:07 -08:00
}
2021-09-26 22:48:14 -07:00
async function startMail(existingInfra) {
assert.strictEqual(typeof existingInfra, 'object');
await restartMail();
}
2021-07-15 09:50:11 -07:00
async function restartMailIfActivated() {
const activated = await users.isActivated();
2019-10-24 13:34:14 -07:00
2021-07-15 09:50:11 -07:00
if (!activated) {
debug('restartMailIfActivated: skipping restart of mail container since Cloudron is not activated yet');
return; // not provisioned yet, do not restart container after dns setup
}
2021-08-25 19:41:46 -07:00
await restartMail();
}
2021-07-15 09:50:11 -07:00
async function handleCertChanged() {
2019-03-04 18:11:07 -08:00
debug('handleCertChanged: will restart if activated');
2021-07-15 09:50:11 -07:00
await restartMailIfActivated();
}
2021-06-29 14:26:34 -07:00
async function getDomain(domain) {
assert.strictEqual(typeof domain, 'string');
2021-06-29 14:26:34 -07:00
const result = await database.query(`SELECT ${MAILDB_FIELDS} FROM mail WHERE domain = ?`, [ domain ]);
if (result.length === 0) return null;
2021-08-17 15:45:57 -07:00
return postProcessDomain(result[0]);
}
2021-06-29 14:26:34 -07:00
async function updateDomain(domain, data) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof data, 'object');
2018-01-24 11:33:09 -08:00
2021-06-29 14:26:34 -07:00
const args = [ ];
const fields = [ ];
for (const k in data) {
2021-06-29 14:26:34 -07:00
if (k === 'catchAll' || k === 'banner') {
fields.push(`${k}Json = ?`);
args.push(JSON.stringify(data[k]));
} else if (k === 'relay') {
fields.push('relayJson = ?');
args.push(JSON.stringify(data[k]));
} else {
fields.push(k + ' = ?');
args.push(data[k]);
}
}
args.push(domain);
2018-01-24 11:33:09 -08:00
2021-06-29 14:26:34 -07:00
const result = await database.query('UPDATE mail SET ' + fields.join(', ') + ' WHERE domain=?', args);
if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Mail domain not found');
}
async function listDomains() {
const results = await database.query(`SELECT ${MAILDB_FIELDS} FROM mail ORDER BY domain`);
2021-08-17 15:45:57 -07:00
results.forEach(function (result) { postProcessDomain(result); });
2021-06-29 14:26:34 -07:00
return results;
2018-01-24 11:33:09 -08:00
}
// https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
async function txtRecordsWithSpf(domain, mailFqdn) {
assert.strictEqual(typeof domain, 'string');
2019-01-31 15:08:14 -08:00
assert.strictEqual(typeof mailFqdn, 'string');
const txtRecords = await dns.getDnsRecords('', domain, 'TXT');
debug('txtRecordsWithSpf: current txt records - %j', txtRecords);
let 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
validSpf = txtRecords[i].indexOf('a:' + mailFqdn) !== -1;
break; // there can only be one SPF record
}
if (validSpf) return null;
if (!matches) { // no spf record was found, create one
txtRecords.push('"v=spf1 a:' + mailFqdn + ' ~all"');
debug('txtRecordsWithSpf: adding txt record');
} else { // just add ourself
txtRecords[i] = matches[1] + ' a:' + mailFqdn + txtRecords[i].slice(matches[1].length);
debug('txtRecordsWithSpf: inserting txt record');
}
return txtRecords;
}
2021-10-11 19:51:29 -07:00
async function generateDkimKey() {
const publicKeyFilePath = path.join(os.tmpdir(), `dkim-${crypto.randomBytes(4).readUInt32LE(0)}.public`);
const privateKeyFilePath = path.join(os.tmpdir(), `dkim-${crypto.randomBytes(4).readUInt32LE(0)}.private`);
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
2021-10-11 19:51:29 -07:00
if (!safe.child_process.execSync(`openssl genrsa -out ${privateKeyFilePath} 1024`)) return new BoxError(BoxError.OPENSSL_ERROR, safe.error);
if (!safe.child_process.execSync(`openssl rsa -in ${privateKeyFilePath} -out ${publicKeyFilePath} -pubout -outform PEM`)) return new BoxError(BoxError.OPENSSL_ERROR, safe.error);
2021-10-11 19:51:29 -07:00
const publicKey = safe.fs.readFileSync(publicKeyFilePath, 'utf8');
if (!publicKey) throw new BoxError(BoxError.FS_ERROR, safe.error.message);
safe.fs.unlinkSync(publicKeyFilePath);
2021-10-11 19:51:29 -07:00
const privateKey = safe.fs.readFileSync(privateKeyFilePath, 'utf8');
if (!privateKey) throw new BoxError(BoxError.FS_ERROR, safe.error.message);
safe.fs.unlinkSync(privateKeyFilePath);
2021-10-11 19:51:29 -07:00
return { publicKey, privateKey };
2018-01-25 14:51:07 -08:00
}
async function upsertDnsRecords(domain, mailFqdn) {
assert.strictEqual(typeof domain, 'string');
2019-01-31 15:08:14 -08:00
assert.strictEqual(typeof mailFqdn, 'string');
2019-04-08 12:23:11 -07:00
debug(`upsertDnsRecords: updating mail dns records of domain ${domain} and mail fqdn ${mailFqdn}`);
const mailDomain = await getDomain(domain);
if (!mailDomain) throw new BoxError(BoxError.NOT_FOUND, 'mail domain not found');
if (process.env.BOX_ENV === 'test') return;
2021-10-11 19:51:29 -07:00
const publicKey = mailDomain.dkimKey.publicKey.split('\n').slice(1, -2).join(''); // remove header, footer and new lines
// t=s limits the domainkey to this domain and not it's subdomains
2021-10-11 19:51:29 -07:00
const dkimRecord = { subdomain: `${mailDomain.dkimSelector}._domainkey`, domain: domain, type: 'TXT', values: [ `"v=DKIM1; t=s; p=${publicKey}"` ] };
2021-10-11 19:51:29 -07:00
const records = [];
records.push(dkimRecord);
if (mailDomain.enabled) records.push({ subdomain: '', domain: domain, type: 'MX', values: [ '10 ' + mailFqdn + '.' ] });
const txtRecords = await txtRecordsWithSpf(domain, mailFqdn);
if (txtRecords) records.push({ subdomain: '', domain: domain, type: 'TXT', values: txtRecords });
const dmarcRecords = await dns.getDnsRecords('_dmarc', domain, 'TXT'); // only update dmarc if absent. this allows user to set email for reporting
if (dmarcRecords.length === 0) records.push({ subdomain: '_dmarc', domain: domain, type: 'TXT', values: [ '"v=DMARC1; p=reject; pct=100"' ] });
debug('upsertDnsRecords: will update %j', records);
for (const record of records) {
await dns.upsertDnsRecords(record.subdomain, record.domain, record.type, record.values);
}
debug('upsertDnsRecords: records %j added', records);
}
async function setDnsRecords(domain) {
2019-02-04 20:51:26 -08:00
assert.strictEqual(typeof domain, 'string');
await upsertDnsRecords(domain, settings.mailFqdn());
2019-02-04 20:51:26 -08:00
}
2021-09-03 11:38:21 -07:00
async function getLocation() {
const domain = settings.mailDomain(), fqdn = settings.mailFqdn();
const subdomain = fqdn.substr(0, fqdn.length - domain.length - 1);
2021-09-03 11:38:21 -07:00
return { domain, subdomain };
}
async function changeLocation(auditSource, progressCallback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof progressCallback, 'function');
const fqdn = settings.mailFqdn(), domain = settings.mailDomain();
const subdomain = fqdn.substr(0, fqdn.length - domain.length - 1);
let progress = 20;
progressCallback({ percent: progress, message: `Setting up DNS of certs of mail server ${fqdn}` });
await cloudron.setupDnsAndCert(subdomain, domain, auditSource, (progress) => progressCallback({ message: progress.message })); // remove the percent
const allDomains = await domains.list();
for (let idx = 0; idx < allDomains.length; idx++) {
const domainObject = allDomains[idx];
progressCallback({ percent: progress, message: `Updating DNS of ${domainObject.domain}` });
progress += Math.round(70/allDomains.length);
2021-09-03 11:38:21 -07:00
const [error] = await safe(upsertDnsRecords(domainObject.domain, fqdn)); // ignore any errors. we anyway report dns errors in status tab
progressCallback({ percent: progress, message: `Updated DNS of ${domainObject.domain}: ${error ? error.message : 'success'}` });
}
progressCallback({ percent: 90, message: 'Restarting mail server' });
await restartMailIfActivated();
}
2021-08-19 13:24:38 -07:00
async function setLocation(subdomain, domain, auditSource) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
2021-08-19 13:24:38 -07:00
const domainObject = await domains.get(domain);
const fqdn = dns.fqdn(subdomain, domainObject);
2021-08-19 13:24:38 -07:00
await settings.setMailLocation(domain, fqdn);
2021-08-19 13:24:38 -07:00
const taskId = await tasks.add(tasks.TASK_CHANGE_MAIL_LOCATION, [ auditSource ]);
2021-09-17 09:22:46 -07:00
tasks.startTask(taskId, {});
2021-08-19 13:24:38 -07:00
await eventlog.add(eventlog.ACTION_MAIL_LOCATION, auditSource, { subdomain, domain, taskId });
2021-08-19 13:24:38 -07:00
return taskId;
}
async function onDomainAdded(domain) {
assert.strictEqual(typeof domain, 'string');
if (!settings.mailFqdn()) return; // mail domain is not set yet (when provisioning)
2021-10-11 19:51:29 -07:00
await upsertDnsRecords(domain, settings.mailFqdn());
await restartMailIfActivated();
}
2021-08-25 19:41:46 -07:00
async function onDomainRemoved(domain) {
assert.strictEqual(typeof domain, 'string');
2021-08-25 19:41:46 -07:00
await restartMail();
}
2021-06-29 14:26:34 -07:00
async function clearDomains() {
await database.query('DELETE FROM mail', []);
}
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', 'banner');
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;
}
2021-06-29 14:26:34 -07:00
async function setMailFromValidation(domain, enabled) {
2018-01-20 23:17:39 -08:00
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof enabled, 'boolean');
2021-06-29 14:26:34 -07:00
await updateDomain(domain, { mailFromValidation: enabled });
safe(restartMail(), { debug }); // have to restart mail container since haraka cannot watch symlinked config files (mail.ini)
}
2021-06-29 14:26:34 -07:00
async function setBanner(domain, banner) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof banner, 'object');
2021-06-29 14:26:34 -07:00
await updateDomain(domain, { banner });
safe(restartMail(), { debug });
}
2021-06-29 14:26:34 -07:00
async function setCatchAllAddress(domain, addresses) {
2018-01-20 23:17:39 -08:00
assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(addresses));
2021-06-29 14:26:34 -07:00
await updateDomain(domain, { catchAll: addresses });
safe(restartMail(), { debug }); // have to restart mail container since haraka cannot watch symlinked config files (mail.ini)
}
2021-06-29 14:26:34 -07:00
async function setMailRelay(domain, relay, options) {
2018-01-20 23:17:39 -08:00
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof relay, 'object');
2021-06-29 14:26:34 -07:00
assert.strictEqual(typeof options, 'object');
2021-06-29 14:26:34 -07:00
const result = await getDomain(domain);
if (!domain) throw new BoxError(BoxError.NOT_FOUND, 'Mail domain not found');
2021-06-29 14:26:34 -07: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;
}
if (!options.skipVerify) {
const error = await verifyRelay(relay);
if (error) throw error;
}
2019-02-15 10:55:15 -08:00
await updateDomain(domain, { relay });
2019-02-15 10:55:15 -08:00
safe(restartMail(), { debug });
}
2021-06-29 14:26:34 -07:00
async function setMailEnabled(domain, enabled, auditSource) {
2018-01-20 23:17:39 -08:00
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof enabled, 'boolean');
assert.strictEqual(typeof auditSource, 'object');
2018-01-24 11:33:09 -08:00
2021-06-29 14:26:34 -07:00
await updateDomain(domain, { enabled: enabled });
2018-01-24 11:33:09 -08:00
safe(restartMail(), { debug });
2021-06-29 14:26:34 -07:00
await eventlog.add(enabled ? eventlog.ACTION_MAIL_ENABLED : eventlog.ACTION_MAIL_DISABLED, auditSource, { domain });
}
2021-08-17 15:45:57 -07:00
async function sendTestMail(domain, to) {
assert.strictEqual(typeof domain, 'string');
2018-02-03 18:27:55 -08:00
assert.strictEqual(typeof to, 'string');
2021-08-17 15:45:57 -07:00
const result = await getDomain(domain);
if (!result) throw new BoxError(BoxError.NOT_FOUND, 'mail domain not found');
2021-08-22 09:40:06 -07:00
await mailer.sendTestMail(result.domain, to);
}
2021-08-17 15:45:57 -07:00
async function listMailboxes(domain, search, page, perPage) {
assert.strictEqual(typeof domain, 'string');
assert(typeof search === 'string' || search === null);
2019-10-22 10:11:35 -07:00
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
2021-08-17 15:45:57 -07:00
const escapedSearch = mysql.escape('%' + search + '%'); // this also quotes the string
const searchQuery = search ? ` HAVING (name LIKE ${escapedSearch} OR aliasNames LIKE ${escapedSearch} OR aliasDomains LIKE ${escapedSearch})` : ''; // having instead of where because of aggregated columns use
2022-08-17 23:18:38 +02:00
const query = 'SELECT m1.name AS name, m1.domain AS domain, m1.ownerId AS ownerId, m1.ownerType as ownerType, m1.active as active, JSON_ARRAYAGG(m2.name) AS aliasNames, JSON_ARRAYAGG(m2.domain) AS aliasDomains, m1.enablePop3 AS enablePop3, m1.storageQuota AS storageQuota, m1.messagesQuota AS messagesQuota '
2021-08-17 15:45:57 -07:00
+ ` FROM (SELECT * FROM mailboxes WHERE type='${exports.TYPE_MAILBOX}') AS m1`
+ ` LEFT JOIN (SELECT * FROM mailboxes WHERE type='${exports.TYPE_ALIAS}') AS m2`
+ ' ON m1.name=m2.aliasName AND m1.domain=m2.aliasDomain AND m1.ownerId=m2.ownerId'
+ ' WHERE m1.domain = ?'
+ ' GROUP BY m1.name, m1.domain, m1.ownerId'
+ searchQuery
+ ' ORDER BY name LIMIT ?,?';
const results = await database.query(query, [ domain, (page-1)*perPage, perPage ]);
results.forEach(postProcessMailbox);
results.forEach(postProcessAliases);
return results;
}
2021-08-17 15:45:57 -07:00
async function listAllMailboxes(page, perPage) {
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
2020-07-15 15:33:53 -07:00
2022-08-17 23:18:38 +02:00
const query = 'SELECT m1.name AS name, m1.domain AS domain, m1.ownerId AS ownerId, m1.ownerType as ownerType, m1.active as active, JSON_ARRAYAGG(m2.name) AS aliasNames, JSON_ARRAYAGG(m2.domain) AS aliasDomains, m1.enablePop3 AS enablePop3, m1.storageQuota AS storageQuota, m1.messagesQuota AS messagesQuota '
2021-08-17 15:45:57 -07:00
+ ` FROM (SELECT * FROM mailboxes WHERE type='${exports.TYPE_MAILBOX}') AS m1`
+ ` LEFT JOIN (SELECT * FROM mailboxes WHERE type='${exports.TYPE_ALIAS}') AS m2`
+ ' ON m1.name=m2.aliasName AND m1.domain=m2.aliasDomain AND m1.ownerId=m2.ownerId'
+ ' GROUP BY m1.name, m1.domain, m1.ownerId'
+ ' ORDER BY name LIMIT ?,?';
2020-07-15 15:33:53 -07:00
2021-08-17 15:45:57 -07:00
const results = await database.query(query, [ (page-1)*perPage, perPage ]);
results.forEach(postProcessMailbox);
results.forEach(postProcessAliases);
return results;
2020-07-15 15:33:53 -07:00
}
2021-08-17 15:45:57 -07:00
async function getMailboxCount(domain) {
2018-02-11 01:18:29 -08:00
assert.strictEqual(typeof domain, 'string');
2021-08-17 15:45:57 -07:00
const results = await database.query('SELECT COUNT(*) AS total FROM mailboxes WHERE type = ? AND domain = ?', [ exports.TYPE_MAILBOX, domain ]);
2018-02-11 01:18:29 -08:00
2021-08-17 15:45:57 -07:00
return results[0].total;
}
async function delByDomain(domain) {
assert.strictEqual(typeof domain, 'string');
await database.query('DELETE FROM mailboxes WHERE domain = ?', [ domain ]);
2018-02-11 01:18:29 -08:00
}
2021-08-17 15:45:57 -07:00
async function get(name, domain) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
2021-08-17 15:45:57 -07:00
const results = await database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND domain = ?', [ name, domain ]);
if (results.length === 0) return null;
2021-08-17 15:45:57 -07:00
return postProcessMailbox(results[0]);
}
2021-08-17 15:45:57 -07:00
async function getMailbox(name, domain) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
const results = await database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND type = ? AND domain = ?', [ name, exports.TYPE_MAILBOX, domain ]);
if (results.length === 0) return null;
return postProcessMailbox(results[0]);
}
async function addMailbox(name, domain, data, auditSource) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof data, 'object');
2018-11-09 18:45:44 -08:00
assert.strictEqual(typeof auditSource, 'object');
2022-08-17 23:18:38 +02:00
const { ownerId, ownerType, active, storageQuota, messagesQuota } = data;
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof ownerType, 'string');
assert.strictEqual(typeof active, 'boolean');
2022-08-17 23:18:38 +02:00
assert(Number.isInteger(storageQuota) && storageQuota >= 0);
assert(Number.isInteger(messagesQuota) && messagesQuota >= 0);
2018-04-05 16:07:51 -07:00
name = name.toLowerCase();
2021-08-17 15:45:57 -07:00
let error = validateName(name);
if (error) throw error;
2020-11-12 23:25:33 -08:00
if (!OWNERTYPES.includes(ownerType)) throw new BoxError(BoxError.BAD_FIELD, 'bad owner type');
2018-04-05 16:07:51 -07:00
2022-08-17 23:18:38 +02:00
[error] = await safe(database.query('INSERT INTO mailboxes (name, type, domain, ownerId, ownerType, active, storageQuota, messagesQuota) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[ name, exports.TYPE_MAILBOX, domain, ownerId, ownerType, active, storageQuota, messagesQuota ]));
2021-08-17 15:45:57 -07:00
if (error && error.code === 'ER_DUP_ENTRY') throw new BoxError(BoxError.ALREADY_EXISTS, 'mailbox already exists');
if (error && error.code === 'ER_NO_REFERENCED_ROW_2' && error.sqlMessage.includes('mailboxes_domain_constraint')) throw new BoxError(BoxError.NOT_FOUND, `no such domain '${domain}'`);
2021-08-17 15:45:57 -07:00
if (error) throw error;
2018-11-09 18:45:44 -08:00
2022-08-17 23:18:38 +02:00
await eventlog.add(eventlog.ACTION_MAIL_MAILBOX_ADD, auditSource, { name, domain, ownerId, ownerType, active, storageQuota, messageQuota: messagesQuota });
}
2021-08-17 15:45:57 -07:00
async function updateMailbox(name, domain, data, auditSource) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof auditSource, 'object');
2022-08-18 11:56:01 +02:00
const args = [];
const fields = [];
for (const k in data) {
if (k === 'enablePop3' || k === 'active') {
fields.push(k + ' = ?');
args.push(data[k] ? 1 : 0);
continue;
}
2022-08-18 11:56:01 +02:00
if (k === 'ownerType' && !OWNERTYPES.includes(data[k])) throw new BoxError(BoxError.BAD_FIELD, 'bad owner type');
2018-04-05 16:07:51 -07:00
2022-08-18 11:56:01 +02:00
fields.push(k + ' = ?');
args.push(data[k]);
}
args.push(name.toLowerCase());
args.push(domain);
2020-11-12 23:25:33 -08:00
2021-08-17 15:45:57 -07:00
const mailbox = await getMailbox(name, domain);
if (!mailbox) throw new BoxError(BoxError.NOT_FOUND, 'No such mailbox');
2022-08-18 11:56:01 +02:00
const result = await safe(database.query('UPDATE mailboxes SET ' + fields.join(', ') + ' WHERE name = ? AND domain = ?', args));
2021-08-17 15:45:57 -07:00
if (result.affectedRows === 0) throw new BoxError(BoxError.NOT_FOUND, 'Mailbox not found');
2022-08-18 11:56:01 +02:00
await eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, Object.assign(data, { name, domain, oldUserId: mailbox.userId }) );
}
2021-08-25 19:41:46 -07:00
async function removeSolrIndex(mailbox) {
assert.strictEqual(typeof mailbox, 'string');
2021-08-25 19:41:46 -07:00
const addonDetails = await services.getContainerDetails('mail', 'CLOUDRON_MAIL_TOKEN');
2021-12-19 00:30:22 -08:00
const [error, response] = await safe(superagent.post(`http://${addonDetails.ip}:3000/solr_delete_index?access_token=${addonDetails.token}`)
2021-08-25 19:41:46 -07:00
.timeout(2000)
.send({ mailbox })
.ok(() => true));
2021-08-25 19:41:46 -07:00
if (error) throw new BoxError(BoxError.MAIL_ERROR, `Could not remove solr index: ${error.message}`);
2021-08-25 19:41:46 -07:00
if (response.status !== 200) throw new BoxError(BoxError.MAIL_ERROR, `Error removing solr index - ${response.status} ${JSON.stringify(response.body)}`);
}
2021-08-17 15:45:57 -07:00
async function delMailbox(name, domain, options, auditSource) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof options, 'object');
2018-11-09 18:45:44 -08:00
assert.strictEqual(typeof auditSource, 'object');
const mailbox =`${name}@${domain}`;
2021-08-17 15:45:57 -07:00
if (options.deleteMails) {
const [error] = await safe(shell.promises.sudo('removeMailbox', [ REMOVE_MAILBOX_CMD, mailbox ], {}));
if (error) throw new BoxError(BoxError.FS_ERROR, `Error removing mailbox: ${error.message}`);
}
2021-08-17 15:45:57 -07:00
// deletes aliases as well
const result = await database.query('DELETE FROM mailboxes WHERE ((name=? AND domain=?) OR (aliasName = ? AND aliasDomain=?))', [ name, domain, name, domain ]);
if (result.affectedRows === 0) throw new BoxError(BoxError.NOT_FOUND, 'Mailbox not found');
2021-08-25 19:41:46 -07:00
const [error] = await safe(removeSolrIndex(mailbox));
if (error) debug(`delMailbox: failed to remove solr index: ${error.message}`);
2022-02-24 20:04:46 -08:00
await eventlog.add(eventlog.ACTION_MAIL_MAILBOX_REMOVE, auditSource, { name, domain });
}
2021-08-17 15:45:57 -07:00
async function getAlias(name, domain) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
2021-08-17 15:45:57 -07:00
const results = await database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE name = ? AND type = ? AND domain = ?`, [ name, exports.TYPE_ALIAS, domain ]);
if (results.length === 0) return null;
2021-08-17 15:45:57 -07:00
results.forEach(function (result) { postProcessMailbox(result); });
2021-08-17 15:45:57 -07:00
return results[0];
}
async function getAliases(name, domain) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
const result = await getMailbox(name, domain); // check if mailbox exists
if (result === null) throw new BoxError(BoxError.NOT_FOUND, 'No such mailbox');
return await database.query('SELECT name, domain FROM mailboxes WHERE type = ? AND aliasName = ? AND aliasDomain = ? ORDER BY name', [ exports.TYPE_ALIAS, name, domain ]);
}
2022-02-24 20:30:13 -08:00
async function setAliases(name, domain, aliases, auditSource) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(aliases));
2022-02-24 20:30:13 -08:00
assert.strictEqual(typeof auditSource, 'object');
2021-08-17 15:45:57 -07:00
for (let i = 0; i < aliases.length; i++) {
const name = aliases[i].name.toLowerCase();
const domain = aliases[i].domain.toLowerCase();
const error = validateName(name);
2021-08-17 15:45:57 -07:00
if (error) throw error;
const mailDomain = await getDomain(domain);
if (!mailDomain) throw new BoxError(BoxError.NOT_FOUND, `mail domain ${domain} not found`);
aliases[i] = { name, domain };
}
2021-08-17 15:45:57 -07:00
const results = await database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND domain = ?', [ name, domain ]);
if (results.length === 0) throw new BoxError(BoxError.NOT_FOUND, 'Mailbox not found');
2022-02-24 20:30:13 -08:00
const queries = [];
2021-08-17 15:45:57 -07:00
// clear existing aliases
queries.push({ query: 'DELETE FROM mailboxes WHERE aliasName = ? AND aliasDomain = ? AND type = ?', args: [ name, domain, exports.TYPE_ALIAS ] });
2022-02-24 20:30:13 -08:00
for (const alias of aliases) {
2021-08-17 15:45:57 -07:00
queries.push({ query: 'INSERT INTO mailboxes (name, domain, type, aliasName, aliasDomain, ownerId, ownerType) VALUES (?, ?, ?, ?, ?, ?, ?)',
args: [ alias.name, alias.domain, exports.TYPE_ALIAS, name, domain, results[0].ownerId, results[0].ownerType ] });
2022-02-24 20:30:13 -08:00
}
2021-08-17 15:45:57 -07:00
const [error] = await safe(database.transaction(queries));
if (error && error.code === 'ER_DUP_ENTRY' && error.message.indexOf('mailboxes_name_domain_unique_index') !== -1) {
const aliasMatch = error.message.match(new RegExp(`^ER_DUP_ENTRY: Duplicate entry '(.*)-${domain}' for key 'mailboxes_name_domain_unique_index'$`));
if (!aliasMatch) throw new BoxError(BoxError.ALREADY_EXISTS, error.message);
throw new BoxError(BoxError.ALREADY_EXISTS, `Mailbox, mailinglist or alias for ${aliasMatch[1]} already exists`);
}
if (error) throw error;
2022-02-24 20:30:13 -08:00
await eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, aliases });
}
2018-01-26 10:22:50 +01:00
2021-08-17 15:45:57 -07:00
async function getLists(domain, search, page, perPage) {
2018-01-26 10:22:50 +01:00
assert.strictEqual(typeof domain, 'string');
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
2018-01-26 10:22:50 +01:00
2021-08-17 15:45:57 -07:00
let query = `SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? AND domain = ?`;
if (search) query += ' AND (name LIKE ' + mysql.escape('%' + search + '%') + ' OR membersJson LIKE ' + mysql.escape('%' + search + '%') + ')';
2018-01-26 10:22:50 +01:00
2021-08-17 15:45:57 -07:00
query += 'ORDER BY name LIMIT ?,?';
const results = await database.query(query, [ exports.TYPE_LIST, domain, (page-1)*perPage, perPage ]);
results.forEach(function (result) { postProcessMailbox(result); });
return results;
2018-01-26 10:22:50 +01:00
}
2021-08-17 15:45:57 -07:00
async function getList(name, domain) {
2020-01-24 16:54:14 -08:00
assert.strictEqual(typeof name, 'string');
2018-01-26 10:22:50 +01:00
assert.strictEqual(typeof domain, 'string');
2021-08-17 15:45:57 -07:00
const results = await database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE type = ? AND name = ? AND domain = ?', [ exports.TYPE_LIST, name, domain ]);
if (results.length === 0) return null;
2018-01-26 10:22:50 +01:00
2021-08-17 15:45:57 -07:00
return postProcessMailbox(results[0]);
2018-01-26 10:22:50 +01:00
}
2021-08-17 15:45:57 -07:00
async function addList(name, domain, data, auditSource) {
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.strictEqual(typeof data, 'object');
2018-11-09 18:49:55 -08:00
assert.strictEqual(typeof auditSource, 'object');
2018-01-26 10:22:50 +01:00
const { members, membersOnly, active } = data;
assert(Array.isArray(members));
assert.strictEqual(typeof membersOnly, 'boolean');
assert.strictEqual(typeof active, 'boolean');
2018-04-05 16:07:51 -07:00
name = name.toLowerCase();
2021-08-17 15:45:57 -07:00
let error = validateName(name);
if (error) throw error;
2018-04-05 16:07:51 -07:00
2021-08-17 15:45:57 -07:00
for (let i = 0; i < members.length; i++) {
if (!validator.isEmail(members[i])) throw new BoxError(BoxError.BAD_FIELD, 'Invalid mail member: ' + members[i]);
2018-04-05 16:07:51 -07:00
}
2021-08-17 15:45:57 -07:00
[error] = await safe(database.query('INSERT INTO mailboxes (name, type, domain, ownerId, ownerType, membersJson, membersOnly, active) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [ name, exports.TYPE_LIST, domain, 'admin', 'user', JSON.stringify(members), membersOnly, active ]));
if (error && error.code === 'ER_DUP_ENTRY') throw new BoxError(BoxError.ALREADY_EXISTS, 'mailbox already exists');
if (error) throw error;
2018-11-09 18:49:55 -08:00
2022-02-24 20:04:46 -08:00
await eventlog.add(eventlog.ACTION_MAIL_LIST_ADD, auditSource, { name, domain, members, membersOnly, active });
2018-01-26 10:22:50 +01:00
}
2021-08-17 15:45:57 -07:00
async function updateList(name, domain, data, auditSource) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof auditSource, 'object');
const { members, membersOnly, active } = data;
assert(Array.isArray(members));
assert.strictEqual(typeof membersOnly, 'boolean');
assert.strictEqual(typeof active, 'boolean');
2018-04-05 16:07:51 -07:00
name = name.toLowerCase();
2021-08-17 15:45:57 -07:00
let error = validateName(name);
if (error) throw error;
2018-04-05 16:07:51 -07:00
2021-08-17 15:45:57 -07:00
for (let i = 0; i < members.length; i++) {
if (!validator.isEmail(members[i])) throw new BoxError(BoxError.BAD_FIELD, 'Invalid email: ' + members[i]);
2018-04-05 16:07:51 -07:00
}
2021-08-17 15:45:57 -07:00
const result = await database.query('UPDATE mailboxes SET membersJson = ?, membersOnly = ?, active = ? WHERE name = ? AND domain = ?',
[ JSON.stringify(members), membersOnly, active, name, domain ]);
if (result.affectedRows === 0) throw new BoxError(BoxError.NOT_FOUND, 'Mailbox not found');
2022-02-24 20:04:46 -08:00
await eventlog.add(eventlog.ACTION_MAIL_LIST_UPDATE, auditSource, { name, domain, oldMembers: result.members, members, membersOnly, active });
}
2021-08-17 15:45:57 -07:00
async function delList(name, domain, auditSource) {
2018-11-09 18:49:55 -08:00
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
2021-08-17 15:45:57 -07:00
// deletes aliases as well
const result = await database.query('DELETE FROM mailboxes WHERE ((name=? AND domain=?) OR (aliasName = ? AND aliasDomain=?))', [ name, domain, name, domain ]);
if (result.affectedRows === 0) throw new BoxError(BoxError.NOT_FOUND, 'Mailbox not found');
2018-11-09 18:49:55 -08:00
2022-02-24 20:04:46 -08:00
await eventlog.add(eventlog.ACTION_MAIL_LIST_REMOVE, auditSource, { name, domain });
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
2021-08-17 15:45:57 -07:00
async function resolveList(listName, listDomain) {
2019-11-06 16:45:44 -08:00
assert.strictEqual(typeof listName, 'string');
assert.strictEqual(typeof listDomain, 'string');
2021-06-29 14:26:34 -07:00
2021-08-17 15:45:57 -07:00
const mailDomains = await listDomains();
const mailInDomains = mailDomains.filter(function (d) { return d.enabled; }).map(function (d) { return d.domain; }).join(',');
2019-11-06 16:45:44 -08:00
2021-08-17 15:45:57 -07:00
const list = await getList(listName, listDomain);
if (!list) throw new BoxError(BoxError.NOT_FOUND, 'List not found');
2019-11-06 16:45:44 -08:00
2021-08-17 15:45:57 -07:00
let resolvedMembers = [], toResolve = list.members.slice(), visited = []; // slice creates a copy of array
2019-11-06 16:45:44 -08:00
2021-08-17 15:45:57 -07:00
while (toResolve.length != 0) {
const toProcess = toResolve.shift();
const parts = toProcess.split('@');
const memberName = parts[0].split('+')[0], memberDomain = parts[1];
2019-11-06 16:45:44 -08:00
2021-08-17 15:45:57 -07:00
if (!mailInDomains.includes(memberDomain)) { // external domain
resolvedMembers.push(toProcess);
continue;
}
2019-11-06 16:45:44 -08:00
2021-08-17 15:45:57 -07:00
const member =`${memberName}@${memberDomain}`; // cleaned up without any '+' subaddress
if (visited.includes(member)) {
debug(`resolveList: list ${listName}@${listDomain} has a recursion at member ${member}`);
continue;
}
visited.push(member);
2019-11-06 16:45:44 -08:00
2021-08-17 15:45:57 -07:00
const entry = await get(memberName, memberDomain);
if (!entry) { // let it bounce
resolvedMembers.push(member);
continue;
}
2019-11-06 16:45:44 -08:00
2021-08-17 15:45:57 -07:00
if (entry.type === exports.TYPE_MAILBOX) { // concrete mailbox
resolvedMembers.push(member);
} else if (entry.type === exports.TYPE_ALIAS) { // resolve aliases
toResolve = toResolve.concat(`${entry.aliasName}@${entry.aliasDomain}`);
} else { // resolve list members
toResolve = toResolve.concat(entry.members);
}
}
2019-11-06 16:45:44 -08:00
2021-08-17 15:45:57 -07:00
return { resolvedMembers, list };
}