From 6dfd460b7c7534eeb56a19c3afe1df7dd29f3de7 Mon Sep 17 00:00:00 2001 From: Antasel Date: Wed, 16 Aug 2023 23:45:17 +0600 Subject: [PATCH 1/7] keep consistensy with backend email validation --- __tests__/ExpensiMark-HTML-test.js | 136 +++++++++++++++++++++++------ __tests__/Str-test.js | 87 +++++++++++++++--- lib/CONST.jsx | 4 +- lib/ExpensiMark.js | 67 +++++++------- 4 files changed, 219 insertions(+), 75 deletions(-) diff --git a/__tests__/ExpensiMark-HTML-test.js b/__tests__/ExpensiMark-HTML-test.js index fbe18fb6..0362f3d3 100644 --- a/__tests__/ExpensiMark-HTML-test.js +++ b/__tests__/ExpensiMark-HTML-test.js @@ -84,31 +84,13 @@ test('Test multi-line strikethrough markdown replacement', () => { expect(parser.replace(testString)).toBe(replacedString); }); -// Emails containing *_~ are successfully wrapped in a mailto anchor tag +// Emails containing _ are successfully wrapped in a mailto anchor tag test('Test markdown replacement for emails and email links containing bold/strikethrough/italic', () => { - let testInput = 'a~b@gmail.com'; - expect(parser.replace(testInput)).toBe('a~b@gmail.com'); - - testInput = 'a*b@gmail.com'; - expect(parser.replace(testInput)).toBe('a*b@gmail.com'); - - testInput = 'a_b@gmail.com'; + let testInput = 'a_b@gmail.com'; expect(parser.replace(testInput)).toBe('a_b@gmail.com'); - testInput = 'a~*_b@gmail.com'; - expect(parser.replace(testInput)).toBe('a~*_b@gmail.com'); - - testInput = '[text](a~b@gmail.com)'; - expect(parser.replace(testInput)).toBe('text'); - - testInput = '[text](a*b@gmail.com)'; - expect(parser.replace(testInput)).toBe('text'); - testInput = '[text](a_b@gmail.com)'; expect(parser.replace(testInput)).toBe('text'); - - testInput = '[text](a~*_b@gmail.com)'; - expect(parser.replace(testInput)).toBe('text'); }); // Single-line emails wrapped in *_~ are successfully wrapped in a mailto anchor tag @@ -160,6 +142,12 @@ test('Test markdown replacement for emails wrapped in bold/strikethrough/italic + 'def@gmail.com'; expect(parser.replace(testInput)).toBe(result); + testInput = '_email@test.com\n_email2@test.com\n\nemail3@test.com_'; + result = 'email@test.com
' + + '_email2@test.com

