diff --git a/.codeclimate.yml b/.codeclimate.yml index 9ec7c3b..0b9e04c 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,13 +1,13 @@ engines: eslint: enabled: true - channel: "eslint-8" + channel: 'eslint-8' config: - config: ".eslintrc.yaml" + config: '.eslintrc.yaml' ratings: - paths: - - "**.js" + paths: + - '**.js' checks: method-complexity: diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 17d4c6a..035a400 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -2,6 +2,6 @@ env: node: true es6: true mocha: true - es2023: true + es2022: true -extends: ["@haraka"] +extends: ['@haraka'] diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fe287e6..af21ea8 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,7 +3,7 @@ version: 2 updates: - - package-ecosystem: "npm" - directory: "/" + - package-ecosystem: 'npm' + directory: '/' schedule: - interval: "weekly" + interval: 'weekly' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 752f845..7034b75 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,6 @@ name: CI -on: [ pull_request, push ] +on: [pull_request, push] env: CI: true @@ -14,9 +14,9 @@ jobs: secrets: inherit test: - needs: [ lint ] + needs: [lint] uses: haraka/.github/.github/workflows/ubuntu.yml@master windows: - needs: [ lint ] + needs: [lint] uses: haraka/.github/.github/workflows/windows.yml@master diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3627451..8314a66 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,10 +2,10 @@ name: CodeQL on: push: - branches: [ master ] + branches: [master] pull_request: # The branches below must be a subset of the branches above - branches: [ master ] + branches: [master] schedule: - cron: '18 7 * * 4' diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d489fbd..e81c15f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,4 +13,4 @@ env: jobs: publish: uses: haraka/.github/.github/workflows/publish.yml@master - secrets: inherit \ No newline at end of file + secrets: inherit diff --git a/.prettierrc.yml b/.prettierrc.yml new file mode 100644 index 0000000..8ded5e0 --- /dev/null +++ b/.prettierrc.yml @@ -0,0 +1,2 @@ +singleQuote: true +semi: false diff --git a/Changes.md b/CHANGELOG.md similarity index 99% rename from Changes.md rename to CHANGELOG.md index acbf76f..1113e0b 100644 --- a/Changes.md +++ b/CHANGELOG.md @@ -2,7 +2,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). - ### Unreleased ### [2.2.1] - 2024-04-08 @@ -11,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). - populate `[files]` in package.json. Delete .npmignore. - updated scripts{} in package.json - lint: remove duplicate / stale rules from .eslintrc +- prettier (except index) ### [2.2.0] - 2024-02-23 @@ -27,53 +27,44 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). - doc(README): add links to RFC 2822, 5322 #53 - doc(README): enabled syntax highlighting with code fences #51 - ### 2.1.0 - 2021-02-26 - make parse accept an options object as second argument - allow comma (,) in display name, default off [#52](https://github.com/haraka/node-address-rfc2822/pull/52) - ### 2.0.6 - 2020-11-17 - replace travis/appveyor CI tests with Github Actions [#48](https://github.com/haraka/node-address-rfc2822/pull/48) - test: when splitting lines, use os.EOL - allow @ symbol in display name [#47](https://github.com/haraka/node-address-rfc2822/pull/47) - ### 2.0.5 - 2020-06-02 - update email-addresses to 3.1.0 [#46](https://github.com/haraka/node-address-rfc2822/pull/46) - test framework: nodeunit -> mocha - ### 2.0.4 - 2018-06-29 - throw a proper error object, not a string. - ### 2.0.3 - 2018-03-01 - use es6 classes - export the Address class [#29](https://github.com/haraka/node-address-rfc2822/pull/29) - ### 2.0.2 - 2018-02-24 - Fix a possible regexp backtracking DoS [#28](https://github.com/haraka/node-address-rfc2822/pull/28) - ### 2.0.1 - 2017-06-26 - trim the line in parse() [#24](https://github.com/haraka/node-address-rfc2822/pull/24) - ### 1.0.2 - 2016-06-16 - updated for eslint 4 compat [#23](https://github.com/haraka/node-address-rfc2822/pull/23) - use email-addresses for parser [#20](https://github.com/haraka/node-address-rfc2822/pull/20) - ### 1.0.1 - 2016-09-23 - use native to[lower|upper]Case functions vs regex @@ -81,7 +72,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). - remove node 0.10, 5, add node 6 - throw error on nothing to parse - ### 1.0.0 - 2016-02-23 - Initial implementation diff --git a/README.md b/README.md index 815ec9a..11e70c7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ [![Code Climate][clim-img]][clim-url] [![Coverage Status][cov-img]][cov-url] - # address-rfc2822 Parser for RFC 2822 & 5322 (Header) format email addresses. @@ -18,16 +17,16 @@ It is almost a direct port of the perl module Mail::Address and I'm grateful to ## Usage ```js -const addrparser = require('address-rfc2822'); +const addrparser = require('address-rfc2822') -const addresses = addrparser.parse("Matt Sergeant "); -const address = addresses[0]; +const addresses = addrparser.parse('Matt Sergeant ') +const address = addresses[0] -console.log(`Email address: ${address.address}`); // helpme+npm@gmail.com -console.log(`Email name: ${address.name()}`); // Matt Sergeant -console.log(`Reformatted: ${address.format()}`); // Matt Sergeant -console.log(`User part: ${address.user()}`); // helpme+npm -console.log(`Host part: ${address.host()}`); // gmail.com +console.log(`Email address: ${address.address}`) // helpme+npm@gmail.com +console.log(`Email name: ${address.name()}`) // Matt Sergeant +console.log(`Reformatted: ${address.format()}`) // Matt Sergeant +console.log(`User part: ${address.user()}`) // helpme+npm +console.log(`Host part: ${address.host()}`) // gmail.com ``` ## More Info @@ -35,7 +34,6 @@ console.log(`Host part: ${address.host()}`); // gmail.com - [RFC 2822](https://tools.ietf.org/html/rfc2822) - [RFC 5322](https://tools.ietf.org/html/rfc5322) - ## License This module is MIT licensed. diff --git a/index.js b/index.js index 7d657c1..bfef9fa 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,6 @@ -'use strict'; +'use strict' -const ea_lib = require('email-addresses'); +const ea_lib = require('email-addresses') /** * @param {string} line string to parse @@ -12,252 +12,270 @@ const ea_lib = require('email-addresses'); * @throws {Error} if input string is empty * @throws {Error} if no addresses are found */ -exports.parse = function parse (line, opts = null) { - if (!line) throw new Error('Nothing to parse'); - - line = line.trim(); - - const defaultOpts = { - startAt: null, - allowAtInDisplayName: true, - allowCommaInDisplayName: false, - } - - const { startAt, allowAtInDisplayName, allowCommaInDisplayName } = typeof opts === 'object' - ? Object.assign({}, defaultOpts, opts) - : Object.assign({}, defaultOpts, { startAt: opts }) - - const addr = ea_lib({ - input: line, - rfc6532: true, // unicode - partial: false, // return failed parses - simple: false, // simple AST - strict: false, // turn off obs- features in the rfc - rejectTLD: false, // domains require a "." - startAt: startAt || null, - atInDisplayName: allowAtInDisplayName, - commaInDisplayName: allowCommaInDisplayName, - }); - - if (!addr) throw new Error('No results'); - - // console.log("Parsed to: ", require('util').inspect(addr, {depth: 10, colors: true})); - - return addr.addresses.map(map_addresses); +exports.parse = function parse(line, opts = null) { + if (!line) throw new Error('Nothing to parse') + + line = line.trim() + + const defaultOpts = { + startAt: null, + allowAtInDisplayName: true, + allowCommaInDisplayName: false, + } + + const { startAt, allowAtInDisplayName, allowCommaInDisplayName } = + typeof opts === 'object' + ? Object.assign({}, defaultOpts, opts) + : Object.assign({}, defaultOpts, { startAt: opts }) + + const addr = ea_lib({ + input: line, + rfc6532: true, // unicode + partial: false, // return failed parses + simple: false, // simple AST + strict: false, // turn off obs- features in the rfc + rejectTLD: false, // domains require a "." + startAt: startAt || null, + atInDisplayName: allowAtInDisplayName, + commaInDisplayName: allowCommaInDisplayName, + }) + + if (!addr) throw new Error('No results') + + // console.log("Parsed to: ", require('util').inspect(addr, {depth: 10, colors: true})); + + return addr.addresses.map(map_addresses) } -function map_addresses (adr) { - if (adr.type === 'group') { - return new Group(adr.name, adr.addresses.map(map_addresses)); - } - let comments; - if (adr.parts.comments) { - comments = adr.parts.comments.map(function (c) { return c.tokens.trim() }).join(' ').trim(); - // if (comments.length) { - // comments = '(' + comments + ')'; - // } - } - let l = adr.local; - if (!adr.name && /:/.test(l)) l = `"${ l }"`; - return new Address(adr.name, `${l }@${ adr.domain}`, comments); +function map_addresses(adr) { + if (adr.type === 'group') { + return new Group(adr.name, adr.addresses.map(map_addresses)) + } + let comments + if (adr.parts.comments) { + comments = adr.parts.comments + .map(function (c) { + return c.tokens.trim() + }) + .join(' ') + .trim() + // if (comments.length) { + // comments = '(' + comments + ')'; + // } + } + let l = adr.local + if (!adr.name && /:/.test(l)) l = `"${l}"` + return new Address(adr.name, `${l}@${adr.domain}`, comments) } exports.parseFrom = function (line) { - return exports.parse(line, 'from'); + return exports.parse(line, 'from') } exports.parseSender = function (line) { - return exports.parse(line, 'sender'); + return exports.parse(line, 'sender') } exports.parseReplyTo = function (line) { - return exports.parse(line, 'reply-to'); + return exports.parse(line, 'reply-to') } class Group { - constructor (display_name, addresses) { - this.phrase = display_name; - this.addresses = addresses; + constructor(display_name, addresses) { + this.phrase = display_name + this.addresses = addresses + } + + format() { + return `${this.phrase}:${this.addresses + .map(function (a) { + return a.format() + }) + .join(',')}` + } + + name() { + let phrase = this.phrase + + if (!(phrase && phrase.length)) phrase = this.comment + + const name = _extract_name(phrase) + return name + } +} + +class Address { + constructor(phrase, address, comment) { + this.phrase = phrase || '' + this.address = address || '' + this.comment = comment || '' + } + + host() { + const match = /.*@(.*)$/.exec(this.address) + if (!match) return null + return match[1] + } + + user() { + const match = /^(.*)@/.exec(this.address) + if (!match) return null + return match[1] + } + + format() { + const phrase = this.phrase + const email = this.address + let comment = this.comment + + const addr = [] + const atext = new RegExp("^[\\-\\w !#$%&'*+/=?^`{|}~]+$") + + if (phrase && phrase.length) { + addr.push( + atext.test(phrase.trim()) + ? phrase + : _quote_no_esc(phrase) + ? phrase + : `"${phrase}"`, + ) + + if (email && email.length) { + addr.push(`<${email}>`) + } + } else if (email && email.length) { + addr.push(email) } - format () { - return `${this.phrase }:${ this.addresses.map(function (a) { return a.format() }).join(',')}`; + if (comment && /\S/.test(comment)) { + comment = comment.replace(/^\s*\(?/, '(').replace(/\)?\s*$/, ')') } - name () { - let phrase = this.phrase; + if (comment && comment.length) { + addr.push(comment) + } - if (!(phrase && phrase.length)) phrase = this.comment; + return addr.join(' ') + } - const name = _extract_name(phrase); - return name; - } -} + name() { + let phrase = this.phrase + const addr = this.address -class Address { - constructor (phrase, address, comment) { - this.phrase = phrase || ''; - this.address = address || ''; - this.comment = comment || ''; + if (!(phrase && phrase.length)) { + phrase = this.comment } - host () { - const match = /.*@(.*)$/.exec(this.address); - if (!match) return null; - return match[1]; - } + let name = _extract_name(phrase) - user () { - const match = /^(.*)@/.exec(this.address); - if (!match) return null; - return match[1]; + // first.last@domain address + if (name === '') { + const match = /([^%.@_]+([._][^%.@_]+)+)[@%]/.exec(addr) + if (match) { + name = match[1].replace(/[._]+/g, ' ') + name = _extract_name(name) + } } - format () { - const phrase = this.phrase; - const email = this.address; - let comment = this.comment; - - const addr = []; - const atext = new RegExp('^[\\-\\w !#$%&\'*+/=?^`{|}~]+$'); - - if (phrase && phrase.length) { - addr.push(atext.test(phrase.trim()) ? phrase - : _quote_no_esc(phrase) ? phrase - : (`"${phrase}"`)); - - if (email && email.length) { - addr.push(`<${email}>`); - } - } - else if (email && email.length) { - addr.push(email); - } - - if (comment && /\S/.test(comment)) { - comment = comment.replace(/^\s*\(?/, '(') - .replace(/\)?\s*$/, ')'); - } - - if (comment && comment.length) { - addr.push(comment); - } - - return addr.join(' '); + if (name === '' && /\/g=/i.test(addr)) { + // X400 style address + let match = /\/g=([^/]*)/i.exec(addr) + const f = match[1] + match = /\/s=([^/]*)/i.exec(addr) + const l = match[1] + name = _extract_name(`${f} ${l}`) } - name () { - let phrase = this.phrase; - const addr = this.address; - - if (!(phrase && phrase.length)) { - phrase = this.comment; - } - - let name = _extract_name(phrase); - - // first.last@domain address - if (name === '') { - const match = /([^%.@_]+([._][^%.@_]+)+)[@%]/.exec(addr); - if (match) { - name = match[1].replace(/[._]+/g, ' '); - name = _extract_name(name); - } - } - - if (name === '' && /\/g=/i.test(addr)) { // X400 style address - let match = /\/g=([^/]*)/i.exec(addr); - const f = match[1]; - match = /\/s=([^/]*)/i.exec(addr); - const l = match[1]; - name = _extract_name(`${f} ${l}`); - } - - return name; - } + return name + } } -exports.Address = Address; +exports.Address = Address // This is because JS regexps have no equivalent of // zero-width negative look-behind assertion for: /(? { - const lines = rows.split(EOLRE); - // console.log(lines) - if (lines[0] === '') lines.shift(); - return lines.filter((l) => { return !/^#/.test(l) }); -}); + const lines = rows.split(EOLRE) + // console.log(lines) + if (lines[0] === '') lines.shift() + return lines.filter((l) => { + return !/^#/.test(l) + }) +}) describe('parse', function () { - - it('throws on empty line', function () { - assert.throws(() => { parse(''); }, { message: 'Nothing to parse' }) - }) - - tests.forEach(function (test) { - - it(test[0], function () { - - const details = {}; - details.format = test[1]; - if (test[2]) details.name = test[2]; - - const parsed = parse(test[0])[0]; - // console.log(`Parsed: ${parsed}`); - - for (const k in details) { - assert.equal(parsed[k](), details[k], `Test '${k}' for '${parsed[k]()}' = '${details[k]}' from ${JSON.stringify(parsed)}`); - } - }) + it('throws on empty line', function () { + assert.throws( + () => { + parse('') + }, + { message: 'Nothing to parse' }, + ) + }) + + tests.forEach(function (test) { + it(test[0], function () { + const details = {} + details.format = test[1] + if (test[2]) details.name = test[2] + + const parsed = parse(test[0])[0] + // console.log(`Parsed: ${parsed}`); + + for (const k in details) { + assert.equal( + parsed[k](), + details[k], + `Test '${k}' for '${parsed[k]()}' = '${details[k]}' from ${JSON.stringify(parsed)}`, + ) + } }) + }) }) diff --git a/test/functions.js b/test/functions.js index e87e9f9..8736c9a 100644 --- a/test/functions.js +++ b/test/functions.js @@ -1,117 +1,120 @@ -const assert = require('assert') +const assert = require('assert') -const address = require('../index'); +const address = require('../index') describe('isAllLower', function () { - it('lower latin string', function (done) { - assert.equal(true, address.isAllLower('abcdefg')); - done(); - }) + it('lower latin string', function (done) { + assert.equal(true, address.isAllLower('abcdefg')) + done() + }) }) describe('isAllUpper', function () { - it('upper latin string', function (done) { - assert.equal(true, address.isAllUpper('ABCDEFG')); - done(); - }) + it('upper latin string', function (done) { + assert.equal(true, address.isAllUpper('ABCDEFG')) + done() + }) }) describe('nameCase', function () { - it('john doe -> John Doe', function (done) { - assert.equal('John Doe', address.nameCase('john doe')); - done(); - }) - it('JANE SMITH -> Jane Smith' , function (done) { - assert.equal('Jane Smith', address.nameCase('JANE SMITH')); - done(); - }) - it('marty mcleod -> Marty McLeod', function (done) { - assert.equal('Marty McLeod', address.nameCase('marty mcleod')); - done(); - }) - it('martin o\'mally -> Martin O\'Malley', function (done) { - assert.equal("Martin O'Malley", address.nameCase("martin o'malley")); - done(); - }) - it('level iii support -> Level III Support', function (done) { - assert.equal("Level III Support", address.nameCase("level iii support")); - done(); - }) + it('john doe -> John Doe', function (done) { + assert.equal('John Doe', address.nameCase('john doe')) + done() + }) + it('JANE SMITH -> Jane Smith', function (done) { + assert.equal('Jane Smith', address.nameCase('JANE SMITH')) + done() + }) + it('marty mcleod -> Marty McLeod', function (done) { + assert.equal('Marty McLeod', address.nameCase('marty mcleod')) + done() + }) + it("martin o'mally -> Martin O'Malley", function (done) { + assert.equal("Martin O'Malley", address.nameCase("martin o'malley")) + done() + }) + it('level iii support -> Level III Support', function (done) { + assert.equal('Level III Support', address.nameCase('level iii support')) + done() + }) }) describe('parseFrom', function () { - it('Travis CI ', function (done) { - try { - const r = address.parseFrom('Travis CI '); - assert.equal(r[0].address, 'builds@travis-ci.org'); - // console.log(r); - } - catch (e) { - console.error(e); - } - done(); - }) - it('root (Cron Daemon)', function (done) { - try { - const r = address.parseFrom('root (Cron Daemon)'); - assert.equal(r[0].address, ''); - // console.log(r); - } - catch (e) { - assert.equal(e.message, 'No results'); - } - done(); - }) + it('Travis CI ', function (done) { + try { + const r = address.parseFrom('Travis CI ') + assert.equal(r[0].address, 'builds@travis-ci.org') + // console.log(r); + } catch (e) { + console.error(e) + } + done() + }) + it('root (Cron Daemon)', function (done) { + try { + const r = address.parseFrom('root (Cron Daemon)') + assert.equal(r[0].address, '') + // console.log(r); + } catch (e) { + assert.equal(e.message, 'No results') + } + done() + }) }) describe('parseSender', function () { - it('"Anne Standley, PMPM" ', function (done) { - try { - const r = address.parseSender('"Anne Standley, PMPM" '); - assert.equal(r[0].address, 'info=protectmypublicmedia.org@mail172.atl101.mcdlv.net'); - // console.log(r); - } - catch (e) { - console.error(e); - } - done(); - }) + it('"Anne Standley, PMPM" ', function (done) { + try { + const r = address.parseSender( + '"Anne Standley, PMPM" ', + ) + assert.equal( + r[0].address, + 'info=protectmypublicmedia.org@mail172.atl101.mcdlv.net', + ) + // console.log(r); + } catch (e) { + console.error(e) + } + done() + }) }) describe('parseReplyTo', function () { - it('=?utf-8?Q?Anne=20Standley=2C=20Protect=20My=20Public=20Media?= ', function (done) { - try { - const r = address.parseReplyTo('=?utf-8?Q?Anne=20Standley=2C=20Protect=20My=20Public=20Media?= '); - assert.equal(r[0].address, 'info@protectmypublicmedia.org'); - // console.log(r); - } - catch (e) { - console.error(e); - } - done(); - }) + it('=?utf-8?Q?Anne=20Standley=2C=20Protect=20My=20Public=20Media?= ', function (done) { + try { + const r = address.parseReplyTo( + '=?utf-8?Q?Anne=20Standley=2C=20Protect=20My=20Public=20Media?= ', + ) + assert.equal(r[0].address, 'info@protectmypublicmedia.org') + // console.log(r); + } catch (e) { + console.error(e) + } + done() + }) }) describe('parse with options', function () { - it('should not allow parsing display name with comma by default', function (done) { - try { - address.parse('Foo, Bar '); - } - catch (e) { - assert.equal(e.message, 'No results'); - } - done(); - }) + it('should not allow parsing display name with comma by default', function (done) { + try { + address.parse('Foo, Bar ') + } catch (e) { + assert.equal(e.message, 'No results') + } + done() + }) - it('should allow parsing display name with comma', function (done) { - try { - const [r] = address.parse('Foo, Bar ', { allowCommaInDisplayName: true }); - assert.equal('foo@example.com', r.address); - assert.equal('Foo, Bar', r.phrase); - } - catch (e) { - console.error(e); - } - done(); - }) + it('should allow parsing display name with comma', function (done) { + try { + const [r] = address.parse('Foo, Bar ', { + allowCommaInDisplayName: true, + }) + assert.equal('foo@example.com', r.address) + assert.equal('Foo, Bar', r.phrase) + } catch (e) { + console.error(e) + } + done() + }) })