diff --git a/__tests__/ExpensiMark-HTML-test.js b/__tests__/ExpensiMark-HTML-test.js index 7f0ba5a3..a98e35aa 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,108 @@ 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__ ' + + '__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); +}); + +// Check email regex's validity at various limits +test('Test markdown replacement for valid emails', () => { + 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 ' + + '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.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 ' + + '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)'; @@ -524,7 +614,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 [] @@ -542,7 +631,6 @@ test('Test markdown style email link with various styles', () => { + 'Expensify ' + 'Expensify ' + 'Expensify! ' - + 'Expensify? ' + 'Applause ' + '[](concierge@expensify.com)' + '[ ](concierge@expensify.com)' @@ -568,14 +656,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 579961db..c7f57d2f 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'); @@ -97,10 +88,83 @@ describe('Str.sanitizeURL', () => { }); 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..d9953ae5 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-z\\d-]{1,63}\\.)+[a-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 d9fbc560..0c34246f 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. @@ -153,6 +139,45 @@ export default class ExpensiMark { return `${g1}${g2}${g1}`; }, }, + { + /** + * Use \b in this case because it will match on words, letters, + * and _: https://www.rexegg.com/regex-boundaries.html#wordboundary + * The !_blank is to prevent the `target="_blank">` section of the + * link replacement from being captured Additionally, something like + * `\b\_([^<>]*?)\_\b` doesn't work because it won't replace + * `_https://www.test.com_` + * Use [\s\S]* instead of .* to match newline + */ + 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 unless textWithinUnderscores starts with valid email + 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}`;
+                },
+            },
+
+            /**
+             * 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'#%+-]|^|)${CONST.REG_EXP.MARKDOWN_EMAIL}(?!((?:(?!|[^<]*(<\\/pre>|<\\/code>))`,
+                    'gim',
+                ),
+                replacement: '$1$2',
+            },
+
             {
                 name: 'heading1',
                 regex: /^# +(?! )((?:(?!
|\n|\r\n).)+)/gm,
@@ -175,26 +200,6 @@ export default class ExpensiMark {
                     return `
${replacedText}
`; }, }, - { - /** - * Use \b in this case because it will match on words, letters, - * and _: https://www.rexegg.com/regex-boundaries.html#wordboundary - * The !_blank is to prevent the `target="_blank">` section of the - * link replacement from being captured Additionally, something like - * `\b\_([^<>]*?)\_\b` doesn't work because it won't replace - * `_https://www.test.com_` - * Use [\s\S]* instead of .* to match newline - */ - 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 - replacement: (match, extraLeadingUnderscores, textWithinUnderscores) => ( - textWithinUnderscores.includes('
') || this.containsNonPairTag(textWithinUnderscores)
-                        ? match
-                        : `${extraLeadingUnderscores}${textWithinUnderscores}`
-                ),
-            },
             {
                 // Use \B in this case because \b doesn't match * or ~.
                 // \B will match everything that \b doesn't, so it works
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$@.+!*()\\/,=%{}:;\\[\\]\\|_|~]')}*)?`;