mail: move dkim keys into the database
This commit is contained in:
118
src/mail.js
118
src/mail.js
@@ -13,6 +13,7 @@ exports = module.exports = {
|
||||
getDomain,
|
||||
clearDomains,
|
||||
|
||||
generateDkimKey,
|
||||
onDomainAdded,
|
||||
onDomainRemoved,
|
||||
|
||||
@@ -64,7 +65,6 @@ exports = module.exports = {
|
||||
TYPE_ALIAS: 'alias',
|
||||
|
||||
_delByDomain: delByDomain,
|
||||
_readDkimPublicKeySync: readDkimPublicKeySync,
|
||||
_updateDomain: updateDomain
|
||||
};
|
||||
|
||||
@@ -72,6 +72,7 @@ const assert = require('assert'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
cloudron = require('./cloudron.js'),
|
||||
constants = require('./constants.js'),
|
||||
crypto = require('crypto'),
|
||||
database = require('./database.js'),
|
||||
debug = require('debug')('box:mail'),
|
||||
dns = require('./dns.js'),
|
||||
@@ -84,6 +85,7 @@ const assert = require('assert'),
|
||||
mysql = require('mysql'),
|
||||
net = require('net'),
|
||||
nodemailer = require('nodemailer'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
@@ -105,7 +107,7 @@ const DNS_OPTIONS = { timeout: 5000 };
|
||||
const REMOVE_MAILBOX_CMD = path.join(__dirname, 'scripts/rmmailbox.sh');
|
||||
|
||||
const MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'ownerType', 'aliasName', 'aliasDomain', 'creationTime', 'membersJson', 'membersOnly', 'domain', 'active', 'enablePop3' ].join(',');
|
||||
const MAILDB_FIELDS = [ 'domain', 'enabled', 'mailFromValidation', 'catchAllJson', 'relayJson', 'dkimSelector', 'bannerJson' ].join(',');
|
||||
const MAILDB_FIELDS = [ 'domain', 'enabled', 'mailFromValidation', 'catchAllJson', 'relayJson', 'dkimKeyJson', 'dkimSelector', 'bannerJson' ].join(',');
|
||||
|
||||
function postProcessMailbox(data) {
|
||||
data.members = safe.JSON.parse(data.membersJson) || [ ];
|
||||
@@ -143,6 +145,9 @@ function postProcessDomain(data) {
|
||||
data.banner = safe.JSON.parse(data.bannerJson) || { text: null, html: null };
|
||||
delete data.bannerJson;
|
||||
|
||||
data.dkimKey = safe.JSON.parse(data.dkimKeyJson) || null;
|
||||
delete data.dkimKeyJson;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -259,13 +264,9 @@ async function checkDkim(mailDomain) {
|
||||
errorMessage: ''
|
||||
};
|
||||
|
||||
const dkimKey = readDkimPublicKeySync(domain);
|
||||
if (!dkimKey) {
|
||||
dkim.errorMessage = `Failed to read dkim public key of ${domain}`;
|
||||
return dkim;
|
||||
}
|
||||
const publicKey = mailDomain.dkimKey.publicKey.split('\n').slice(1, -2).join(''); // remove header, footer and new lines
|
||||
|
||||
dkim.expected = 'v=DKIM1; t=s; p=' + dkimKey;
|
||||
dkim.expected = `v=DKIM1; t=s; p=${publicKey}`;
|
||||
|
||||
const [error, txtRecords] = await safe(dns.promises.resolve(dkim.domain, dkim.type, DNS_OPTIONS));
|
||||
if (error) {
|
||||
@@ -276,7 +277,7 @@ async function checkDkim(mailDomain) {
|
||||
if (txtRecords.length !== 0) {
|
||||
dkim.value = txtRecords[0].join('');
|
||||
const actual = txtToDict(dkim.value);
|
||||
dkim.status = actual.p === dkimKey;
|
||||
dkim.status = actual.p === publicKey;
|
||||
}
|
||||
|
||||
return dkim;
|
||||
@@ -620,13 +621,13 @@ async function createMailConfig(mailFqdn, mailDomain) {
|
||||
const mailInDomains = mailDomains.filter(function (d) { return d.enabled; }).map(function (d) { return d.domain; }).join(',');
|
||||
|
||||
// mail_domain is used for SRS
|
||||
if (!safe.fs.writeFileSync(path.join(paths.ADDON_CONFIG_DIR, 'mail/mail.ini'),
|
||||
if (!safe.fs.writeFileSync(`${paths.MAIL_CONFIG_DIR}/mail.ini`,
|
||||
`mail_in_domains=${mailInDomains}\nmail_out_domains=${mailOutDomains}\nmail_server_name=${mailFqdn}\nmail_domain=${mailDomain}\n\n`, 'utf8')) {
|
||||
throw 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')) {
|
||||
if (!safe.fs.writeFileSync(`${paths.MAIL_CONFIG_DIR}/smtp_forward.ini`, 'enable_outbound=false\ndomain_selector=mail_from\n', 'utf8')) {
|
||||
throw new BoxError(BoxError.FS_ERROR, `Could not create smtp forward file: ${safe.error.message}`);
|
||||
}
|
||||
|
||||
@@ -635,13 +636,21 @@ async function createMailConfig(mailFqdn, mailDomain) {
|
||||
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'),
|
||||
if (!safe.fs.appendFileSync(`${paths.MAIL_CONFIG_DIR}/mail.ini`,
|
||||
`[${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}`);
|
||||
}
|
||||
|
||||
if (!safe.fs.writeFileSync(`${paths.ADDON_CONFIG_DIR}/mail/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.ADDON_CONFIG_DIR}/mail/banner/${domain.domain}.html`, domain.banner.html || '')) throw new BoxError(BoxError.FS_ERROR, `Could not create html banner file: ${safe.error.message}`);
|
||||
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
|
||||
if (!safe.fs.chmodSync(`${paths.MAIL_CONFIG_DIR}/mail/${domain.domain}/private`, 0o644)) return new BoxError(BoxError.FS_ERROR, safe.error);
|
||||
|
||||
const relay = domain.relay;
|
||||
|
||||
@@ -654,7 +663,7 @@ async function createMailConfig(mailFqdn, mailDomain) {
|
||||
|
||||
if (!enableRelay) continue;
|
||||
|
||||
if (!safe.fs.appendFileSync(paths.ADDON_CONFIG_DIR + '/mail/smtp_forward.ini',
|
||||
if (!safe.fs.appendFileSync(paths.MAIL_CONFIG_DIR + '/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')) {
|
||||
throw new BoxError(BoxError.FS_ERROR, `Could not create mail var file: ${safe.error.message}`);
|
||||
}
|
||||
@@ -680,9 +689,9 @@ async function configureMail(mailFqdn, mailDomain, serviceConfig) {
|
||||
|
||||
const bundle = await reverseProxy.getCertificatePath(mailFqdn, mailDomain);
|
||||
|
||||
const dhparamsFilePath = path.join(paths.ADDON_CONFIG_DIR, 'mail/dhparams.pem');
|
||||
const mailCertFilePath = path.join(paths.ADDON_CONFIG_DIR, 'mail/tls_cert.pem');
|
||||
const mailKeyFilePath = path.join(paths.ADDON_CONFIG_DIR, 'mail/tls_key.pem');
|
||||
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`;
|
||||
|
||||
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 ${bundle.certFilePath} ${mailCertFilePath}`)) throw new BoxError(BoxError.FS_ERROR, 'Could not create cert file:' + safe.error.message);
|
||||
@@ -711,7 +720,7 @@ async function configureMail(mailFqdn, mailDomain, serviceConfig) {
|
||||
-e CLOUDRON_MAIL_TOKEN="${cloudronToken}" \
|
||||
-e CLOUDRON_RELAY_TOKEN="${relayToken}" \
|
||||
-v "${paths.MAIL_DATA_DIR}:/app/data" \
|
||||
-v "${paths.PLATFORM_DATA_DIR}/addons/mail:/etc/mail" \
|
||||
-v "${paths.MAIL_CONFIG_DIR}:/etc/mail" \
|
||||
${ports} \
|
||||
--label isCloudronManaged=true \
|
||||
${readOnly} -v /run -v /tmp ${tag} ${cmd}`;
|
||||
@@ -842,58 +851,23 @@ async function txtRecordsWithSpf(domain, mailFqdn) {
|
||||
return 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');
|
||||
|
||||
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);
|
||||
return new BoxError(BoxError.FS_ERROR, safe.error);
|
||||
}
|
||||
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`);
|
||||
|
||||
// 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
|
||||
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);
|
||||
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);
|
||||
|
||||
if (!safe.fs.writeFileSync(dkimSelectorFile, mailDomain.dkimSelector, 'utf8')) return new BoxError(BoxError.FS_ERROR, safe.error);
|
||||
const publicKey = safe.fs.readFileSync(publicKeyFilePath, 'utf8');
|
||||
if (!publicKey) throw new BoxError(BoxError.FS_ERROR, safe.error.message);
|
||||
safe.fs.unlinkSync(publicKeyFilePath);
|
||||
|
||||
// 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(dkimPrivateKeyFile, 0o644)) return new BoxError(BoxError.FS_ERROR, safe.error);
|
||||
const privateKey = safe.fs.readFileSync(privateKeyFilePath, 'utf8');
|
||||
if (!privateKey) throw new BoxError(BoxError.FS_ERROR, safe.error.message);
|
||||
safe.fs.unlinkSync(privateKeyFilePath);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
return { publicKey, privateKey };
|
||||
}
|
||||
|
||||
async function upsertDnsRecords(domain, mailFqdn) {
|
||||
@@ -905,18 +879,14 @@ async function upsertDnsRecords(domain, mailFqdn) {
|
||||
const mailDomain = await getDomain(domain);
|
||||
if (!mailDomain) throw new BoxError(BoxError.NOT_FOUND, 'mail domain not found');
|
||||
|
||||
let error = ensureDkimKeySync(mailDomain);
|
||||
if (error) throw error;
|
||||
|
||||
if (process.env.BOX_ENV === 'test') return;
|
||||
|
||||
const dkimKey = readDkimPublicKeySync(domain);
|
||||
if (!dkimKey) throw new BoxError(BoxError.FS_ERROR, 'Failed to read dkim public key');
|
||||
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
|
||||
const dkimRecord = { subdomain: `${mailDomain.dkimSelector}._domainkey`, domain: domain, type: 'TXT', values: [ `"v=DKIM1; t=s; p=${dkimKey}"` ] };
|
||||
const dkimRecord = { subdomain: `${mailDomain.dkimSelector}._domainkey`, domain: domain, type: 'TXT', values: [ `"v=DKIM1; t=s; p=${publicKey}"` ] };
|
||||
|
||||
let records = [];
|
||||
const records = [];
|
||||
records.push(dkimRecord);
|
||||
if (mailDomain.enabled) records.push({ subdomain: '', domain: domain, type: 'MX', values: [ '10 ' + mailFqdn + '.' ] });
|
||||
|
||||
@@ -997,7 +967,7 @@ async function onDomainAdded(domain) {
|
||||
|
||||
if (!settings.mailFqdn()) return; // mail domain is not set yet (when provisioning)
|
||||
|
||||
await upsertDnsRecords(domain, settings.mailFqdn()); // do this first to ensure DKIM keys
|
||||
await upsertDnsRecords(domain, settings.mailFqdn());
|
||||
await restartMailIfActivated();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user