' + + 'email3@test.com
'; + expect(parser.replace(testInput)).toBe(result); + testInput = '[text](~abc@gmail.com)\n[text](def@gmail.com~)'; result = '[text](abc@gmail.com)
' + '[text](def@gmail.com
)'; @@ -171,8 +159,8 @@ test('Test markdown replacement for emails wrapped in bold/strikethrough/italic expect(parser.replace(testInput)).toBe(result); testInput = '[text](_abc@gmail.com)\n[text](def@gmail.com_)'; - result = '[text](abc@gmail.com)
' - + '[text](def@gmail.com
)'; + result = 'text
' + + '[text](def@gmail.com_)'; expect(parser.replace(testInput)).toBe(result); testInput = '[text](~*_abc@gmail.com)\n[text](def@gmail.com_*~)'; @@ -181,6 +169,104 @@ test('Test markdown replacement for emails wrapped in bold/strikethrough/italic expect(parser.replace(testInput)).toBe(result); }); +// Check emails within other markdown +test('Test emails within other markdown', () => { + const testString = '> test@example.com\n' + + '```test@example.com```\n' + + '`test@example.com`\n' + + '_test@example.com_ ' + + '__test@example.com_'; + const result = '
test@example.com
' + + '
test@example.com
' + + 'test@example.com
' + + 'test@example.com ' + + '_test@example.com'; + expect(parser.replace(testString)).toBe(result); +}); + +// Check email regex's validity at various limits +test('Test markdown replacement for valid emails', () => { + const testString = 'A simple email: abc@gmail.com, ' + + 'or a very short one a@example.com ' + + 'hitting the maximum domain length (63 chars) test@asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcj.com ' + + 'or the maximum address length (64 chars) sjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcjab@test.com ' + + 'overall length of 254 averylongaddresspartthatalmostwillreachthelimitofcharsperaddress@nowwejustneedaverylongdomainpartthatwill.reachthetotallengthlimitforthewholeemailaddress.whichis254charsaccordingtothePHPvalidate-email-filter.extendingthetestlongeruntilwereachtheright.com ' + + 'domain with many dashes sjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcjab@asj-j-s-sjdjdjdjd-jdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdke.com.ab.net.aa.bb.cc.dd.ee ' + + ' how about a domain with repeated labels of 63 chars test@asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekasgasgasgasgashfnfn.asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekasgasgasgasgashfnfn.asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekasgasgasgasgashfnfn.com ' + + 'max length email with italics ' + + '_averylongaddresspartthatalmostwillreachthelimitofcharsperaddress@nowwejustneedaverylongdomainpartthatwill.reachthetotallengthlimitforthewholeemailaddress.whichis254charsaccordingtothePHPvalidate-email-filter.extendingthetestlongeruntilwereachtheright.com_ ' + + ' xn-- style domain test@xn--diseolatinoamericano-76b.com ' + + 'or a more complex case where we need to determine where to apply italics markdown ' + + '_email@test.com\n_email2@test.com\n\nemail3@test.com_ ' + + 'some unusual, but valid prefixes -test@example.com ' + + ' and _test@example.com ' + + 'a max length email enclosed in brackets ' + + '(averylongaddresspartthatalmostwillreachthelimitofcharsperaddress@nowwejustneedaverylongdomainpartthatwill.reachthetotallengthlimitforthewholeemailaddress.whichis254charsaccordingtothePHPvalidate-email-filter.extendingthetestlongeruntilwereachtheright.com) ' + + 'max length email with ellipsis ending ' + + 'averylongaddresspartthatalmostwillreachthelimitofcharsperaddress@nowwejustneedaverylongdomainpartthatwill.reachthetotallengthlimitforthewholeemailaddress.whichis254charsaccordingtothePHPvalidate-email-filter.extendingthetestlongeruntilwereachtheright.com... ' + + 'try a markdown link with a valid max-length email ' + + '[text](sjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcjab@asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcj.com.a.aa.asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcj.asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjasfff)' + + '$--test@gmail.com'; + const result = 'A simple email: abc@gmail.com, ' + + 'or a very short one a@example.com ' + + 'hitting the maximum domain length (63 chars) test@asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcj.com ' + + 'or the maximum address length (64 chars) sjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcjab@test.com ' + + 'overall length of 254 averylongaddresspartthatalmostwillreachthelimitofcharsperaddress@nowwejustneedaverylongdomainpartthatwill.reachthetotallengthlimitforthewholeemailaddress.whichis254charsaccordingtothePHPvalidate-email-filter.extendingthetestlongeruntilwereachtheright.com ' + + 'domain with many dashes sjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcjab@asj-j-s-sjdjdjdjd-jdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdke.com.ab.net.aa.bb.cc.dd.ee ' + + ' how about a domain with repeated labels of 63 chars test@asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekasgasgasgasgashfnfn.asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekasgasgasgasgashfnfn.asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekasgasgasgasgashfnfn.com ' + + 'max length email with italics ' + + 'averylongaddresspartthatalmostwillreachthelimitofcharsperaddress@nowwejustneedaverylongdomainpartthatwill.reachthetotallengthlimitforthewholeemailaddress.whichis254charsaccordingtothePHPvalidate-email-filter.extendingthetestlongeruntilwereachtheright.com ' + + ' xn-- style domain test@xn--diseolatinoamericano-76b.com ' + + 'or a more complex case where we need to determine where to apply italics markdown ' + + 'email@test.com
' + + '_email2@test.com

' + + 'email3@test.com
' + + 'some unusual, but valid prefixes -test@example.com ' + + ' and _test@example.com ' + + 'a max length email enclosed in brackets ' + + '(averylongaddresspartthatalmostwillreachthelimitofcharsperaddress@nowwejustneedaverylongdomainpartthatwill.reachthetotallengthlimitforthewholeemailaddress.whichis254charsaccordingtothePHPvalidate-email-filter.extendingthetestlongeruntilwereachtheright.com) ' + + 'max length email with ellipsis ending ' + + 'averylongaddresspartthatalmostwillreachthelimitofcharsperaddress@nowwejustneedaverylongdomainpartthatwill.reachthetotallengthlimitforthewholeemailaddress.whichis254charsaccordingtothePHPvalidate-email-filter.extendingthetestlongeruntilwereachtheright.com... ' + + 'try a markdown link with a valid max-length email ' + + 'text' + + '$--test@gmail.com'; + expect(parser.replace(testString)).toBe(result); +}); + +test('Test markdown replacement for invalid emails', () => { + const testString = 'Replace the valid email part within a string ' + + '.test@example.com ' + + '$test@gmail.com ' + + 'test..new@example.com ' + + 'Some chars that are not allowed within emails ' + + 'test{@example.com ' + + 'domain length over limit test@averylongdomainpartoftheemailthatwillgooverthelimitasitismorethan63chars.com ' + + 'address over limit averylongaddresspartoftheemailthatwillgovoerthelimitasitismorethan64chars@example.com ' + + 'overall length too long ' + + 'sjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcjab@asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcj.com.a.aa.asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcj.asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjasfffa ' + + 'invalid domain start/end ' + + 'test@example-.com ' + + 'test@-example-.com ' + + 'test@example.a ' + + '[text](sjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcjab@asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcj.com.a.aa.asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcj.asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjasfffa)'; + const result = 'Replace the valid email part within a string ' + + '.test@example.com ' + + '$test@gmail.com ' + + 'test..new@example.com ' + + 'Some chars that are not allowed within emails ' + + 'test{@example.com ' + + 'domain length over limit test@averylongdomainpartoftheemailthatwillgooverthelimitasitismorethan63chars.com ' + + 'address over limit averylongaddresspartoftheemailthatwillgovoerthelimitasitismorethan64chars@example.com ' + + 'overall length too long ' + + 'sjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcjab@asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcj.com.a.aa.asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcj.asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjasfffa ' + + 'invalid domain start/end ' + + 'test@example-.com ' + + 'test@-example-.com ' + + 'test@example.a ' + + '[text](sjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcjab@asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcj.com.a.aa.asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcj.asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjasfffa)'; + expect(parser.replace(testString)).toBe(result); +}); + // Markdown style links replaced successfully test('Test markdown style links', () => { let testString = 'Go to [Expensify](https://www.expensify.com) to learn more. [Expensify](www.expensify.com) [Expensify](expensify.com) [It\'s really the coolest](expensify.com) [`Some` Special cases - + . = , \'](expensify.com/some?query=par|am)'; @@ -519,7 +605,6 @@ test('Test markdown style email link with various styles', () => { + '_[Expensify](concierge@expensify.com)_ ' + '*[Expensify](concierge@expensify.com)* ' + '[Expensify!](no-concierge1@expensify.com) ' - + '[Expensify?](concierge?@expensify.com) ' + '[Applause](applausetester+qaabecciv@applause.expensifail.com) ' + '[](concierge@expensify.com)' // only parse autoEmail in () + '[ ](concierge@expensify.com)' // only parse autoEmail in () and keep spaces in [] @@ -537,7 +622,6 @@ test('Test markdown style email link with various styles', () => { + 'Expensify ' + 'Expensify ' + 'Expensify! ' - + 'Expensify? ' + 'Applause ' + '[](concierge@expensify.com)' + '[ ](concierge@expensify.com)' @@ -563,14 +647,10 @@ test('Test a url with multiple underscores', () => { test('Test general email link with various styles', () => { const testString = 'Go to concierge@expensify.com ' + 'no-concierge@expensify.com ' - + 'concierge!@expensify.com ' - + 'concierge1?@expensify.com ' + 'applausetester+qaabecciv@applause.expensifail.com '; const resultString = 'Go to concierge@expensify.com ' + 'no-concierge@expensify.com ' - + 'concierge!@expensify.com ' - + 'concierge1?@expensify.com ' + 'applausetester+qaabecciv@applause.expensifail.com '; expect(parser.replace(testString)).toBe(resultString); diff --git a/__tests__/Str-test.js b/__tests__/Str-test.js index 0132ddfb..8ab1fce7 100644 --- a/__tests__/Str-test.js +++ b/__tests__/Str-test.js @@ -43,15 +43,6 @@ describe('Str.isValidURL', () => { }); }); -describe('Str.isValidEmailMarkdown', () => { - it('Correctly identifies valid mark down emails', () => { - expect(Str.isValidEmailMarkdown('abc@gmail.com')).toBeTruthy(); - expect(Str.isValidEmailMarkdown('$test@gmail.com')).toBeTruthy(); - expect(Str.isValidEmailMarkdown('~abc@gmail.com~')).toBeFalsy(); - expect(Str.isValidEmailMarkdown('abc@gmail.com~')).toBeFalsy(); - }); -}); - describe('Str.stripHTML', () => { it('Correctly strips HTML/XML tags', () => { expect(Str.stripHTML('hello')).toBe('hello'); @@ -129,10 +120,80 @@ describe('Str.fromCurrencyToNumber', () => { }); describe('Str.isValidEmail', () => { - it('Correctly detects a valid email', () => { + it('Correctly identifies valid emails', () => { expect(Str.isValidEmail('abc@gmail.com')).toBeTruthy(); - expect(Str.isValidEmail('test@gmail')).toBeFalsy(); - expect(Str.isValidEmail('@gmail.com')).toBeFalsy(); - expect(Str.isValidEmail('usernamelongerthan64charactersshouldnotworkaccordingtorfc822whichisusedbyphp@gmail.com')).toBeFalsy(); + + // Domain length (63 chars in each label) + expect(Str.isValidEmail('test@asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcj.com')).toBeTruthy(); + expect(Str.isValidEmail('abc@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.km')).toBeTruthy(); + expect(Str.isValidEmail('abc@co.abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.km')).toBeTruthy(); + + // Address length (64 chars) + expect(Str.isValidEmail('sjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcjab@test.com')).toBeTruthy(); + + // Overall length (254 chars) + expect(Str.isValidEmail('averylongaddresspartthatalmostwillreachthelimitofcharsperaddress@nowwejustneedaverylongdomainpartthatwill.reachthetotallengthlimitforthewholeemailaddress.whichis254charsaccordingtothePHPvalidate-email-filter.extendingthetestlongeruntilwereachtheright.com')).toBeTruthy(); + + // Domain with lots of dashes + expect(Str.isValidEmail('sjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcjab@asj-j-s-sjdjdjdjd-jdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdke.com.ab.net.aa.bb.cc.dd.ee')).toBeTruthy(); + expect(Str.isValidEmail('abc@g---m--ai-l.com')).toBeTruthy(); + + // Domain with repeated labels of 63 chars + expect(Str.isValidEmail('test@asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekasgasgasgasgashfnfn.asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekasgasgasgasgashfnfn.asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekasgasgasgasgashfnfn.com')).toBeTruthy(); + + // TLD >=2 chars + expect(Str.isValidEmail('abc@gmail.co')).toBeTruthy(); + expect(Str.isValidEmail('a@a.abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijk')).toBeTruthy(); + + // Very short address + expect(Str.isValidEmail('a@example.com')).toBeTruthy(); + + // xn-- style domain name + expect(Str.isValidEmail('test@xn--diseolatinoamericano-76b.com')).toBeTruthy(); + + // Unusual but valid prefixes + expect(Str.isValidEmail('-test@example.com')).toBeTruthy(); + expect(Str.isValidEmail('_test@example.com')).toBeTruthy(); + expect(Str.isValidEmail('#test@example.com')).toBeTruthy(); + expect(Str.isValidEmail('test.+123@example.com')).toBeTruthy(); + expect(Str.isValidEmail('-test-@example.com')).toBeTruthy(); + + // Invalid chars + expect(Str.isValidEmail('$test@gmail.com')).toBeFalsy(); + expect(Str.isValidEmail('!test@gmail.com')).toBeFalsy(); + expect(Str.isValidEmail('"test"@gmail.com')).toBeFalsy(); + expect(Str.isValidEmail('~abc@gmail.com~')).toBeFalsy(); + expect(Str.isValidEmail('abc@gmail.com~')).toBeFalsy(); + expect(Str.isValidEmail('test@example_123site.com')).toBeFalsy(); + expect(Str.isValidEmail('test{@example.com')).toBeFalsy(); + expect(Str.isValidEmail('test..new@example.com')).toBeFalsy(); + expect(Str.isValidEmail('test@example-.a.com')).toBeFalsy(); + expect(Str.isValidEmail('test@example......a.com')).toBeFalsy(); + + // Invalid period location + expect(Str.isValidEmail('.test@example.com')).toBeFalsy(); + expect(Str.isValidEmail('.test.new@example.com')).toBeFalsy(); + expect(Str.isValidEmail('test.@example.com')).toBeFalsy(); + + // Domain too long (>63 chars in each label) + expect(Str.isValidEmail('test@averylongdomainpartoftheemailthatwillgooverthelimitasitismorethan63chars.com')).toBeFalsy(); + expect(Str.isValidEmail('abc@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890a.km')).toBeFalsy(); + expect(Str.isValidEmail('abc@co.abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890a.km')).toBeFalsy(); + + // Address too long (>64 chars) + expect(Str.isValidEmail('averylongaddresspartoftheemailthatwillgovoerthelimitasitismorethan64chars@example.com')).toBeFalsy(); + + // Overall length too long + expect(Str.isValidEmail('sjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcjab@asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcj.com.a.aa.asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcj.asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjasfffa')).toBeFalsy(); + + // Incorrect domains start/end + expect(Str.isValidEmail('test@example-.com')).toBeFalsy(); + expect(Str.isValidEmail('test@-example-.com')).toBeFalsy(); + + // TLD too short + expect(Str.isValidEmail('test@example.a')).toBeFalsy(); + + // TLD too long + expect(Str.isValidEmail('a@a.abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijkl')).toBeFalsy(); }); }); diff --git a/lib/CONST.jsx b/lib/CONST.jsx index 4b35e75b..626ed177 100644 --- a/lib/CONST.jsx +++ b/lib/CONST.jsx @@ -1,6 +1,6 @@ /* eslint-disable no-useless-escape */ -const EMAIL_BASE_REGEX = "([\\w\\-\\+\\'#]{1,64}(?:\\.[\\w\\-\\'\\+]+)*@(?:[\\w\\-]+\\.)+[a-z]{2,})"; +const EMAIL_BASE_REGEX = "(?=((?=[\\w'#%+-]+(?:\\.[\\w'#%+-]+)*@)[\\w\\.'#%+-]{1,64}@(?:(?=[a-z\\d]+(?:-+[a-z\\d]+)*\\.)(?:[a-zA-Z\\d-]{1,63}\\.)+[a-zA-Z]{2,63})(?= |_|\\b))(?.*))\\S{3,254}(?=\\k$)"; const MOMENT_FORMAT_STRING = 'YYYY-MM-DD'; @@ -330,7 +330,7 @@ export const CONST = { * * @type String */ - MARKDOWN_EMAIL: "([a-zA-Z0-9.!#$%&'+/=?^`{|}-][a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]*@[a-zA-Z0-9-]+?(\\.[a-zA-Z]+)+)", + MARKDOWN_EMAIL: EMAIL_BASE_REGEX, /** * Regex matching an text containing an Emoji diff --git a/lib/ExpensiMark.js b/lib/ExpensiMark.js index 644b108b..1363974e 100644 --- a/lib/ExpensiMark.js +++ b/lib/ExpensiMark.js @@ -119,20 +119,6 @@ export default class ExpensiMark { }, }, - /** - * Automatically links emails that are not in a link. Runs before the autolinker as it will not link an - * email that is in a link - * Prevent emails from starting with [~_*]. Such emails should not be supported. - */ - { - name: 'autoEmail', - regex: new RegExp( - `(?![^<]*>|[^<>]*<\\/)${CONST.REG_EXP.MARKDOWN_EMAIL}(?![^<]*(<\\/pre>|<\\/code>|<\\/a>))`, - 'gim', - ), - replacement: '$1', - }, - /** * Automatically link urls. Runs last of our linkers since we want anything manual to link before this, * and we do not want to break emails. @@ -158,23 +144,6 @@ export default class ExpensiMark { regex: /^# +(?! )((?:(?!
|\n|\r\n).)+)/gm,
                 replacement: '

$1

', }, - { - name: 'quote', - - // We also want to capture a blank line before or after the quote so that we do not add extra spaces. - // block quotes naturally appear on their own line. Blockquotes should not appear in code fences or - // inline code blocks. A single prepending space should be stripped if it exists - process: (textToProcess, replacement) => { - const regex = new RegExp( - /\n?^> *(?! )(?![^<]*(?:<\/pre>|<\/code>))([^\v\n\r]+)\n?/gm, - ); - return this.modifyTextForQuote(regex, textToProcess, replacement); - }, - replacement: (g1) => { - const replacedText = this.replace(g1, {filterRules: ['heading1'], shouldEscapeText: false}); - return `
${replacedText}
`; - }, - }, { /** * Use \b in this case because it will match on words, letters, @@ -188,13 +157,47 @@ export default class ExpensiMark { name: 'italic', regex: /(\b_+|\b)(?!_blank")_((?![\s_])[\s\S]*?[^\s_])_(?![^\W_])(?![^<]*(<\/pre>|<\/code>|<\/a>|<\/mention-user>|_blank))/g, - // We want to add extraLeadingUnderscores back before the tag + // We want to add extraLeadingUnderscores back before the tag unless textWithinUnderscores starts with valid email replacement: (match, extraLeadingUnderscores, textWithinUnderscores) => ( textWithinUnderscores.includes('
') || this.containsNonPairTag(textWithinUnderscores)
                         ? match
+                        : Boolean(String(textWithinUnderscores).match(`^${CONST.REG_EXP.MARKDOWN_EMAIL}`))
+                        ? `${extraLeadingUnderscores}${textWithinUnderscores}`
                         : `${extraLeadingUnderscores}${textWithinUnderscores}`
                 ),
             },
+
+            /**
+             * Automatically links emails that are not in a link. Runs before the autolinker as it will not link an
+             * email that is in a link
+             * Prevent emails from starting with [~_*]. Such emails should not be supported.
+             */
+            {
+                name: 'autoEmail',
+                regex: new RegExp(
+                    `(?![^<]*>|[^<>]*<\\/(?!em))(?:(?<= |[^\\w'#%+-])|^|\\b)${CONST.REG_EXP.MARKDOWN_EMAIL}(?!((?:(?!|[^<]*(<\/pre>|<\/code>))`,
+                    'gim',
+                ),
+                replacement: '$1',
+            },
+
+            {
+                name: 'quote',
+
+                // We also want to capture a blank line before or after the quote so that we do not add extra spaces.
+                // block quotes naturally appear on their own line. Blockquotes should not appear in code fences or
+                // inline code blocks. A single prepending space should be stripped if it exists
+                process: (textToProcess, replacement) => {
+                    const regex = new RegExp(
+                        /\n?^> *(?! )(?![^<]*(?:<\/pre>|<\/code>))([^\v\n\r]+)\n?/gm,
+                    );
+                    return this.modifyTextForQuote(regex, textToProcess, replacement);
+                },
+                replacement: (g1) => {
+                    const replacedText = this.replace(g1, {filterRules: ['heading1'], shouldEscapeText: false});
+                    return `
${replacedText}
`; + }, + }, { // Use \B in this case because \b doesn't match * or ~. // \B will match everything that \b doesn't, so it works From a71dd68bf72772fcb9aa733635b7ff919f71d0b7 Mon Sep 17 00:00:00 2001 From: Antasel Date: Thu, 17 Aug 2023 20:45:35 +0600 Subject: [PATCH 2/7] lint,test cases of _prefixed email inside italic --- __tests__/ExpensiMark-HTML-test.js | 4 ++++ lib/CONST.jsx | 2 +- lib/ExpensiMark.js | 18 ++++++++++-------- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/__tests__/ExpensiMark-HTML-test.js b/__tests__/ExpensiMark-HTML-test.js index 0362f3d3..3e50499e 100644 --- a/__tests__/ExpensiMark-HTML-test.js +++ b/__tests__/ExpensiMark-HTML-test.js @@ -175,11 +175,15 @@ test('Test emails within other markdown', () => { + '```test@example.com```\n' + '`test@example.com`\n' + '_test@example.com_ ' + + '_test@example.com__ ' + + '__test@example.com__ ' + '__test@example.com_'; const result = '
test@example.com
' + '
test@example.com
' + 'test@example.com
' + 'test@example.com ' + + 'test@example.com_ ' + + '_test@example.com_ ' + '_test@example.com'; expect(parser.replace(testString)).toBe(result); }); diff --git a/lib/CONST.jsx b/lib/CONST.jsx index 626ed177..d28b37f8 100644 --- a/lib/CONST.jsx +++ b/lib/CONST.jsx @@ -1,6 +1,6 @@ /* eslint-disable no-useless-escape */ -const EMAIL_BASE_REGEX = "(?=((?=[\\w'#%+-]+(?:\\.[\\w'#%+-]+)*@)[\\w\\.'#%+-]{1,64}@(?:(?=[a-z\\d]+(?:-+[a-z\\d]+)*\\.)(?:[a-zA-Z\\d-]{1,63}\\.)+[a-zA-Z]{2,63})(?= |_|\\b))(?.*))\\S{3,254}(?=\\k$)"; +const EMAIL_BASE_REGEX = '(?=((?=[\\w\'#%+-]+(?:\\.[\\w\'#%+-]+)*@)[\\w\\.\'#%+-]{1,64}@(?:(?=[a-z\\d]+(?:-+[a-z\\d]+)*\\.)(?:[a-zA-Z\\d-]{1,63}\\.)+[a-zA-Z]{2,63})(?= |_|\\b))(?.*))\\S{3,254}(?=\\k$)'; const MOMENT_FORMAT_STRING = 'YYYY-MM-DD'; diff --git a/lib/ExpensiMark.js b/lib/ExpensiMark.js index 1363974e..7f5a3483 100644 --- a/lib/ExpensiMark.js +++ b/lib/ExpensiMark.js @@ -158,13 +158,15 @@ export default class ExpensiMark { regex: /(\b_+|\b)(?!_blank")_((?![\s_])[\s\S]*?[^\s_])_(?![^\W_])(?![^<]*(<\/pre>|<\/code>|<\/a>|<\/mention-user>|_blank))/g, // We want to add extraLeadingUnderscores back before the tag unless textWithinUnderscores starts with valid email - replacement: (match, extraLeadingUnderscores, textWithinUnderscores) => ( - textWithinUnderscores.includes('
') || this.containsNonPairTag(textWithinUnderscores)
-                        ? match
-                        : Boolean(String(textWithinUnderscores).match(`^${CONST.REG_EXP.MARKDOWN_EMAIL}`))
-                        ? `${extraLeadingUnderscores}${textWithinUnderscores}`
-                        : `${extraLeadingUnderscores}${textWithinUnderscores}`
-                ),
+                replacement: (match, extraLeadingUnderscores, textWithinUnderscores) => {
+                    if (textWithinUnderscores.includes('
') || this.containsNonPairTag(textWithinUnderscores)) {
+                        return match;
+                    }
+                    if (String(textWithinUnderscores).match(`^${CONST.REG_EXP.MARKDOWN_EMAIL}`)) {
+                        return `${extraLeadingUnderscores}${textWithinUnderscores}`;
+                    }
+                    return `${extraLeadingUnderscores}${textWithinUnderscores}`;
+                },
             },
 
             /**
@@ -175,7 +177,7 @@ export default class ExpensiMark {
             {
                 name: 'autoEmail',
                 regex: new RegExp(
-                    `(?![^<]*>|[^<>]*<\\/(?!em))(?:(?<= |[^\\w'#%+-])|^|\\b)${CONST.REG_EXP.MARKDOWN_EMAIL}(?!((?:(?!|[^<]*(<\/pre>|<\/code>))`,
+                    `(?![^<]*>|[^<>]*<\\/(?!em))(?:(?<= |[^\\w'#%+-])|^|\\b)${CONST.REG_EXP.MARKDOWN_EMAIL}(?!((?:(?!|[^<]*(<\\/pre>|<\\/code>))`,
                     'gim',
                 ),
                 replacement: '$1',

From 09907d81e0897d2e9be30e94fc5aa1cff1b9418b Mon Sep 17 00:00:00 2001
From: Antasel 
Date: Thu, 17 Aug 2023 23:24:03 +0600
Subject: [PATCH 3/7] prevent email's address parsed as url by autolink

---
 __tests__/ExpensiMark-HTML-test.js | 4 ++--
 lib/ExpensiMark.js                 | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/__tests__/ExpensiMark-HTML-test.js b/__tests__/ExpensiMark-HTML-test.js
index 3e50499e..318b2a6e 100644
--- a/__tests__/ExpensiMark-HTML-test.js
+++ b/__tests__/ExpensiMark-HTML-test.js
@@ -190,7 +190,7 @@ test('Test emails within other markdown', () => {
 
 // Check email regex's validity at various limits
 test('Test markdown replacement for valid emails', () => {
-    const testString = 'A simple email: abc@gmail.com, '
+    const testString = 'A simple email: abc.new@gmail.com, '
     + 'or a very short one a@example.com '
     + 'hitting the maximum domain length (63 chars) test@asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcj.com '
     + 'or the maximum address length (64 chars) sjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcjab@test.com '
@@ -211,7 +211,7 @@ test('Test markdown replacement for valid emails', () => {
     + 'try a markdown link with a valid max-length email '
     + '[text](sjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcjab@asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcj.com.a.aa.asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcj.asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjasfff)'
     + '$--test@gmail.com';
-    const result = 'A simple email: abc@gmail.com, '
+    const result = 'A simple email: abc.new@gmail.com, '
     + 'or a very short one a@example.com '
     + 'hitting the maximum domain length (63 chars) test@asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcj.com '
     + 'or the maximum address length (64 chars) sjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcjab@test.com '
diff --git a/lib/ExpensiMark.js b/lib/ExpensiMark.js
index 7f5a3483..d2545f4b 100644
--- a/lib/ExpensiMark.js
+++ b/lib/ExpensiMark.js
@@ -128,7 +128,7 @@ export default class ExpensiMark {
 
                 process: (textToProcess, replacement) => {
                     const regex = new RegExp(
-                        `(?![^<]*>|[^<>]*<\\/)([_*~]*?)${MARKDOWN_URL_REGEX}\\1(?!((?:(?!|[^<]*(<\\/pre>|<\\/code>))`,
+                        `(?![^<]*>|[^<>]*<\\/)([_*~]*?)${MARKDOWN_URL_REGEX}\\1(?!(@(?:[a-z\\d-]+\\.)+[a-z]{2,})|((?:(?!|[^<]*(<\\/pre>|<\\/code>))`,
                         'gi',
                     );
                     return this.modifyTextForUrlLinks(regex, textToProcess, replacement);

From ae43d20794699ff8bf19afa6cb1abaa48ae12237 Mon Sep 17 00:00:00 2001
From: Antasel 
Date: Mon, 21 Aug 2023 13:24:34 +0600
Subject: [PATCH 4/7] remove needless parts, refactor

---
 lib/CONST.jsx      | 2 +-
 lib/ExpensiMark.js | 4 ++--
 lib/Url.js         | 2 +-
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/lib/CONST.jsx b/lib/CONST.jsx
index d28b37f8..d9953ae5 100644
--- a/lib/CONST.jsx
+++ b/lib/CONST.jsx
@@ -1,6 +1,6 @@
 /* eslint-disable no-useless-escape */
 
-const EMAIL_BASE_REGEX = '(?=((?=[\\w\'#%+-]+(?:\\.[\\w\'#%+-]+)*@)[\\w\\.\'#%+-]{1,64}@(?:(?=[a-z\\d]+(?:-+[a-z\\d]+)*\\.)(?:[a-zA-Z\\d-]{1,63}\\.)+[a-zA-Z]{2,63})(?= |_|\\b))(?.*))\\S{3,254}(?=\\k$)';
+const EMAIL_BASE_REGEX = '(?=((?=[\\w\'#%+-]+(?:\\.[\\w\'#%+-]+)*@)[\\w\\.\'#%+-]{1,64}@(?:(?=[a-z\\d]+(?:-+[a-z\\d]+)*\\.)(?:[a-z\\d-]{1,63}\\.)+[a-z]{2,63})(?= |_|\\b))(?.*))\\S{3,254}(?=\\k$)';
 
 const MOMENT_FORMAT_STRING = 'YYYY-MM-DD';
 
diff --git a/lib/ExpensiMark.js b/lib/ExpensiMark.js
index d2545f4b..89f22d74 100644
--- a/lib/ExpensiMark.js
+++ b/lib/ExpensiMark.js
@@ -128,7 +128,7 @@ export default class ExpensiMark {
 
                 process: (textToProcess, replacement) => {
                     const regex = new RegExp(
-                        `(?![^<]*>|[^<>]*<\\/)([_*~]*?)${MARKDOWN_URL_REGEX}\\1(?!(@(?:[a-z\\d-]+\\.)+[a-z]{2,})|((?:(?!|[^<]*(<\\/pre>|<\\/code>))`,
+                        `(?![^<]*>|[^<>]*<\\/)([_*~]*?)${MARKDOWN_URL_REGEX}\\1(?!((?:(?!|[^<]*(<\\/pre>|<\\/code>))`,
                         'gi',
                     );
                     return this.modifyTextForUrlLinks(regex, textToProcess, replacement);
@@ -177,7 +177,7 @@ export default class ExpensiMark {
             {
                 name: 'autoEmail',
                 regex: new RegExp(
-                    `(?![^<]*>|[^<>]*<\\/(?!em))(?:(?<= |[^\\w'#%+-])|^|\\b)${CONST.REG_EXP.MARKDOWN_EMAIL}(?!((?:(?!|[^<]*(<\\/pre>|<\\/code>))`,
+                    `(?![^<]*>|[^<>]*<\\/(?!em))(?:(?<=[^\\w'#%+-])|^)${CONST.REG_EXP.MARKDOWN_EMAIL}(?!((?:(?!|[^<]*(<\\/pre>|<\\/code>))`,
                     'gim',
                 ),
                 replacement: '$1',
diff --git a/lib/Url.js b/lib/Url.js
index f8eb7e9e..12c53af1 100644
--- a/lib/Url.js
+++ b/lib/Url.js
@@ -2,7 +2,7 @@ import TLD_REGEX from './tlds';
 
 const ALLOWED_PORTS = '([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])';
 const URL_PROTOCOL_REGEX = '((ht|f)tps?:\\/\\/)';
-const URL_WEBSITE_REGEX = `${URL_PROTOCOL_REGEX}?((?:www\\.)?[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\\.)+(?:${TLD_REGEX})(?:\\:${ALLOWED_PORTS}|\\b|(?=_))`;
+const URL_WEBSITE_REGEX = `${URL_PROTOCOL_REGEX}?((?:www\\.)?[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\\.)+(?:${TLD_REGEX})(?:\\:${ALLOWED_PORTS}|\\b|(?=_))(?!@(?:[a-z\\d-]+\\.)+[a-z]{2,})`;
 const addEscapedChar = reg => `(?:${reg}|&(?:amp|quot|#x27);)`;
 const URL_PATH_REGEX = `(?:${addEscapedChar('[.,=(+$!*]')}?\\/${addEscapedChar('[-\\w$@.+!*:(),=%~]')}*${addEscapedChar('[-\\w~@:%)]')}|\\/)*`;
 const URL_PARAM_REGEX = `(?:\\?${addEscapedChar('[-\\w$@.+!*()\\/,=%{}:;\\[\\]\\|_|~]')}*)?`;

From 9d17f89b512c8bff7d0a804e3d795e6e2a86f840 Mon Sep 17 00:00:00 2001
From: Antasel 
Date: Mon, 21 Aug 2023 19:24:43 +0600
Subject: [PATCH 5/7] alternative instead of lookbehind

---
 lib/ExpensiMark.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/lib/ExpensiMark.js b/lib/ExpensiMark.js
index 89f22d74..bf588df6 100644
--- a/lib/ExpensiMark.js
+++ b/lib/ExpensiMark.js
@@ -177,10 +177,10 @@ export default class ExpensiMark {
             {
                 name: 'autoEmail',
                 regex: new RegExp(
-                    `(?![^<]*>|[^<>]*<\\/(?!em))(?:(?<=[^\\w'#%+-])|^)${CONST.REG_EXP.MARKDOWN_EMAIL}(?!((?:(?!|[^<]*(<\\/pre>|<\\/code>))`,
+                    `(?![^<]*>|[^<>]*<\\/(?!em))([^\\w'#%+-]|^|)${CONST.REG_EXP.MARKDOWN_EMAIL}(?!((?:(?!|[^<]*(<\\/pre>|<\\/code>))`,
                     'gim',
                 ),
-                replacement: '$1',
+                replacement: '$1$2',
             },
 
             {

From 0919b76f020fca4c5cf500674e3e3ffba52b247e Mon Sep 17 00:00:00 2001
From: Antasel 
Date: Tue, 22 Aug 2023 19:04:29 +0600
Subject: [PATCH 6/7] move heading1 after autoemail

---
 lib/ExpensiMark.js | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/lib/ExpensiMark.js b/lib/ExpensiMark.js
index bf588df6..6c6f5488 100644
--- a/lib/ExpensiMark.js
+++ b/lib/ExpensiMark.js
@@ -139,11 +139,6 @@ export default class ExpensiMark {
                     return `${g1}${g2}${g1}`;
                 },
             },
-            {
-                name: 'heading1',
-                regex: /^# +(?! )((?:(?!
|\n|\r\n).)+)/gm,
-                replacement: '

$1

', - }, { /** * Use \b in this case because it will match on words, letters, @@ -183,6 +178,11 @@ export default class ExpensiMark { replacement: '$1$2', }, + { + name: 'heading1', + regex: /^# +(?! )((?:(?!
|\n|\r\n).)+)/gm,
+                replacement: '

$1

', + }, { name: 'quote', From e5f7917345c3d0a090e1ae82a91b8aa668a54cf2 Mon Sep 17 00:00:00 2001 From: Antasel Date: Mon, 28 Aug 2023 12:10:17 +0600 Subject: [PATCH 7/7] restore a few invalid emails test cases --- __tests__/Str-test.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/__tests__/Str-test.js b/__tests__/Str-test.js index 5a1c04f7..c7f57d2f 100644 --- a/__tests__/Str-test.js +++ b/__tests__/Str-test.js @@ -90,7 +90,10 @@ describe('Str.sanitizeURL', () => { describe('Str.isValidEmail', () => { it('Correctly identifies valid emails', () => { expect(Str.isValidEmail('abc@gmail.com')).toBeTruthy(); - + expect(Str.isValidEmail('test@gmail')).toBeFalsy(); + expect(Str.isValidEmail('@gmail.com')).toBeFalsy(); + expect(Str.isValidEmail('usernamelongerthan64charactersshouldnotworkaccordingtorfc822whichisusedbyphp@gmail.com')).toBeFalsy(); + // Domain length (63 chars in each label) expect(Str.isValidEmail('test@asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcj.com')).toBeTruthy(); expect(Str.isValidEmail('abc@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.km')).toBeTruthy();