parse-email-address
Version:
Parse/validate email address with RFC-5321 and sane size limits.
106 lines (105 loc) • 4.56 kB
JavaScript
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/**
* This is largely copied from
* https://github.com/gene-hightower/smtp-address-parser/blob/75e0f93837cc302c122cfd3f9a6d9a49f9dd56a3/lib/index.ts
* which has the following license:
*
* MIT License
*
* Copyright (c) 2021 Gene Hightower
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* It has been modified to have proper imports.
*/
import * as rawNearley from './nearley.js';
import myGrammar from './grammar.js';
const { Grammar, Parser } = rawNearley;
myGrammar.ParserStart = 'Mailbox';
const grammar = Grammar.fromCompiled(myGrammar);
// <https://tools.ietf.org/html/rfc5321#section-4.1.2>
export function parse(address) {
// An insane length, to protect the parsing code from huge input. SMTP line limit, minus command size.
const insane_length = 1000 - 'MAIL FROM:<>\r\n'.length;
if (address.length > insane_length) {
throw new Error('address too long');
}
const parser = new Parser(grammar);
parser.feed(address);
if (parser.results.length !== 1) {
throw new Error('address parsing failed: ambiguous grammar');
}
// Domain checks
const at_idx = address.lastIndexOf('@'); // must be found, since parse was successful
const domain = address.slice(Math.max(0, at_idx + 1));
if (domain[0] !== '[') {
// Not an address literal
if (domain.length > 253) {
throw new Error('domain too long');
}
const labels = domain.split('.');
if (labels.length < 2) {
throw new Error('domain not fully qualified');
}
if (labels[labels.length - 1].length < 2) {
throw new Error('top level domain label too short');
}
labels.sort(function (a, b) {
return b.length - a.length;
});
if (labels[0].length > 63) {
throw new Error('domain label too long');
}
}
return parser.results[0];
}
/** Strip +something, strip '.'s, and map to lower case. */
export function normalize_dot_string(dot_string) {
const noTag = (function () {
const plus_loc = dot_string.indexOf('+');
if (plus_loc === -1) {
return dot_string;
}
return dot_string.slice(0, Math.max(0, plus_loc));
})();
const noDot = noTag.replace(/\./g, '');
return noDot.toLowerCase();
}
/** The G style address normalization. */
export function normalize(address) {
const a = parse(address);
const domain = a.domainPart.AddressLiteral ?? a.domainPart.DomainName.toLowerCase();
const local = a.localPart.QuotedString ?? normalize_dot_string(a.localPart.DotString);
return `${local}@${domain}`;
}
export function canonicalize_quoted_string(quoted_string) {
const unquoted = quoted_string.slice(1).slice(0, Math.max(0, quoted_string.length - 2));
const unescaped = unquoted.replace(/(?:\\(.))/g, '$1');
const reEscaped = unescaped.replace(/(?:(["\\]))/g, String.raw `\$1`);
return `"${reEscaped}"`; // re-quote
}
/** Apply a canonicalization consistent with standards to support comparison as a string. */
export function canonicalize(address) {
const a = parse(address);
const domain = a.domainPart.AddressLiteral ?? a.domainPart.DomainName.toLowerCase();
const local = a.localPart.QuotedString
? canonicalize_quoted_string(a.localPart.QuotedString)
: a.localPart.DotString;
return `${local}@${domain}`;
}