Skip to content
This repository has been archived by the owner on Dec 12, 2024. It is now read-only.

Commit

Permalink
feat: added regex support with substitutions and flags too (closes #245
Browse files Browse the repository at this point in the history
…, closes #250, closes #226)
  • Loading branch information
niftylettuce committed Sep 27, 2021
1 parent 8cfc0b6 commit 27f2974
Show file tree
Hide file tree
Showing 4 changed files with 891 additions and 624 deletions.
85 changes: 82 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const mxConnect = require('mx-connect');
const nodemailer = require('nodemailer');
const pify = require('pify');
const punycode = require('punycode/');
const regexParser = require('regex-parser');
const revHash = require('rev-hash');
const safeStringify = require('fast-safe-stringify');
const sharedConfig = require('@ladjs/shared-config');
Expand Down Expand Up @@ -2461,9 +2462,87 @@ class ForwardEmail {
for (const element of addresses) {
// convert addresses to lowercase
const lowerCaseAddress = element.toLowerCase();
if (
(lowerCaseAddress.includes(':') ||
lowerCaseAddress.indexOf('!') === 0) &&

// must start with / and end with /: and not have the same index for the last index
// forward-email=/^(support|info)$/:[email protected]
// -> this would forward to [email protected] if email sent to support@

// it either ends with:
// "/gi:"
// "/ig:"
// "/g:"
// "/i:"
// "/:"
//
let lastIndex;
const REGEX_FLAG_ENDINGS = ['/gi:', '/ig:', '/g:', '/i:', '/:'];
const hasTwoSlashes = element.lastIndexOf('/') !== 0;
const startsWithSlash = element.startsWith('/');
if (startsWithSlash && hasTwoSlashes) {
for (const ending of REGEX_FLAG_ENDINGS) {
if (
element.lastIndexOf(ending) !== -1 &&
element.lastIndexOf(ending) !== 0
) {
lastIndex = ending;
break;
}
}
}

//
// regular expression support
// <https://github.com/forwardemail/free-email-forwarding/pull/245/commits/e04ea02d700b51771bf61ed512d1763bbf80784b>
// (with added support for regex gi flags)
//
if (startsWithSlash && hasTwoSlashes && lastIndex) {
const elementWithoutRegex = element.slice(
Math.max(0, element.lastIndexOf(lastIndex) + lastIndex.length)
);
let parsedRegex = element.slice(
0,
Math.max(0, element.lastIndexOf(lastIndex) + 1)
);

// add case insensitive flag since email addresses are case insensitive
if (lastIndex === '/g:' || lastIndex === '/:') parsedRegex += 'i';
//
// `forward-email=/^(support|info)$/:[email protected]`
// [email protected] -> [email protected]
//
// `forward-email=/^(support|info)$/:niftylettuce.com/$1`
// [email protected] -> POST to niftylettuce.com/info
//
// `forward-email=/Support/g:niftylettuce.com`
//
// `forward-email=/SUPPORT/gi:niftylettuce.com`
const regex = new RE2(regexParser(parsedRegex));
if (regex.test(username.toLowerCase())) {
const substitutedAlias = username
.toLowerCase()
.replace(regex, elementWithoutRegex);
if (substitutedAlias.startsWith('!')) {
ignored = true;
break;
}

if (
!isFQDN(substitutedAlias) &&
!validator.isIP(substitutedAlias) &&
!validator.isEmail(substitutedAlias) &&
!validator.isURL(substitutedAlias, this.config.isURLOptions)
)
throw new CustomError(
// TODO: we may want to replace this with "Invalid Recipients"
`Domain of ${domain} has an invalid "${this.config.recordPrefix}" TXT record due to an invalid regular expression email address match`
);

if (validator.isURL(substitutedAlias, this.config.isURLOptions))
forwardingAddresses.push(substitutedAlias);
else forwardingAddresses.push(substitutedAlias.toLowerCase());
}
} else if (
(element.includes(':') || element.indexOf('!') === 0) &&
!validator.isURL(element, this.config.isURLOptions)
) {
// > const str = 'foo:https://foo.com'
Expand Down
19 changes: 10 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"bytes": "^3.1.0",
"common-tags": "^1.8.0",
"dnsbl": "^3.2.0",
"fast-safe-stringify": "^2.0.8",
"fast-safe-stringify": "^2.1.1",
"get-fqdn": "^0.0.4",
"get-port": "^5.1.1",
"get-stream": "^6.0.1",
Expand All @@ -47,17 +47,18 @@
"is-fqdn": "^2.0.1",
"is-string-and-not-blank": "^0.0.2",
"lodash": "^4.17.21",
"mailauth": "^2.1.3",
"mailparser": "^3.3.0",
"mailsplit": "^5.0.1",
"mailauth": "^2.2.2",
"mailparser": "^3.3.1",
"mailsplit": "^5.2.0",
"ms": "^2.1.3",
"mx-connect": "^1.2.0",
"nodemailer": "^6.6.3",
"nodemailer": "^6.6.5",
"pify": "^5.0.0",
"pino": "^6.13.1",
"pino": "^6.13.3",
"punycode": "^2.1.1",
"ratelimiter": "^3.4.1",
"re2": "^1.16.0",
"regex-parser": "^2.2.11",
"rev-hash": "^3.0.0",
"sender-rewriting-scheme": "^1.0.0",
"signale": "^1.4.0",
Expand All @@ -68,7 +69,7 @@
"titleize": "2",
"uuid-by-string": "^3.0.4",
"validator": "^13.6.0",
"zone-mta": "^3.1.0"
"zone-mta": "^3.2.3"
},
"devDependencies": {
"@commitlint/cli": "^13.1.0",
Expand All @@ -79,8 +80,8 @@
"eslint": "^7.32.0",
"eslint-config-xo-lass": "^1.0.5",
"fixpack": "^4.0.0",
"husky": "^7.0.1",
"ioredis": "^4.27.8",
"husky": "^7.0.2",
"ioredis": "^4.27.9",
"is-ci": "^3.0.0",
"lint-staged": "^11.1.2",
"nyc": "^15.1.0",
Expand Down
68 changes: 68 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,74 @@ Mime-Version: 1.0
Content-Type: text/plain; charset=us-ascii
Content-Transfer-Encoding: 7bit
Test`.trim()
});
return new Promise((resolve) => {
connection.once('end', resolve);
connection.connect(() => {
connection.send(info.envelope, info.message, (err) => {
t.is(err, null);
connection.close();
});
});
});
});

test('regex with global flag', async (t) => {
const transporter = nodemailer.createTransport({
streamTransport: true
});
const { port } = t.context.forwardEmail.server.address();
const connection = new Client({ port, tls });
const info = await transporter.sendMail({
envelope: {
from: '[email protected]',
to: '[email protected]'
},
raw: `
Message-ID: <123.abc@test>
Date: Thu, 9 Nov 2000 10:44:00 -0800 (PST)
To: [email protected]
From: [email protected]
Subject: testing regex with global flag
Mime-Version: 1.0
Content-Type: text/plain; charset=us-ascii
Content-Transfer-Encoding: 7bit
Test`.trim()
});
return new Promise((resolve) => {
connection.once('end', resolve);
connection.connect(() => {
connection.send(info.envelope, info.message, (err) => {
t.is(err, null);
connection.close();
});
});
});
});

test('regex with replacement', async (t) => {
const transporter = nodemailer.createTransport({
streamTransport: true
});
const { port } = t.context.forwardEmail.server.address();
const connection = new Client({ port, tls });
const info = await transporter.sendMail({
envelope: {
from: '[email protected]',
to: '[email protected]'
},
raw: `
Message-ID: <123.abc@test>
Date: Thu, 9 Nov 2000 10:44:00 -0800 (PST)
To: [email protected]
From: [email protected]
Subject: testing regex with global flag
Mime-Version: 1.0
Content-Type: text/plain; charset=us-ascii
Content-Transfer-Encoding: 7bit
Test`.trim()
});
return new Promise((resolve) => {
Expand Down
Loading

0 comments on commit 27f2974

Please sign in to comment.