diff --git a/config/index.js b/config/index.js index 3d73a20292..9975baa290 100644 --- a/config/index.js +++ b/config/index.js @@ -39,6 +39,430 @@ const alternatives = require('./alternatives'); const zxcvbn = require('#helpers/zxcvbn'); +const brandAndCorporateDomains = [ + 'aaa', + 'aarp', + 'abarth', + 'abb', + 'abbott', + 'abbvie', + 'abc', + 'accenture', + 'aco', + 'aeg', + 'aetna', + 'afl', + 'agakhan', + 'aig', + 'aigo', + 'airbus', + 'airtel', + 'akdn', + 'alfaromeo', + 'alibaba', + 'alipay', + 'allfinanz', + 'allstate', + 'ally', + 'alstom', + 'amazon', + 'americanexpress', + 'amex', + 'amica', + 'android', + 'anz', + 'aol', + 'apple', + 'aquarelle', + 'aramco', + 'audi', + 'auspost', + 'aws', + 'axa', + 'azure', + 'baidu', + 'bananarepublic', + 'barclaycard', + 'barclays', + 'basketball', + 'bauhaus', + 'bbc', + 'bbt', + 'bbva', + 'bcg', + 'bentley', + 'bharti', + 'bing', + 'blanco', + 'bloomberg', + 'bms', + 'bmw', + 'bnl', + 'bnpparibas', + 'boehringer', + 'bond', + 'booking', + 'bosch', + 'bostik', + 'bradesco', + 'bridgestone', + 'brother', + 'bugatti', + 'cal', + 'calvinklein', + 'canon', + 'capitalone', + 'caravan', + 'cartier', + 'cba', + 'cbn', + 'cbre', + 'cbs', + 'cern', + 'cfa', + 'chanel', + 'chase', + 'chintai', + 'chrome', + 'chrysler', + 'cipriani', + 'cisco', + 'citadel', + 'citi', + 'citic', + 'clubmed', + 'comcast', + 'commbank', + 'creditunion', + 'crown', + 'crs', + 'csc', + 'cuisinella', + 'dabur', + 'datsun', + 'dealer', + 'dell', + 'deloitte', + 'delta', + 'dhl', + 'discover', + 'dish', + 'dnp', + 'dodge', + 'dunlop', + 'dupont', + 'dvag', + 'edeka', + 'emerck', + 'epson', + 'ericsson', + 'erni', + 'esurance', + 'etisalat', + 'eurovision', + 'everbank', + 'extraspace', + 'fage', + 'fairwinds', + 'farmers', + 'fedex', + 'ferrari', + 'ferrero', + 'fiat', + 'fidelity', + 'firestone', + 'firmdale', + 'flickr', + 'flir', + 'flsmidth', + 'ford', + 'fox', + 'fresenius', + 'forex', + 'frogans', + 'frontier', + 'fujitsu', + 'fujixerox', + 'gallo', + 'gallup', + 'gap', + 'gbiz', + 'gea', + 'genting', + 'giving', + 'gle', + 'globo', + 'gmail', + 'gmo', + 'gmx', + 'godaddy', + 'goldpoint', + 'goodyear', + 'goog', + 'google', + 'grainger', + 'guardian', + 'gucci', + 'hbo', + 'hdfc', + 'hdfcbank', + 'hermes', + 'hisamitsu', + 'hitachi', + 'hkt', + 'honda', + 'honeywell', + 'hotmail', + 'hsbc', + 'hughes', + 'hyatt', + 'hyundai', + 'ibm', + 'ieee', + 'ifm', + 'ikano', + 'imdb', + 'infiniti', + 'intel', + 'intuit', + 'ipiranga', + 'iselect', + 'itau', + 'itv', + 'iveco', + 'jaguar', + 'java', + 'jcb', + 'jcp', + 'jeep', + 'jpmorgan', + 'juniper', + 'kddi', + 'kerryhotels', + 'kerrylogistics', + 'kerryproperties', + 'kfh', + 'kia', + 'kinder', + 'kindle', + 'komatsu', + 'kpmg', + 'kred', + 'kuokgroup', + 'lacaixa', + 'ladbrokes', + 'lamborghini', + 'lancaster', + 'lancia', + 'lancome', + 'landrover', + 'lanxess', + 'lasalle', + 'latrobe', + 'lds', + 'leclerc', + 'lego', + 'liaison', + 'lexus', + 'lidl', + 'lifestyle', + 'lilly', + 'lincoln', + 'linde', + 'lipsy', + 'lixil', + 'locus', + 'lotte', + 'lpl', + 'lplfinancial', + 'lundbeck', + 'lupin', + 'macys', + 'maif', + 'man', + 'mango', + 'marriott', + 'maserati', + 'mattel', + 'mckinsey', + 'metlife', + 'microsoft', + 'mini', + 'mit', + 'mitsubishi', + 'mlb', + 'mma', + 'monash', + 'mormon', + 'moto', + 'movistar', + 'msd', + 'mtn', + 'mtr', + 'mutual', + 'nadex', + 'nationwide', + 'natura', + 'nba', + 'nec', + 'netflix', + 'neustar', + 'newholland', + 'nfl', + 'nhk', + 'nico', + 'nike', + 'nikon', + 'nissan', + 'nissay', + 'nokia', + 'northwesternmutual', + 'norton', + 'nra', + 'ntt', + 'obi', + 'office', + 'omega', + 'oracle', + 'orange', + 'otsuka', + 'ovh', + 'panasonic', + 'pccw', + 'pfizer', + 'philips', + 'piaget', + 'pictet', + 'ping', + 'pioneer', + 'play', + 'playstation', + 'pohl', + 'politie', + 'praxi', + 'prod', + 'progressive', + 'pru', + 'prudential', + 'pwc', + 'quest', + 'qvc', + 'redstone', + 'reliance', + 'rexroth', + 'ricoh', + 'rmit', + 'rocher', + 'rogers', + 'rwe', + 'safety', + 'sakura', + 'samsung', + 'sandvik', + 'sandvikcoromant', + 'sanofi', + 'sap', + 'saxo', + 'sbi', + 'sbs', + 'sca', + 'scb', + 'schaeffler', + 'schmidt', + 'schwarz', + 'scjohnson', + 'scor', + 'seat', + 'sener', + 'ses', + 'sew', + 'seven', + 'sfr', + 'seek', + 'shangrila', + 'sharp', + 'shaw', + 'shell', + 'shriram', + 'sina', + 'sky', + 'skype', + 'smart', + 'sncf', + 'softbank', + 'sohu', + 'sony', + 'spiegel', + 'stada', + 'staples', + 'star', + 'starhub', + 'statebank', + 'statefarm', + 'statoil', + 'stc', + 'stcgroup', + 'suzuki', + 'swatch', + 'swiftcover', + 'symantec', + 'taobao', + 'target', + 'tatamotors', + 'tdk', + 'telecity', + 'telefonica', + 'temasek', + 'teva', + 'tiffany', + 'tjx', + 'toray', + 'toshiba', + 'total', + 'toyota', + 'travelchannel', + 'travelers', + 'tui', + 'tvs', + 'ubs', + 'unicom', + 'uol', + 'ups', + 'vanguard', + 'verisign', + 'vig', + 'viking', + 'virgin', + 'visa', + 'vista', + 'vistaprint', + 'vivo', + 'volkswagen', + 'volvo', + 'walmart', + 'walter', + 'weatherchannel', + 'weber', + 'weir', + 'williamhill', + 'windows', + 'wme', + 'wolterskluwer', + 'woodside', + 'wtc', + 'xbox', + 'xerox', + 'xfinity', + 'yahoo', + 'yamaxun', + 'yandex', + 'yodobashi', + 'youtube', + 'zappos', + 'zara', + 'zippo' +]; + // now we can set up imap clients for all providers and get their values all at once const imapConfigurations = [ // Forward Email @@ -978,427 +1402,7 @@ const config = { 'ukaea.uk', // - 'aaa', - 'aarp', - 'abarth', - 'abb', - 'abbott', - 'abbvie', - 'abc', - 'accenture', - 'aco', - 'aeg', - 'aetna', - 'afl', - 'agakhan', - 'aig', - 'aigo', - 'airbus', - 'airtel', - 'akdn', - 'alfaromeo', - 'alibaba', - 'alipay', - 'allfinanz', - 'allstate', - 'ally', - 'alstom', - 'amazon', - 'americanexpress', - 'amex', - 'amica', - 'android', - 'anz', - 'aol', - 'apple', - 'aquarelle', - 'aramco', - 'audi', - 'auspost', - 'aws', - 'axa', - 'azure', - 'baidu', - 'bananarepublic', - 'barclaycard', - 'barclays', - 'basketball', - 'bauhaus', - 'bbc', - 'bbt', - 'bbva', - 'bcg', - 'bentley', - 'bharti', - 'bing', - 'blanco', - 'bloomberg', - 'bms', - 'bmw', - 'bnl', - 'bnpparibas', - 'boehringer', - 'bond', - 'booking', - 'bosch', - 'bostik', - 'bradesco', - 'bridgestone', - 'brother', - 'bugatti', - 'cal', - 'calvinklein', - 'canon', - 'capitalone', - 'caravan', - 'cartier', - 'cba', - 'cbn', - 'cbre', - 'cbs', - 'cern', - 'cfa', - 'chanel', - 'chase', - 'chintai', - 'chrome', - 'chrysler', - 'cipriani', - 'cisco', - 'citadel', - 'citi', - 'citic', - 'clubmed', - 'comcast', - 'commbank', - 'creditunion', - 'crown', - 'crs', - 'csc', - 'cuisinella', - 'dabur', - 'datsun', - 'dealer', - 'dell', - 'deloitte', - 'delta', - 'dhl', - 'discover', - 'dish', - 'dnp', - 'dodge', - 'dunlop', - 'dupont', - 'dvag', - 'edeka', - 'emerck', - 'epson', - 'ericsson', - 'erni', - 'esurance', - 'etisalat', - 'eurovision', - 'everbank', - 'extraspace', - 'fage', - 'fairwinds', - 'farmers', - 'fedex', - 'ferrari', - 'ferrero', - 'fiat', - 'fidelity', - 'firestone', - 'firmdale', - 'flickr', - 'flir', - 'flsmidth', - 'ford', - 'fox', - 'fresenius', - 'forex', - 'frogans', - 'frontier', - 'fujitsu', - 'fujixerox', - 'gallo', - 'gallup', - 'gap', - 'gbiz', - 'gea', - 'genting', - 'giving', - 'gle', - 'globo', - 'gmail', - 'gmo', - 'gmx', - 'godaddy', - 'goldpoint', - 'goodyear', - 'goog', - 'google', - 'grainger', - 'guardian', - 'gucci', - 'hbo', - 'hdfc', - 'hdfcbank', - 'hermes', - 'hisamitsu', - 'hitachi', - 'hkt', - 'honda', - 'honeywell', - 'hotmail', - 'hsbc', - 'hughes', - 'hyatt', - 'hyundai', - 'ibm', - 'ieee', - 'ifm', - 'ikano', - 'imdb', - 'infiniti', - 'intel', - 'intuit', - 'ipiranga', - 'iselect', - 'itau', - 'itv', - 'iveco', - 'jaguar', - 'java', - 'jcb', - 'jcp', - 'jeep', - 'jpmorgan', - 'juniper', - 'kddi', - 'kerryhotels', - 'kerrylogistics', - 'kerryproperties', - 'kfh', - 'kia', - 'kinder', - 'kindle', - 'komatsu', - 'kpmg', - 'kred', - 'kuokgroup', - 'lacaixa', - 'ladbrokes', - 'lamborghini', - 'lancaster', - 'lancia', - 'lancome', - 'landrover', - 'lanxess', - 'lasalle', - 'latrobe', - 'lds', - 'leclerc', - 'lego', - 'liaison', - 'lexus', - 'lidl', - 'lifestyle', - 'lilly', - 'lincoln', - 'linde', - 'lipsy', - 'lixil', - 'locus', - 'lotte', - 'lpl', - 'lplfinancial', - 'lundbeck', - 'lupin', - 'macys', - 'maif', - 'man', - 'mango', - 'marriott', - 'maserati', - 'mattel', - 'mckinsey', - 'metlife', - 'microsoft', - 'mini', - 'mit', - 'mitsubishi', - 'mlb', - 'mma', - 'monash', - 'mormon', - 'moto', - 'movistar', - 'msd', - 'mtn', - 'mtr', - 'mutual', - 'nadex', - 'nationwide', - 'natura', - 'nba', - 'nec', - 'netflix', - 'neustar', - 'newholland', - 'nfl', - 'nhk', - 'nico', - 'nike', - 'nikon', - 'nissan', - 'nissay', - 'nokia', - 'northwesternmutual', - 'norton', - 'nra', - 'ntt', - 'obi', - 'office', - 'omega', - 'oracle', - 'orange', - 'otsuka', - 'ovh', - 'panasonic', - 'pccw', - 'pfizer', - 'philips', - 'piaget', - 'pictet', - 'ping', - 'pioneer', - 'play', - 'playstation', - 'pohl', - 'politie', - 'praxi', - 'prod', - 'progressive', - 'pru', - 'prudential', - 'pwc', - 'quest', - 'qvc', - 'redstone', - 'reliance', - 'rexroth', - 'ricoh', - 'rmit', - 'rocher', - 'rogers', - 'rwe', - 'safety', - 'sakura', - 'samsung', - 'sandvik', - 'sandvikcoromant', - 'sanofi', - 'sap', - 'saxo', - 'sbi', - 'sbs', - 'sca', - 'scb', - 'schaeffler', - 'schmidt', - 'schwarz', - 'scjohnson', - 'scor', - 'seat', - 'sener', - 'ses', - 'sew', - 'seven', - 'sfr', - 'seek', - 'shangrila', - 'sharp', - 'shaw', - 'shell', - 'shriram', - 'sina', - 'sky', - 'skype', - 'smart', - 'sncf', - 'softbank', - 'sohu', - 'sony', - 'spiegel', - 'stada', - 'staples', - 'star', - 'starhub', - 'statebank', - 'statefarm', - 'statoil', - 'stc', - 'stcgroup', - 'suzuki', - 'swatch', - 'swiftcover', - 'symantec', - 'taobao', - 'target', - 'tatamotors', - 'tdk', - 'telecity', - 'telefonica', - 'temasek', - 'teva', - 'tiffany', - 'tjx', - 'toray', - 'toshiba', - 'total', - 'toyota', - 'travelchannel', - 'travelers', - 'tui', - 'tvs', - 'ubs', - 'unicom', - 'uol', - 'ups', - 'vanguard', - 'verisign', - 'vig', - 'viking', - 'virgin', - 'visa', - 'vista', - 'vistaprint', - 'vivo', - 'volkswagen', - 'volvo', - 'walmart', - 'walter', - 'weatherchannel', - 'weber', - 'weir', - 'williamhill', - 'windows', - 'wme', - 'wolterskluwer', - 'woodside', - 'wtc', - 'xbox', - 'xerox', - 'xfinity', - 'yahoo', - 'yamaxun', - 'yandex', - 'yodobashi', - 'youtube', - 'zappos', - 'zara', - 'zippo' + ...brandAndCorporateDomains ], goodDomains: [ @@ -1518,11 +1522,15 @@ const config = { // arbitrarily add domains to the denylist for (const tld of tlds) { - if (config.restrictedDomains.includes(tld)) continue; + if ( + config.restrictedDomains.includes(tld) && + !brandAndCorporateDomains.includes(tld) + ) + continue; // amikalpop.* (e.g. "amikalpop.mom") - config.denylist.add(`amikalpop.${tld}`); + config.denylist.add(`amikalpop.${punycode.toASCII(tld)}`); // postline.* (e.g. "postline.ml") - config.denylist.add(`postline.${tld}`); + config.denylist.add(`postline.${punycode.toASCII(tld)}`); } // sanity test against validDurations and durationMapping length diff --git a/helpers/get-error-code.js b/helpers/get-error-code.js index 40e17831cb..4d570a033f 100644 --- a/helpers/get-error-code.js +++ b/helpers/get-error-code.js @@ -37,14 +37,9 @@ function getErrorCode(err) { if ( (typeof err.responseCode !== 'number' || err.responseCode > 500) && (['defer', 'slowdown'].includes(err.bounceInfo.action) || - [ - 'block', - 'blocklist', - 'capacity', - 'network', - 'protocol', - 'policy' - ].includes(err.bounceInfo.category)) + ['block', 'blocklist', 'network', 'protocol', 'policy'].includes( + err.bounceInfo.category + )) ) return 421; diff --git a/helpers/is-allowlisted.js b/helpers/is-allowlisted.js index c53a2a8420..8733d1b157 100644 --- a/helpers/is-allowlisted.js +++ b/helpers/is-allowlisted.js @@ -74,6 +74,12 @@ async function isAllowlisted(val, client, resolver, ignoreRedis = false) { } else if (isFQDN(lc)) { const root = parseRootDomain(lc); if (root === env.WEB_HOST) return true; + // check if root domain was allowlisted + if ( + root !== lc && + (await isAllowlisted(root, client, resolver, ignoreRedis)) + ) + return true; } else if (isIP(val)) { try { // reverse lookup IP and if it was allowlisted then return early diff --git a/helpers/send-email.js b/helpers/send-email.js index 47c36ef141..f622045d78 100644 --- a/helpers/send-email.js +++ b/helpers/send-email.js @@ -27,29 +27,48 @@ const { decrypt } = require('./encrypt-decrypt'); const config = require('#config'); -// eslint-disable-next-line complexity -async function sendEmail( - { - session, - cache, - target, - port = 25, - envelope, - raw, - resolver, - client, - publicKey - }, +async function signMessage(raw, domain) { + const signResult = await dkimSign(raw, { + canonicalization: 'relaxed/relaxed', + algorithm: 'rsa-sha256', + signTime: new Date(), + signatureData: domain + ? [ + { + signingDomain: domain.name, + selector: domain.dkim_key_selector, + privateKey: decrypt(domain.dkim_private_key), + algorithm: 'rsa-sha256', + canonicalization: 'relaxed/relaxed' + } + ] + : [config.signatureData] + }); + + if (signResult.errors.length > 0) { + const err = combineErrors(signResult.errors.map((error) => error.err)); + // we may want to remove cyclical reference + // for (const error of signResult.errors) { + // delete error.err; + // } + err.signResult = signResult; + throw err; + } + + const signatures = Buffer.from(signResult.signatures, 'utf8'); + return Buffer.concat([signatures, raw], signatures.length + raw.length); +} + +async function getPGPResults({ + session, + envelope, + raw, + publicKey, + resolver, + client, email, domain -) { - if ( - !_.isObject(envelope) || - typeof envelope.to !== 'string' || - !isEmail(envelope.to) - ) - throw new TypeError('Envelope to missing or not a single email'); - +}) { // check if the message was encrypted already let isEncrypted = false; try { @@ -63,6 +82,7 @@ async function sendEmail( // NOTE: This is basically a fallback in case the message was not encrypted already to the recipient(s)) // (e.g. when it arrives at the destination mail server, it is already encrypted) // + let finalRaw; let pgp = false; if ( @@ -95,40 +115,7 @@ async function sendEmail( false ); - const signResult = await dkimSign(encryptedUnsignedMessage, { - canonicalization: 'relaxed/relaxed', - algorithm: 'rsa-sha256', - signTime: new Date(), - signatureData: domain - ? [ - { - signingDomain: domain.name, - selector: domain.dkim_key_selector, - privateKey: decrypt(domain.dkim_private_key), - algorithm: 'rsa-sha256', - canonicalization: 'relaxed/relaxed' - } - ] - : [config.signatureData] - }); - - if (signResult.errors.length > 0) { - const err = combineErrors( - signResult.errors.map((error) => error.err) - ); - // we may want to remove cyclical reference - // for (const error of signResult.errors) { - // delete error.err; - // } - err.signResult = signResult; - throw err; - } - - const signatures = Buffer.from(signResult.signatures, 'utf8'); - raw = Buffer.concat( - [signatures, encryptedUnsignedMessage], - signatures.length + encryptedUnsignedMessage.length - ); + finalRaw = await signMessage(encryptedUnsignedMessage, domain); pgp = true; } catch (err) { logger.fatal( @@ -160,37 +147,32 @@ async function sendEmail( } // if no PGP then we need to still sign with DKIM - if (!pgp) { - const signResult = await dkimSign(raw, { - canonicalization: 'relaxed/relaxed', - algorithm: 'rsa-sha256', - signTime: new Date(), - signatureData: domain - ? [ - { - signingDomain: domain.name, - selector: domain.dkim_key_selector, - privateKey: decrypt(domain.dkim_private_key), - algorithm: 'rsa-sha256', - canonicalization: 'relaxed/relaxed' - } - ] - : [config.signatureData] - }); + if (!pgp || !finalRaw) finalRaw = await signMessage(raw, domain); - if (signResult.errors.length > 0) { - const err = combineErrors(signResult.errors.map((error) => error.err)); - // we may want to remove cyclical reference - // for (const error of signResult.errors) { - // delete error.err; - // } - err.signResult = signResult; - throw err; - } + return { finalRaw, pgp }; +} - const signatures = Buffer.from(signResult.signatures, 'utf8'); - raw = Buffer.concat([signatures, raw], signatures.length + raw.length); - } +async function sendEmail( + { + session, + cache, + target, + port = 25, + envelope, + raw, + resolver, + client, + publicKey + }, + email, + domain +) { + if ( + !_.isObject(envelope) || + typeof envelope.to !== 'string' || + !isEmail(envelope.to) + ) + throw new TypeError('Envelope to missing or not a single email'); // // if we're in development mode then use preview-email to render queue processing @@ -208,44 +190,76 @@ async function sendEmail( }; } - const ignoreMXHosts = []; - let mxLastError; - - try { - const { - truthSource, - mx, - requireTLS, - ignoreTLS, - opportunisticTLS, - tls, - transporter - } = await getTransporter({ + const [pgpResults, transporterResults] = await Promise.all([ + getPGPResults({ + session, + envelope, + raw, + publicKey, + resolver, + client, + email, + domain + }), + getTransporter({ target, port, resolver, logger, cache, client - }); + }) + ]); + + const { + truthSource, + mx, + requireTLS, + ignoreTLS, + opportunisticTLS, + tls, + transporter + } = transporterResults; + + // + // NOTE: Proton Mail rewrites messages, so we shouldn't use PGP for them + // and should email the users separately regarding this + // + // https://github.com/ProtonMail/proton-bridge/issues/26 + // https://github.com/ProtonMail/proton-bridge/issues/216 + // https://proton.me/support/email-has-failed-its-domains-authentication-requirements-warning + // https://news.ycombinator.com/item?id=36639530 + // https://proton.me/support/what-is-difference-between-proton-domains + // - proton.me + // - protonmail.ch + // - protonmail.com + // - pm.me + // - + supports custom domains since we use truthSource (resolved MX server) + // + if (truthSource === 'protonmail.ch') { + pgpResults.finalRaw = await signMessage(raw, domain); + pgpResults.pgp = false; + // TODO: email users one-time per month regarding this issue + } - session.truthSource = truthSource; - session.mx = _.omit(mx, ['socket']); - session.requireTLS = requireTLS; - session.ignoreTLS = ignoreTLS; - session.opportunisticTLS = opportunisticTLS; - session.tls = tls; + const ignoreMXHosts = []; + let mxLastError; + session.truthSource = truthSource; + session.mx = _.omit(mx, ['socket']); + session.requireTLS = requireTLS; + session.ignoreTLS = ignoreTLS; + session.opportunisticTLS = opportunisticTLS; + session.tls = tls; + + try { // TODO: handle transporter cleanup // TODO: handle mx socket close - const info = await transporter.sendMail({ envelope, - raw + raw: pgpResults.finalRaw }); - - info.pgp = pgp; - + info.pgp = pgpResults.pgp; return info; } catch (err) { // delete `err.cert` for security @@ -336,11 +350,9 @@ async function sendEmail( try { const info = await transporter.sendMail({ envelope, - raw + raw: pgpResults.finalRaw }); - - info.pgp = pgp; - + info.pgp = pgpResults.pgp; return info; } catch (err) { // delete `err.cert` for security