diff --git a/defaults/preferences/prefs.js b/defaults/preferences/prefs.js index 94ad1556..91ff9ce0 100644 --- a/defaults/preferences/prefs.js +++ b/defaults/preferences/prefs.js @@ -18,6 +18,8 @@ pref("extensions.dkim_verifier.key.storing", 0); pref("extensions.dkim_verifier.saveResult", false); pref("extensions.dkim_verifier.arh.read", false); +pref("extensions.dkim_verifier.internationalized.enable", false); + //////////////////////////////////////////////////////////////////////////////// // general preferences - DNS //////////////////////////////////////////////////////////////////////////////// diff --git a/modules/ARHParser.jsm.js b/modules/ARHParser.jsm.js index 4a43989a..dafc78ca 100644 --- a/modules/ARHParser.jsm.js +++ b/modules/ARHParser.jsm.js @@ -15,7 +15,7 @@ // options for ESLint /* global Components, Services */ -/* global Logging, DKIM_Error */ +/* global Logging, rfcParser, DKIM_Error */ /* exported EXPORTED_SYMBOLS, ARHParser */ "use strict"; @@ -37,70 +37,13 @@ Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://dkim_verifier/logging.jsm.js"); Cu.import("resource://dkim_verifier/helper.jsm.js"); - +Cu.import("resource://dkim_verifier/rfcParser.jsm.js"); // @ts-ignore var prefs = Services.prefs.getBranch(PREF_BRANCH); // @ts-ignore const log = Logging.getLogger("ARHParser"); - -// WSP as specified in Appendix B.1 of RFC 5234 -const WSP_p = "[ \t]"; -// VCHAR as specified in Appendix B.1 of RFC 5234 -const VCHAR_p = "[!-~]"; -// Let-dig as specified in Section 4.1.2 of RFC 5321 [SMTP]. -const Let_dig_p = "[A-Za-z0-9]"; -// Ldh-str as specified in Section 4.1.2 of RFC 5321 [SMTP]. -const Ldh_str_p = `(?:[A-Za-z0-9-]*${Let_dig_p})`; -// "Keyword" as specified in Section 4.1.2 of RFC 5321 [SMTP]. -const Keyword_p = Ldh_str_p; -// sub-domain as specified in Section 4.1.2 of RFC 5321 [SMTP]. -const sub_domain_p = `(?:${Let_dig_p}${Ldh_str_p}?)`; -// obs-FWS as specified in Section 4.2 of RFC 5322 -const obs_FWS_p = `(?:${WSP_p}+(?:\r\n${WSP_p}+)*)`; -// quoted-pair as specified in Section 3.2.1 of RFC 5322 -// Note: obs-qp is not included, so this pattern matches less then specified! -const quoted_pair_p = `(?:\\\\(?:${VCHAR_p}|${WSP_p}))`; -// FWS as specified in Section 3.2.2 of RFC 5322 -const FWS_p = `(?:(?:(?:${WSP_p}*\r\n)?${WSP_p}+)|${obs_FWS_p})`; -const FWS_op = `${FWS_p}?`; -// ctext as specified in Section 3.2.2 of RFC 5322 -const ctext_p = "[!-'*-[\\]-~]"; -// ccontent as specified in Section 3.2.2 of RFC 5322 -// Note: comment is not included, so this pattern matches less then specified! -const ccontent_p = `(?:${ctext_p}|${quoted_pair_p})`; -// comment as specified in Section 3.2.2 of RFC 5322 -const comment_p = `\\((?:${FWS_op}${ccontent_p})*${FWS_op}\\)`; -// CFWS as specified in Section 3.2.2 of RFC 5322 [MAIL] -const CFWS_p = `(?:(?:(?:${FWS_op}${comment_p})+${FWS_op})|${FWS_p})`; -const CFWS_op = `${CFWS_p}?`; -// atext as specified in Section 3.2.3 of RFC 5322 -const atext_p = "[!#-'*-+/-9=?A-Z^-~-]"; -// dot-atom-text as specified in Section 3.2.3 of RFC 5322 -const dot_atom_text_p = `(?:${atext_p}+(?:\\.${atext_p}+)*)`; -// dot-atom as specified in Section 3.2.3 of RFC 5322 -// dot-atom = [CFWS] dot-atom-text [CFWS] -const dot_atom_p = `(?:${CFWS_op}${dot_atom_text_p}${CFWS_op})`; -// qtext as specified in Section 3.2.4 of RFC 5322 -// Note: obs-qtext is not included, so this pattern matches less then specified! -const qtext_p = "[!#-[\\]-~]"; -// qcontent as specified in Section 3.2.4 of RFC 5322 -const qcontent_p = `(?:${qtext_p}|${quoted_pair_p})`; -// quoted-string as specified in Section 3.2.4 of RFC 5322 -const quoted_string_p = `(?:${CFWS_op}"(?:${FWS_op}${qcontent_p})*${FWS_op}"${CFWS_op})`; -const quoted_string_cp = `(?:${CFWS_op}"((?:${FWS_op}${qcontent_p})*)${FWS_op}"${CFWS_op})`; -// local-part as specified in Section 3.4.1 of RFC 5322 -// Note: obs-local-part is not included, so this pattern matches less then specified! -const local_part_p = `(?:${dot_atom_p}|${quoted_string_p})`; -// token as specified in Section 5.1 of RFC 2045. -const token_p = "[^ \\x00-\\x1F\\x7F()<>@,;:\\\\\"/[\\]?=]+"; -// "value" as specified in Section 5.1 of RFC 2045. -const value_cp = `(?:(${token_p})|${quoted_string_cp})`; -// domain-name as specified in Section 3.5 of RFC 6376 [DKIM]. -const domain_name_p = `(?:${sub_domain_p}(?:\\.${sub_domain_p})+)`; - - /** * @typedef {Object} ARHHeader * @property {String} authserv_id @@ -137,7 +80,7 @@ let ARHParser = { parse: function _ARHParser_parse(authresHeader) { // remove header name authresHeader = authresHeader.replace( - new RegExp(`^Authentication-Results:${CFWS_op}`, "i"), ""); + new RegExp(`^Authentication-Results:${rfcParser.get("CFWS_op")}`, "i"), ""); let authresHeaderRef = new RefString(authresHeader); /** @type {ARHHeader} */ @@ -146,7 +89,7 @@ let ARHParser = { let reg_match; // get authserv-id and authres-version - reg_match = match(authresHeaderRef, `${value_cp}(?:${CFWS_p}([0-9]+)${CFWS_op})?`); + reg_match = match(authresHeaderRef, `${rfcParser.get("value_cp")}(?:${rfcParser.get("CFWS")}([0-9]+)${rfcParser.get("CFWS_op")})?`); const authserv_id = reg_match[1] || reg_match[2]; if (!authserv_id) { throw new DKIM_Error("Error matching the ARH authserv-id."); @@ -159,7 +102,7 @@ let ARHParser = { } // check if message authentication was performed - reg_match = match_o(authresHeaderRef, `;${CFWS_op}?none`); + reg_match = match_o(authresHeaderRef, `;${rfcParser.get("CFWS_op")}?none`); if (reg_match !== null) { log.debug("no-result"); return res; @@ -193,10 +136,10 @@ function parseResinfo(str) { let res = {}; // get methodspec - const method_version_p = `${CFWS_op}/${CFWS_op}([0-9]+)`; - const method_p = `(${Keyword_p})(?:${method_version_p})?`; - const result_p = `=${CFWS_op}(${Keyword_p})`; - const methodspec_p = `;${CFWS_op}${method_p}${CFWS_op}${result_p}`; + const method_version_p = `${rfcParser.get("CFWS_op")}/${rfcParser.get("CFWS_op")}([0-9]+)`; + const method_p = `(${rfcParser.get("Keyword")})(?:${method_version_p})?`; + const result_p = `=${rfcParser.get("CFWS_op")}(${rfcParser.get("Keyword")})`; + const methodspec_p = `;${rfcParser.get("CFWS_op")}${method_p}${rfcParser.get("CFWS_op")}${result_p}`; try { reg_match = match(str, methodspec_p); } catch (exception) { @@ -227,21 +170,21 @@ function parseResinfo(str) { checkResultKeyword(res.method, reg_match[3]); // get reasonspec (optional) - const reasonspec_p = `reason${CFWS_op}=${CFWS_op}${value_cp}`; + const reasonspec_p = `reason${rfcParser.get("CFWS_op")}=${rfcParser.get("CFWS_op")}${rfcParser.get("value_cp")}`; reg_match = match_o(str, reasonspec_p); if (reg_match !== null) { res.reason = reg_match[1] || reg_match[2]; } // get propspec (optional) - let pvalue_p = `${value_cp}|((?:${local_part_p}?@)?${domain_name_p})`; + let pvalue_p = `${rfcParser.get("value_cp")}|((?:${rfcParser.get("local_part")}?@)?${rfcParser.get("domain_name")})`; if (prefs.getBoolPref("relaxedParsing")) { // allow "/" and ":" in properties, even if it is not in a quoted-string pvalue_p += "|([^ \\x00-\\x1F\\x7F()<>@,;\\\\\"[\\]?=]+)"; } const special_smtp_verb_p = "mailfrom|rcptto"; - const property_p = `${special_smtp_verb_p}|${Keyword_p}`; - const propspec_p = `(${Keyword_p})${CFWS_op}\\.${CFWS_op}(${property_p})${CFWS_op}=${CFWS_op}(?:${pvalue_p})`; + const property_p = `${special_smtp_verb_p}|${rfcParser.get("Keyword")}`; + const propspec_p = `(${rfcParser.get("Keyword")})${rfcParser.get("CFWS_op")}\\.${rfcParser.get("CFWS_op")}(${property_p})${rfcParser.get("CFWS_op")}=${rfcParser.get("CFWS_op")}(?:${pvalue_p})`; res.propertys = {}; res.propertys.smtp = {}; res.propertys.header = {}; @@ -368,7 +311,7 @@ function match(str, pattern) { * an Array, containing the matches */ function match_o(str, pattern) { - const regexp = new RegExp(`^${CFWS_op}(?:${pattern})(?:(?:${CFWS_op}\r\n$)|(?=;)|(?=${CFWS_p}))`); + const regexp = new RegExp(`^${rfcParser.get("CFWS_op")}(?:${pattern})(?:(?:${rfcParser.get("CFWS_op")}\r\n$)|(?=;)|(?=${rfcParser.get("CFWS")}))`); const reg_match = str.match(regexp); if (reg_match === null || !reg_match[0]) { return null; diff --git a/modules/bimi.jsm.js b/modules/bimi.jsm.js index 11c19328..d19fb74d 100644 --- a/modules/bimi.jsm.js +++ b/modules/bimi.jsm.js @@ -18,7 +18,7 @@ // options for ESLint /* global Components */ -/* global Logging */ +/* global Logging, rfcParser */ /* exported EXPORTED_SYMBOLS, BIMI */ "use strict"; @@ -32,18 +32,12 @@ const Cu = Components.utils; Cu.import("resource://dkim_verifier/logging.jsm.js"); Cu.import("resource://dkim_verifier/ARHParser.jsm.js"); +Cu.import("resource://dkim_verifier/rfcParser.jsm.js"); let BIMI = (function() { const log = Logging.getLogger("BIMI"); - // WSP as specified in Appendix B.1 of RFC 5234 - const WSP_p = "[ \t]"; - // obs-FWS as specified in Section 4.2 of RFC 5322 - const obs_FWS_p = `(?:${WSP_p}+(?:\r\n${WSP_p}+)*)`; - // FWS as specified in Section 3.2.2 of RFC 5322 - const FWS_p = `(?:(?:(?:${WSP_p}*\r\n)?${WSP_p}+)|${obs_FWS_p})`; - let that = { /** * Try to get the BIMI Indicator if available. @@ -87,7 +81,7 @@ let BIMI = (function() { // Remove header name and new line at end bimiIndicator = bimiIndicator.slice("bimi-indicator:".length, -"\r\n".length); // Remove all whitespace - bimiIndicator = bimiIndicator.replace(new RegExp(`${FWS_p}`, "g"), ""); + bimiIndicator = bimiIndicator.replace(new RegExp(`${rfcParser.get("FWS")}`, "g"), ""); return bimiIndicator; } diff --git a/modules/dkimDMARC.jsm.js b/modules/dkimDMARC.jsm.js index 609e4453..90da72e5 100644 --- a/modules/dkimDMARC.jsm.js +++ b/modules/dkimDMARC.jsm.js @@ -18,8 +18,8 @@ // options for ESLint /* eslint strict: ["warn", "function"] */ -/* global Components, Services, XPCOMUtils */ -/* global Logging, Verifier, DNS */ +/* global Components, Services */ +/* global Logging, DNS, rfcParser */ /* global getBaseDomainFromAddr, getDomainFromAddr, toType, DKIM_TempError, DKIM_Error */ /* exported EXPORTED_SYMBOLS, DMARC */ @@ -34,19 +34,11 @@ var EXPORTED_SYMBOLS = [ const Cu = Components.utils; Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://dkim_verifier/logging.jsm.js"); Cu.import("resource://dkim_verifier/helper.jsm.js"); Cu.import("resource://dkim_verifier/DNSWrapper.jsm.js"); - -XPCOMUtils.defineLazyModuleGetter( - this, - "Verifier", - "resource://dkim_verifier/dkimVerifier.jsm.js" -); - - +Cu.import("resource://dkim_verifier/rfcParser.jsm.js"); /** * @public @@ -316,14 +308,14 @@ function parseDMARCRecord(DMARCRecordStr) { }; // parse tag-value list - let parsedTagMap = Verifier.parseTagValueList(DMARCRecordStr); + let parsedTagMap = rfcParser.parseTagValueList(DMARCRecordStr); if (parsedTagMap === -1) { throw new DKIM_Error("DKIM_DMARCERROR_ILLFORMED_TAGSPEC"); } else if (parsedTagMap === -2) { throw new DKIM_Error("DKIM_DMARCERROR_DUPLICATE_TAG"); } if (!(toType(parsedTagMap) === "Map")) { - throw new DKIM_Error(`unexpected return value from Verifier.parseTagValueList: ${parsedTagMap}`); + throw new DKIM_Error(`unexpected return value from rfcParser.parseTagValueList: ${parsedTagMap}`); } /** @type {Map} */ // @ts-ignore @@ -334,7 +326,7 @@ function parseDMARCRecord(DMARCRecordStr) { // of this tag MUST match precisely; if it does not or it is absent, // the entire retrieved record MUST be ignored. It MUST be the first // tag in the list. - let versionTag = Verifier.parseTagValue(tagMap, "v", "DMARC1", 3); + let versionTag = rfcParser.parseTagValue(tagMap, "v", "DMARC1", 3); if (versionTag === null) { throw new DKIM_Error("DKIM_DMARCERROR_MISSING_V"); } else { @@ -344,7 +336,7 @@ function parseDMARCRecord(DMARCRecordStr) { // adkim: (plain-text; OPTIONAL, default is "r".) Indicates whether // strict or relaxed DKIM identifier alignment mode is required by // the Domain Owner. - let adkimTag = Verifier.parseTagValue(tagMap, "adkim", "[rs]", 3); + let adkimTag = rfcParser.parseTagValue(tagMap, "adkim", "[rs]", 3); if (adkimTag === null || versionTag[0] === "DMARC1") { dmarcRecord.adkim = "r"; } else { @@ -369,7 +361,7 @@ function parseDMARCRecord(DMARCRecordStr) { // email that fails the DMARC mechanism check. Rejection SHOULD // occur during the SMTP transaction. See Section 15.4 for some // discussion of SMTP rejection methods and their implications. - let pTag = Verifier.parseTagValue(tagMap, "p", "(?:none|quarantine|reject)", 3); + let pTag = rfcParser.parseTagValue(tagMap, "p", "(?:none|quarantine|reject)", 3); if (pTag === null) { throw new DKIM_Error("DKIM_DMARCERROR_MISSING_P"); } else { @@ -392,7 +384,7 @@ function parseDMARCRecord(DMARCRecordStr) { // selected = true // else // selected = false - let pctTag = Verifier.parseTagValue(tagMap, "pct", "[0-9]{1,3}", 3); + let pctTag = rfcParser.parseTagValue(tagMap, "pct", "[0-9]{1,3}", 3); if (pctTag === null) { dmarcRecord.pct = 100; } else { @@ -411,7 +403,7 @@ function parseDMARCRecord(DMARCRecordStr) { // Note that "sp" will be ignored for DMARC records published on sub- // domains of Organizational Domains due to the effect of the DMARC // Policy Discovery mechanism described in Section 8. - let spTag = Verifier.parseTagValue(tagMap, "sp", "(?:none|quarantine|reject)", 3); + let spTag = rfcParser.parseTagValue(tagMap, "sp", "(?:none|quarantine|reject)", 3); if (spTag !== null) { dmarcRecord.sp = spTag[0]; } diff --git a/modules/dkimVerifier.jsm.js b/modules/dkimVerifier.jsm.js index 6e093708..06487859 100644 --- a/modules/dkimVerifier.jsm.js +++ b/modules/dkimVerifier.jsm.js @@ -27,8 +27,8 @@ // options for ESLint /* eslint strict: ["warn", "function"] */ /* global Components, Services */ -/* global Logging, Key, Policy, MsgReader */ -/* global dkimStrings, addrIsInDomain2, domainIsInDomain, stringEndsWith, stringEqual, writeStringToTmpFile, DKIM_SigError, DKIM_TempError, DKIM_Error */ +/* global Logging, Key, Policy, MsgReader, rfcParser */ +/* global dkimStrings, addrIsInDomain2, domainIsInDomain, stringEndsWith, stringEqual, writeStringToTmpFile, toType, DKIM_SigError, DKIM_TempError, DKIM_Error */ /* exported EXPORTED_SYMBOLS, Verifier */ // @ts-ignore @@ -52,6 +52,7 @@ Cu.import("resource://dkim_verifier/helper.jsm.js"); Cu.import("resource://dkim_verifier/dkimKey.jsm.js"); Cu.import("resource://dkim_verifier/dkimPolicy.jsm.js"); Cu.import("resource://dkim_verifier/MsgReader.jsm.js"); +Cu.import("resource://dkim_verifier/rfcParser.jsm.js"); // namespaces var RSA = {}; @@ -175,33 +176,11 @@ var Verifier = (function() { */ var prefs = Services.prefs.getBranch(PREF_BRANCH); - /* +/* * private variables */ var log = Logging.getLogger("Verifier"); - // WSP help pattern as specified in Section 2.8 of RFC 6376 - var pattWSP = "[ \t]"; - // FWS help pattern as specified in Section 2.8 of RFC 6376 - var pattFWS = "(?:" + pattWSP + "*(?:\r\n)?" + pattWSP + "+)"; - // Pattern for hyphenated-word as specified in Section 2.10 of RFC 6376 - var hyphenated_word = "(?:[A-Za-z](?:[A-Za-z0-9-]*[A-Za-z0-9])?)"; - // Pattern for ALPHADIGITPS as specified in Section 2.10 of RFC 6376 - var ALPHADIGITPS = "[A-Za-z0-9+/]"; - // Pattern for base64string as specified in Section 2.10 of RFC 6376 - var base64string = "(?:"+ALPHADIGITPS+"(?:"+pattFWS+"?"+ALPHADIGITPS+")*(?:"+pattFWS+"?=){0,2})"; - // Pattern for dkim-safe-char as specified in Section 2.11 of RFC 6376 - var dkim_safe_char = "[!-:<>-~]"; - // Pattern for hex-octet as specified in Section 6.7 of RFC 2045 - // we also allow added FWS (needed for Copied header fields) - var hex_octet = "(?:="+pattFWS+"?[0-9ABCDEF]"+pattFWS+"?[0-9ABCDEF])"; - // Pattern for qp-hdr-value as specified in Section 2.10 of RFC 6376 - // same as dkim-quoted-printable with "|" encoded as specified in Section 2.11 of RFC 6376 - var qp_hdr_value = "(?:(?:"+pattFWS+"|"+hex_octet+"|[!-:<>-{}-~])*)"; - // Pattern for field-name as specified in Section 3.6.8 of RFC 5322 without ";" - // used as hdr-name in RFC 6376 - var hdr_name = "(?:[!-9<-~]+)"; - /* * private methods */ @@ -380,92 +359,6 @@ var Verifier = (function() { NaClUtil.nacl.util.decodeBase64(key)); } - /** - * Parses a Tag=Value list. - * Specified in Section 3.2 of RFC 6376. - * - * @param {String} str - * - * @return {Map|Number} Map - * -1 if a tag-spec is ill-formed - * -2 duplicate tag names - */ - function parseTagValueList(str) { - var tval = "[!-:<-~]+"; - var tag_name = "[A-Za-z][A-Za-z0-9_]*"; - var tag_value = "(?:"+tval+"(?:("+pattWSP+"|"+pattFWS+")+"+tval+")*)?"; - - // delete optional semicolon at end - if (str.charAt(str.length-1) === ";") { - str = str.substr(0, str.length-1); - } - - var array = str.split(";"); - /** @type{Map} */ - var map = new Map(); - var tmp; - /** @type{String} */ - var name; - /** @type{String} */ - var value; - for (var elem of array) { - // get tag name and value - tmp = elem.match(new RegExp( - "^"+pattFWS+"?("+tag_name+")"+pattFWS+"?="+pattFWS+"?("+tag_value+")"+pattFWS+"?$" - )); - if (tmp === null || !tmp[1] || tmp[2] === undefined) { - return -1; - } - name = tmp[1]; - value = tmp[2]; - - // check that tag is no duplicate - if (map.has(name)) { - return -2; - } - - // store Tag=Value pair - map.set(name, value); - } - - return map; - } - - /** - * Parse a tag value stored in a Map. - * - * @param {Map} map - * @param {String} tag_name name of the tag - * @param {String} pattern_tag_value Pattern for the tag-value - * @param {Number} [expType=1] Type of exception to throw. 1 for DKIM header, 2 for DKIM key, 3 for general. - * - * @return {RegExpMatchArray|Null} The match from the RegExp if tag_name exists, otherwise null - * - * @throws {DKIM_SigError|DKIM_Error} Throws if tag_value does not match. - */ - function parseTagValue(map, tag_name, pattern_tag_value, expType = 1) { - var tag_value = map.get(tag_name); - // return null if tag_name doesn't exists - if (tag_value === undefined) { - return null; - } - - var res = tag_value.match(new RegExp("^"+pattern_tag_value+"$")); - - // throw DKIM_SigError if tag_value is ill-formed - if (res === null) { - if (expType === 1) { - throw new DKIM_SigError(`DKIM_SIGERROR_ILLFORMED_${tag_name.toUpperCase()}`); - } else if (expType === 2) { - throw new DKIM_SigError(`DKIM_SIGERROR_KEY_ILLFORMED_${tag_name.toUpperCase()}`); - } else { - throw new DKIM_Error(`illformed tag ${tag_name}`); - } - } - - return res; - } - function newDKIMSignature( DKIMSignatureHeader ) { var DKIMSignature = { original_header : DKIMSignatureHeader, @@ -507,19 +400,22 @@ var Verifier = (function() { // strip the \r\n at the end DKIMSignatureHeader = DKIMSignatureHeader.substr(0, DKIMSignatureHeader.length-2); // parse tag-value list - var tagMap = parseTagValueList(DKIMSignatureHeader); - if (tagMap === -1) { + let parsedTagMap = rfcParser.parseTagValueList(DKIMSignatureHeader); + if (parsedTagMap === -1) { throw new DKIM_SigError("DKIM_SIGERROR_ILLFORMED_TAGSPEC"); - } else if (tagMap === -2) { + } else if (parsedTagMap === -2) { throw new DKIM_SigError("DKIM_SIGERROR_DUPLICATE_TAG"); } - if (!(tagMap instanceof Map)) { - throw new Error(`unexpected return value from parseTagValueList: ${tagMap}`); + if (!(toType(parsedTagMap) === "Map")) { + throw new Error(`unexpected return value from parseTagValueList: ${parsedTagMap}`); } + /** @type {Map} */ + // @ts-ignore + let tagMap = parsedTagMap; // get Version (plain-text; REQUIRED) // must be "1" - var versionTag = parseTagValue(tagMap, "v", "[0-9]+"); + var versionTag = rfcParser.parseTagValue(tagMap, "v", "[0-9]+"); if (versionTag === null) { throw new DKIM_SigError("DKIM_SIGERROR_MISSING_V"); } @@ -534,7 +430,7 @@ var Verifier = (function() { var sig_a_tag_k = "(rsa|ed25519|[A-Za-z](?:[A-Za-z]|[0-9])*)"; var sig_a_tag_h = "(sha1|sha256|[A-Za-z](?:[A-Za-z]|[0-9])*)"; var sig_a_tag_alg = sig_a_tag_k+"-"+sig_a_tag_h; - var algorithmTag = parseTagValue(tagMap, "a", sig_a_tag_alg); + var algorithmTag = rfcParser.parseTagValue(tagMap, "a", sig_a_tag_alg); if (algorithmTag === null) { throw new DKIM_SigError("DKIM_SIGERROR_MISSING_A"); } @@ -563,24 +459,24 @@ var Verifier = (function() { } // get signature data (base64;REQUIRED) - var signatureDataTag = parseTagValue(tagMap, "b", base64string); + var signatureDataTag = rfcParser.parseTagValue(tagMap, "b", rfcParser.get("base64string")); if (signatureDataTag === null) { throw new DKIM_SigError("DKIM_SIGERROR_MISSING_B"); } - DKIMSignature.b = signatureDataTag[0].replace(new RegExp(pattFWS,"g"), ""); + DKIMSignature.b = signatureDataTag[0].replace(new RegExp(rfcParser.get("FWS"),"g"), ""); DKIMSignature.b_folded = signatureDataTag[0]; // get body hash (base64;REQUIRED) - var bodyHashTag = parseTagValue(tagMap, "bh", base64string); + var bodyHashTag = rfcParser.parseTagValue(tagMap, "bh", rfcParser.get("base64string")); if (bodyHashTag === null) { throw new DKIM_SigError("DKIM_SIGERROR_MISSING_BH"); } - DKIMSignature.bh = bodyHashTag[0].replace(new RegExp(pattFWS,"g"), ""); + DKIMSignature.bh = bodyHashTag[0].replace(new RegExp(rfcParser.get("FWS"),"g"), ""); // get Message canonicalization (plain-text; OPTIONAL, default is "simple/simple") // currently only "simple" or "relaxed" for both header and body - var sig_c_tag_alg = "(simple|relaxed|"+hyphenated_word+")"; - var msCanonTag = parseTagValue(tagMap, "c", sig_c_tag_alg+"(?:/"+sig_c_tag_alg+")?"); + var sig_c_tag_alg = `(simple|relaxed|${rfcParser.get("hyphenated_word")})`; + var msCanonTag = rfcParser.parseTagValue(tagMap, "c", `${sig_c_tag_alg}(?:/${sig_c_tag_alg})?`); if (msCanonTag === null) { DKIMSignature.c_header = "simple"; DKIMSignature.c_body = "simple"; @@ -605,22 +501,19 @@ var Verifier = (function() { } // get SDID (plain-text; REQUIRED) - // Pattern for sub-domain as specified in Section 4.1.2 of RFC 5321 - var sub_domain = "(?:[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)"; - var domain_name = "(?:"+sub_domain+"(?:\\."+sub_domain+")+)"; - var SDIDTag = parseTagValue(tagMap, "d", domain_name); + var SDIDTag = rfcParser.parseTagValue(tagMap, "d", rfcParser.get("domain_name")); if (SDIDTag === null) { throw new DKIM_SigError("DKIM_SIGERROR_MISSING_D"); } DKIMSignature.d = SDIDTag[0]; // get Signed header fields (plain-text, but see description; REQUIRED) - var sig_h_tag = "("+hdr_name+")(?:"+pattFWS+"?:"+pattFWS+"?"+hdr_name+")*"; - var signedHeadersTag = parseTagValue(tagMap, "h", sig_h_tag); + var sig_h_tag = `(${rfcParser.get("hdr_name")})(?:${rfcParser.get("FWS")}?:${rfcParser.get("FWS")}?${rfcParser.get("hdr_name")})*`; + var signedHeadersTag = rfcParser.parseTagValue(tagMap, "h", sig_h_tag); if (signedHeadersTag === null) { throw new DKIM_SigError("DKIM_SIGERROR_MISSING_H"); } - DKIMSignature.h = signedHeadersTag[0].replace(new RegExp(pattFWS,"g"), ""); + DKIMSignature.h = signedHeadersTag[0].replace(new RegExp(rfcParser.get("FWS"),"g"), ""); // get the header field names and store them in lower case in an array DKIMSignature.h_array = DKIMSignature.h.split(":"). map(x => x.trim().toLowerCase()). @@ -687,12 +580,10 @@ var Verifier = (function() { http://stackoverflow.com/questions/201323/using-a-regular-expression-to-validate-an-email-address */ - var atext = "[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]"; - var local_part = "(?:"+atext+"+(?:\\."+atext+"+)*)"; - var sig_i_tag = local_part+"?@("+domain_name+")"; + var sig_i_tag = `${rfcParser.get("local_part")}?@(${rfcParser.get("domain_name")})`; var AUIDTag = null; try { - AUIDTag = parseTagValue(tagMap, "i", sig_i_tag); + AUIDTag = rfcParser.parseTagValue(tagMap, "i", sig_i_tag); } catch (exception) { if (exception instanceof DKIM_SigError && exception.errorType === "DKIM_SIGERROR_ILLFORMED_I") @@ -727,15 +618,15 @@ var Verifier = (function() { } // get Body length count (plain-text unsigned decimal integer; OPTIONAL, default is entire body) - var BodyLengthTag = parseTagValue(tagMap, "l", "[0-9]{1,76}"); + var BodyLengthTag = rfcParser.parseTagValue(tagMap, "l", "[0-9]{1,76}"); if (BodyLengthTag !== null) { DKIMSignature.l = parseInt(BodyLengthTag[0], 10); } // get query methods (plain-text; OPTIONAL, default is "dns/txt") - var sig_q_tag_method = "(?:dns/txt|"+hyphenated_word+"(?:/"+qp_hdr_value+")?)"; - var sig_q_tag = sig_q_tag_method+"(?:"+pattFWS+"?:"+pattFWS+"?"+sig_q_tag_method+")*"; - var QueryMetTag = parseTagValue(tagMap, "q", sig_q_tag); + var sig_q_tag_method = `(?:dns/txt|${rfcParser.get("hyphenated_word")}(?:/${rfcParser.get("qp_hdr_value")})?)`; + var sig_q_tag = `${sig_q_tag_method}(?:${rfcParser.get("FWS")}?:${rfcParser.get("FWS")}?${sig_q_tag_method})*`; + var QueryMetTag = rfcParser.parseTagValue(tagMap, "q", sig_q_tag); if (QueryMetTag === null) { DKIMSignature.q = "dns/txt"; } else { @@ -748,14 +639,15 @@ var Verifier = (function() { // get selector subdividing the namespace for the "d=" (domain) tag (plain-text; REQUIRED) var SelectorTag; try { - SelectorTag = parseTagValue(tagMap, "s", sub_domain+"(?:\\."+sub_domain+")*"); + SelectorTag = rfcParser.parseTagValue(tagMap, "s", `${rfcParser.get("sub_domain")}(?:\\.${rfcParser.get("sub_domain")})*`); } catch (exception) { if (exception instanceof DKIM_SigError && exception.errorType === "DKIM_SIGERROR_ILLFORMED_S") { + // TODO: Find an internationalized more relaxed version, if needed // try to parse selector in a more relaxed way var sub_domain_ = "(?:[A-Za-z0-9_](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?)"; - SelectorTag = parseTagValue(tagMap, "s", sub_domain_+"(?:\\."+sub_domain_+")*"); + SelectorTag = rfcParser.parseTagValue(tagMap, "s", `${sub_domain_}(?:\\.${sub_domain_})*`); switch (prefs.getIntPref("error.illformed_s.treatAs")) { case 0: // error throw exception; @@ -778,7 +670,7 @@ var Verifier = (function() { // get Signature Timestamp (plain-text unsigned decimal integer; RECOMMENDED, // default is an unknown creation time) - var SigTimeTag = parseTagValue(tagMap, "t", "[0-9]+"); + var SigTimeTag = rfcParser.parseTagValue(tagMap, "t", "[0-9]+"); if (SigTimeTag !== null) { DKIMSignature.t = parseInt(SigTimeTag[0], 10); } @@ -786,7 +678,7 @@ var Verifier = (function() { // get Signature Expiration (plain-text unsigned decimal integer; // RECOMMENDED, default is no expiration) // The value of the "x=" tag MUST be greater than the value of the "t=" tag if both are present - var ExpTimeTag = parseTagValue(tagMap, "x", "[0-9]+"); + var ExpTimeTag = rfcParser.parseTagValue(tagMap, "x", "[0-9]+"); if (ExpTimeTag !== null) { DKIMSignature.x = parseInt(ExpTimeTag[0], 10); if (DKIMSignature.t !== null && DKIMSignature.x < DKIMSignature.t) { @@ -795,12 +687,12 @@ var Verifier = (function() { } // get Copied header fields (dkim-quoted-printable, but see description; OPTIONAL, default is null) - var hdr_name_FWS = "(?:(?:[!-9<-~]"+pattFWS+"?)+)"; - var sig_z_tag_copy = hdr_name_FWS+pattFWS+"?:"+qp_hdr_value; - var sig_z_tag = sig_z_tag_copy+"(\\|"+pattFWS+"?"+sig_z_tag_copy+")*"; - var CopyHeaderFieldsTag = parseTagValue(tagMap, "z", sig_z_tag); + var hdr_name_FWS = `(?:(?:[!-9<-~]${rfcParser.get("FWS")}?)+)`; + var sig_z_tag_copy = `${hdr_name_FWS}${rfcParser.get("FWS")}?:${rfcParser.get("qp_hdr_value")}`; + var sig_z_tag = `${sig_z_tag_copy}(\\|${rfcParser.get("FWS")}?${sig_z_tag_copy})*`; + var CopyHeaderFieldsTag = rfcParser.parseTagValue(tagMap, "z", sig_z_tag); if (CopyHeaderFieldsTag !== null) { - DKIMSignature.z = CopyHeaderFieldsTag[0].replace(new RegExp(pattFWS,"g"), ""); + DKIMSignature.z = CopyHeaderFieldsTag[0].replace(new RegExp(rfcParser.get("FWS"),"g"), ""); } return DKIMSignature; @@ -834,21 +726,24 @@ var Verifier = (function() { }; // parse tag-value list - var tagMap = parseTagValueList(DKIMKeyRecord); - if (tagMap === -1) { + var parsedTagMap = rfcParser.parseTagValueList(DKIMKeyRecord); + if (parsedTagMap === -1) { throw new DKIM_SigError("DKIM_SIGERROR_KEY_ILLFORMED_TAGSPEC"); - } else if (tagMap === -2) { + } else if (parsedTagMap === -2) { throw new DKIM_SigError("DKIM_SIGERROR_KEY_DUPLICATE_TAG"); } - if (!(tagMap instanceof Map)) { - throw new Error(`unexpected return value from parseTagValueList: ${tagMap}`); + if (!(toType(parsedTagMap) === "Map")) { + throw new Error(`unexpected return value from parseTagValueList: ${parsedTagMap}`); } + /** @type {Map} */ + // @ts-ignore + let tagMap = parsedTagMap; // get version (plain-text; RECOMMENDED, default is "DKIM1") // If specified, this tag MUST be set to "DKIM1" // This tag MUST be the first tag in the record - var key_v_tag_value = dkim_safe_char+"*"; - var versionTag = parseTagValue(tagMap, "v", key_v_tag_value, 2); + var key_v_tag_value = `${rfcParser.get("dkim_safe_char")}*`; + var versionTag = rfcParser.parseTagValue(tagMap, "v", key_v_tag_value, 2); if (versionTag === null || versionTag[0] === "DKIM1") { DKIMKey.v = "DKIM1"; } else { @@ -856,17 +751,17 @@ var Verifier = (function() { } // get Acceptable hash algorithms (plain-text; OPTIONAL, defaults toallowing all algorithms) - var key_h_tag_alg = "(?:sha1|sha256|"+hyphenated_word+")"; - var key_h_tag = key_h_tag_alg+"(?:"+pattFWS+"?:"+pattFWS+"?"+key_h_tag_alg+")*"; - var algorithmTag = parseTagValue(tagMap, "h", key_h_tag, 2); + var key_h_tag_alg = `(?:sha1|sha256|${rfcParser.get("hyphenated_word")})`; + var key_h_tag = `${key_h_tag_alg}(?:${rfcParser.get("FWS")}?:${rfcParser.get("FWS")}?${key_h_tag_alg})*`; + var algorithmTag = rfcParser.parseTagValue(tagMap, "h", key_h_tag, 2); if (algorithmTag !== null) { DKIMKey.h = algorithmTag[0]; DKIMKey.h_array = DKIMKey.h.split(":").map(s => s.trim()).filter(x => x); } // get Key type (plain-text; OPTIONAL, default is "rsa") - var key_k_tag_type = "(?:rsa|ed25519|"+hyphenated_word+")"; - var keyTypeTag = parseTagValue(tagMap, "k", key_k_tag_type, 2); + var key_k_tag_type = `(?:rsa|ed25519|${rfcParser.get("hyphenated_word")})`; + var keyTypeTag = rfcParser.parseTagValue(tagMap, "k", key_k_tag_type, 2); if (keyTypeTag === null) { DKIMKey.k = "rsa"; } else if (keyTypeTag[0] === "ed25519" || keyTypeTag[0] === "rsa") { @@ -876,16 +771,16 @@ var Verifier = (function() { } // get Notes (qp-section; OPTIONAL, default is empty) - var ptext = "(?:"+hex_octet+"|[!-<>-~])"; - var qp_section = "(?:(?:"+ptext+"| |\t)*"+ptext+")?"; - var notesTag = parseTagValue(tagMap, "n", qp_section, 2); + var ptext = `(?:${rfcParser.get("hex_octet")}|[!-<>-~])`; + var qp_section = `(?:(?:${ptext}| |\t)*${ptext})?`; + var notesTag = rfcParser.parseTagValue(tagMap, "n", qp_section, 2); if (notesTag !== null) { DKIMKey.n = notesTag[0]; } // get Public-key data (base64; REQUIRED) // empty value means that this public key has been revoked - var keyTag = parseTagValue(tagMap, "p", base64string+"?", 2); + var keyTag = rfcParser.parseTagValue(tagMap, "p", `${rfcParser.get("base64string")}?`, 2); if (keyTag === null) { throw new DKIM_SigError("DKIM_SIGERROR_KEY_MISSING_P"); } else { @@ -897,9 +792,9 @@ var Verifier = (function() { } // get Service Type (plain-text; OPTIONAL; default is "*") - var key_s_tag_type = "(?:email|\\*|"+hyphenated_word+")"; - var key_s_tag = key_s_tag_type+"(?:"+pattFWS+"?:"+pattFWS+"?"+key_s_tag_type+")*"; - var serviceTypeTag = parseTagValue(tagMap, "s", key_s_tag, 2); + var key_s_tag_type = `(?:email|\\*|${rfcParser.get("hyphenated_word")})`; + var key_s_tag = `${key_s_tag_type}(?:${rfcParser.get("FWS")}?:${rfcParser.get("FWS")}?${key_s_tag_type})*`; + var serviceTypeTag = rfcParser.parseTagValue(tagMap, "s", key_s_tag, 2); if (serviceTypeTag === null) { DKIMKey.s = "*"; } else { @@ -912,9 +807,9 @@ var Verifier = (function() { } // get Flags (plaintext; OPTIONAL, default is no flags set) - var key_t_tag_flag = "(?:y|s|"+hyphenated_word+")"; - var key_t_tag = key_t_tag_flag+"(?:"+pattFWS+"?:"+pattFWS+"?"+key_t_tag_flag+")*"; - var flagsTag = parseTagValue(tagMap, "t", key_t_tag, 2); + var key_t_tag_flag = `(?:y|s|${rfcParser.hyphenated_word})`; + var key_t_tag = `${key_t_tag_flag}(?:${rfcParser.get("FWS")}?:${rfcParser.get("FWS")}?${key_t_tag_flag})*`; + var flagsTag = rfcParser.parseTagValue(tagMap, "t", key_t_tag, 2); if (flagsTag !== null) { DKIMKey.t = flagsTag[0]; // get the flags and store them in an array @@ -1100,9 +995,9 @@ var Verifier = (function() { // with the value of the "b=" tag (including all surrounding whitespace) deleted var pos_bTag = DKIMSignature.original_header.indexOf(DKIMSignature.b_folded); var tempBegin = DKIMSignature.original_header.substr(0, pos_bTag); - tempBegin = tempBegin.replace(new RegExp(pattFWS+"?$"), ""); + tempBegin = tempBegin.replace(new RegExp(`${rfcParser.get("FWS")}?$`), ""); var tempEnd = DKIMSignature.original_header.substr(pos_bTag+DKIMSignature.b_folded.length); - tempEnd = tempEnd.replace(new RegExp("^"+pattFWS+"?"), ""); + tempEnd = tempEnd.replace(new RegExp(`^${rfcParser.get("FWS")}?`), ""); var temp = tempBegin + tempEnd; // canonicalized using the header canonicalization algorithm specified in the "c=" tag temp = headerCanonAlgo(temp); @@ -1196,7 +1091,7 @@ var Verifier = (function() { const verifyTime = receivedTime ? receivedTime : new Date(); const time = Math.round(verifyTime.getTime() / 1000); - log.debug("Info: Using '"+verifyTime+"' as timestamp for expiration check"); + log.debug(`Info: Using '${verifyTime}' as timestamp for expiration check`); // warning if signature expired if (DKIMSignature.x !== null && DKIMSignature.x < time) { @@ -1706,12 +1601,6 @@ var that = { */ checkForSignatureExsistens : checkForSignatureExsistens, - /* - * make parsing of the tag-value list public - */ - parseTagValueList : parseTagValueList, - parseTagValue : parseTagValue, - version: module_version, }; return that; diff --git a/modules/rfcParser.jsm.js b/modules/rfcParser.jsm.js new file mode 100644 index 00000000..5566f45d --- /dev/null +++ b/modules/rfcParser.jsm.js @@ -0,0 +1,274 @@ +/** + * RegExp pattern for ABNF definitions in various RFCs. + * + * Copyright (c) 2020-2023 Philippe Lieser + * + * This software is licensed under the terms of the MIT License. + * + * The above copyright and license notice shall be + * included in all copies or substantial portions of the Software. + */ + +// options for ESLint +/* global Components, Services */ +/* global DKIM_SigError, DKIM_Error */ +/* exported EXPORTED_SYMBOLS, rfcParser */ + +"use strict"; + +var EXPORTED_SYMBOLS = [ + "rfcParser" +]; + +// @ts-ignore +const Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); + +Cu.import("resource://dkim_verifier/helper.jsm.js"); + +let rfcParser = (function() { + + let RfcParserStd = {}; + + ////// RFC 2045 - Multipurpose Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies + //// 5.1. Syntax of the Content-Type Header Field + RfcParserStd.token = "[^ \\x00-\\x1F\\x7F()<>@,;:\\\\\"/[\\]?=\\u0080-\\uFFFF]+"; + + ////// RFC 5234 - Augmented BNF for Syntax Specifications: ABNF + //// Appendix B.1. Core Rules + RfcParserStd.VCHAR = "[!-~]"; + RfcParserStd.WSP = "[ \t]"; + + ////// RFC 5321 - Simple Mail Transfer Protocol + //// 4.1.2. Command Argument Syntax + RfcParserStd.Let_dig = "[A-Za-z0-9]"; + RfcParserStd.Ldh_str = `(?:[A-Za-z0-9-]*${RfcParserStd.Let_dig})`; + RfcParserStd.Keyword = RfcParserStd.Ldh_str; + RfcParserStd.sub_domain = `(?:${RfcParserStd.Let_dig}${RfcParserStd.Ldh_str}?)`; + + ////// RFC 5322 - Internet Message Format + //// 3.2.1. Quoted characters + // Note: this is incomplete (obs-qp is missing) + RfcParserStd.quoted_pair = `(?:\\\\(?:${RfcParserStd.VCHAR}|${RfcParserStd.WSP}))`; + //// 3.2.2. Folding White Space and Comments + // Note: this is incomplete (obs-FWS is missing) + // Note: this is as specified in Section 2.8. of RFC 6376 [DKIM] + RfcParserStd.FWS = `(?:${RfcParserStd.WSP}*(?:\r\n)?${RfcParserStd.WSP}+)`; + // Note: helper only, not part of the RFC + RfcParserStd.FWS_op = `${RfcParserStd.FWS}?`; + // Note: this is incomplete (obs-ctext is missing) + RfcParserStd.ctext = "[!-'*-[\\]-~]"; + // Note: this is incomplete (comment is missing) + RfcParserStd.ccontent = `(?:${RfcParserStd.ctext}|${RfcParserStd.quoted_pair})`; + RfcParserStd.comment = `\\((?:${RfcParserStd.FWS_op}${RfcParserStd.ccontent})*${RfcParserStd.FWS_op}\\)`; + RfcParserStd.CFWS = `(?:(?:(?:${RfcParserStd.FWS_op}${RfcParserStd.comment})+${RfcParserStd.FWS_op})|${RfcParserStd.FWS})`; + // Note: helper only, not part of the RFC + RfcParserStd.CFWS_op = `${RfcParserStd.CFWS}?`; + //// 3.2.3. Atom + RfcParserStd.atext = "[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]"; + RfcParserStd.atom = `(?:${RfcParserStd.CFWS_op}${RfcParserStd.atext}+${RfcParserStd.CFWS_op})`; + // Note: helper only, not part of the RFC: an atom without the optional surrounding CFWS. dot is included for obs-phrase + RfcParserStd.atom_b_obs = `(?:(?:${RfcParserStd.atext}|\\.)+)`; + RfcParserStd.dot_atom_text = `(?:${RfcParserStd.atext}+(?:\\.${RfcParserStd.atext}+)*)`; + RfcParserStd.dot_atom = `(?:${RfcParserStd.CFWS_op}${RfcParserStd.dot_atom_text}${RfcParserStd.CFWS_op})`; + //// 3.2.4. Quoted Strings + // Note: this is incomplete (obs-qtext is missing) + RfcParserStd.qtext = "[!#-[\\]-~]"; + RfcParserStd.qcontent = `(?:${RfcParserStd.qtext}|${RfcParserStd.quoted_pair})`; + RfcParserStd.quoted_string = `(?:${RfcParserStd.CFWS_op}"(?:${RfcParserStd.FWS_op}${RfcParserStd.qcontent})*${RfcParserStd.FWS_op}"${RfcParserStd.CFWS_op})`; + //// 3.2.5. Miscellaneous Tokens + RfcParserStd.word = `(?:${RfcParserStd.atom}|${RfcParserStd.quoted_string})`; + // Note: helper only, not part of the RFC: chain of word (including dot for obs-phrase) without whitespace between, or quoted string chain + RfcParserStd.word_chain = `(?:(?:${RfcParserStd.atom_b_obs}|(?:${RfcParserStd.atom_b_obs}?${RfcParserStd.quoted_string})+${RfcParserStd.atom_b_obs}?))`; + // Note: this is incomplete (obs-phrase is missing) + // Note: this is rewritten to avoid backtracking issues (in RFC specified as `1*word / obs-phrase`) + RfcParserStd.phrase = `(?:${RfcParserStd.CFWS_op}${RfcParserStd.word_chain}(?:${RfcParserStd.CFWS}${RfcParserStd.word_chain})*${RfcParserStd.CFWS_op})`; + //// 3.4. Address Specification + RfcParserStd.name_addr = `(?:${RfcParserStd.display_name}?${RfcParserStd.angle_addr})`; + // Note: this is incomplete (obs-angle-addr is missing) + RfcParserStd.angle_addr = `(?:${RfcParserStd.CFWS_op}<${RfcParserStd.addr_spec}>${RfcParserStd.CFWS_op})`; + RfcParserStd.display_name = `(?:${RfcParserStd.phrase})`; + //// 3.4.1. Addr-Spec Specification + RfcParserStd.addr_spec = `(?:${RfcParserStd.local_part}@${RfcParserStd.domain})`; + // Note: this is incomplete (obs-local-part is missing) + RfcParserStd.local_part = `(?:${RfcParserStd.dot_atom}|${RfcParserStd.quoted_string})`; + // Note: this is incomplete (domain-literal and obs-domain are missing) + RfcParserStd.domain = `(?:${RfcParserStd.dot_atom})`; + ////// RFC 6376 - DomainKeys Identified Mail (DKIM) Signatures + //// 3.5. The DKIM-Signature Header Field + RfcParserStd.domain_name = `(?:${RfcParserStd.sub_domain}(?:\\.${RfcParserStd.sub_domain})+)`; + + /* Customs - not in 5.x branch */ + // "value" as specified in Section 5.1 of RFC 2045. + RfcParserStd.quoted_string_cp = `(?:${RfcParserStd.CFWS_op}"((?:${RfcParserStd.FWS_op}${RfcParserStd.qcontent})*)${RfcParserStd.FWS_op}"${RfcParserStd.CFWS_op})`; + RfcParserStd.value_cp = `(?:(${RfcParserStd.token})|${RfcParserStd.quoted_string_cp})`; + // Pattern for hyphenated-word as specified in Section 2.10 of RFC 6376 + RfcParserStd.hyphenated_word = "(?:[A-Za-z](?:[A-Za-z0-9-]*[A-Za-z0-9])?)"; + // Pattern for ALPHADIGITPS as specified in Section 2.10 of RFC 6376 + RfcParserStd.ALPHADIGITPS = "[A-Za-z0-9+/]"; + // Pattern for base64string as specified in Section 2.10 of RFC 6376 + RfcParserStd.base64string = `(?:${RfcParserStd.ALPHADIGITPS}(?:${RfcParserStd.FWS}?${RfcParserStd.ALPHADIGITPS})*(?:${RfcParserStd.FWS}?=){0,2})`; + // Pattern for dkim-safe-char as specified in Section 2.11 of RFC 6376 + RfcParserStd.dkim_safe_char = "[!-:<>-~]"; + // Pattern for hex-octet as specified in Section 6.7 of RFC 2045 + // we also allow added FWS (needed for Copied header fields) + RfcParserStd.hex_octet = `(?:=${RfcParserStd.FWS}?[0-9ABCDEF]${RfcParserStd.FWS}?[0-9ABCDEF])`; + // Pattern for qp-hdr-value as specified in Section 2.10 of RFC 6376 + // same as dkim-quoted-printable with "|" encoded as specified in Section 2.11 of RFC 6376 + RfcParserStd.qp_hdr_value = `(?:(?:${RfcParserStd.FWS}|${RfcParserStd.hex_octet}|[!-:<>-{}-~])*)`; + // Pattern for field-name as specified in Section 3.6.8 of RFC 5322 without ";" + // used as hdr-name in RFC 6376 + RfcParserStd.hdr_name = "(?:[!-9<-~]+)"; + + + let RfcParserI = {}; + ////// RFC 3629 - UTF-8, a transformation format of ISO 10646 + //// 4. Syntax of UTF-8 Byte Sequences + //// https://datatracker.ietf.org/doc/html/rfc3629#section-4 + RfcParserI.UTF8_tail = "[\x80-\xBF]"; + RfcParserI.UTF8_2 = `(?:[\xC2-\xDF]${RfcParserI.UTF8_tail})`; + RfcParserI.UTF8_3 = `(?:(?:\xE0[\xA0-\xBF]${RfcParserI.UTF8_tail})|(?:[\xE1-\xEC]${RfcParserI.UTF8_tail}${RfcParserI.UTF8_tail})|(?:\xED[\x80-\x9F]${RfcParserI.UTF8_tail})|(?:[\xEE-\xEF]${RfcParserI.UTF8_tail}${RfcParserI.UTF8_tail}))`; + RfcParserI.UTF8_4 = `(?:(?:\xF0[\x90-\xBF]${RfcParserI.UTF8_tail}${RfcParserI.UTF8_tail})|(?:[\xF1-\xF3]${RfcParserI.UTF8_tail}${RfcParserI.UTF8_tail}${RfcParserI.UTF8_tail})|(?:\xF4[\x80-\x8F]${RfcParserI.UTF8_tail}${RfcParserI.UTF8_tail}))`; + ////// RFC 5890 - Internationalized Domain Names for Applications (IDNA): Definitions and Document Framework + //// 2.3.2.1. IDNA-valid strings, A-label, and U-label + //// https://datatracker.ietf.org/doc/html/rfc5890#section-2.3.2.1 + // IMPORTANT: This does not validate if the label is valid. + // E.g. the character "⒈" (U+2488) should be disallowed but matches UTF8_non_ascii + RfcParserI.u_label = `(?:(?:${RfcParserI.Let_dig}|${RfcParserI.UTF8_non_ascii})(?:${RfcParserI.Let_dig}|-|${RfcParserI.UTF8_non_ascii})*(?:${RfcParserI.Let_dig}|${RfcParserI.UTF8_non_ascii})?)`; + ////// RFC 6531 - SMTP Extension for Internationalized Email + //// 3.3. Extended Mailbox Address Syntax + //// https://datatracker.ietf.org/doc/html/rfc6531#section-3.3 + /** @override */ + RfcParserI.sub_domain = `(?:${RfcParserStd.sub_domain}|${RfcParserI.u_label})`; + ////// RFC 6532 - Internationalized Email Headers + //// 3.1. UTF-8 Syntax and Normalization + //// https://datatracker.ietf.org/doc/html/rfc6532#section-3.1 + RfcParserI.UTF8_non_ascii = `(?:${RfcParserI.UTF8_2}|${RfcParserI.UTF8_3}|${RfcParserI.UTF8_4})`; + //// 3.2. Syntax Extensions to RFC 5322 + //// https://datatracker.ietf.org/doc/html/rfc6532#section-3.2 + /** @override */ + RfcParserI.VCHAR = `(?:${RfcParserStd.VCHAR}|${RfcParserI.UTF8_non_ascii})`; + /** @override */ + RfcParserI.ctext = `(?:${RfcParserStd.ctext}|${RfcParserI.UTF8_non_ascii})`; + /** @override */ + RfcParserI.atext = `(?:${RfcParserStd.atext}|${RfcParserI.UTF8_non_ascii})`; + /** @override */ + RfcParserI.qtext = `(?:${RfcParserStd.qtext}|${RfcParserI.UTF8_non_ascii})`; + + /** @readonly */ + const TAG_PARSE_ERROR = { + /** @readonly */ + ILL_FORMED: -1, + /** @readonly */ + DUPLICATE: -2, + }; + + /** + * Parses a Tag=Value list. + * Specified in Section 3.2 of RFC 6376. + * + * @param {string} str + * @returns {Map|number} Map of the parsed list or: + * - -1 if a tag-spec is ill-formed. + * - -2 duplicate tag names. + */ + function parseTagValueList(str) { + const tval = "[!-:<-~]+"; + const tagName = "[A-Za-z][A-Za-z0-9_]*"; + const tagValue = `(?:${tval}(?:(${RfcParserStd.WSP}|${RfcParserStd.FWS})+${tval})*)?`; + + // delete optional semicolon at end + let listStr = str; + if (listStr.charAt(listStr.length - 1) === ";") { + listStr = listStr.substr(0, listStr.length - 1); + } + + const array = listStr.split(";"); + /** @type {Map} */ + const map = new Map(); + for (const elem of array) { + // get tag name and value + const tmp = elem.match(new RegExp( + `^${RfcParserStd.FWS}?(${tagName})${RfcParserStd.FWS}?=${RfcParserStd.FWS}?(${tagValue})${RfcParserStd.FWS}?$` + )); + if (tmp === null || !tmp[1] || tmp[2] === undefined) { + return TAG_PARSE_ERROR.ILL_FORMED; + } + const name = tmp[1]; + const value = tmp[2]; + + // check that tag is no duplicate + if (map.has(name)) { + return TAG_PARSE_ERROR.DUPLICATE; + } + + // store Tag=Value pair + map.set(name, value); + } + + return map; + } + + /** + * Parse a tag value stored in a Map. + * + * @param {ReadonlyMap} map + * @param {string} tagName - name of the tag + * @param {string} patternTagValue - Pattern for the tag-value + * @param {number} [expType] - Type of exception to throw. 1 for DKIM header, 2 for DKIM key, 3 for general. + * @returns {[string, ...string[]]|null} The match from the RegExp if tag_name exists, otherwise null + * @throws {DKIM_SigError|DKIM_Error} Throws if tag_value does not match. + */ + function parseTagValue(map, tagName, patternTagValue, expType = 1) { + const tagValue = map.get(tagName); + // return null if tag_name doesn't exists + if (tagValue === undefined) { + return null; + } + + const res = tagValue.match(new RegExp(`^${patternTagValue}$`)); + + // throw DKIM_SigError if tag_value is ill-formed + if (res === null) { + if (expType === 1) { + throw new DKIM_SigError(`DKIM_SIGERROR_ILLFORMED_${tagName.toUpperCase()}`); + } else if (expType === 2) { + throw new DKIM_SigError(`DKIM_SIGERROR_KEY_ILLFORMED_${tagName.toUpperCase()}`); + } else { + throw new DKIM_Error(`illformed tag ${tagName}`); + } + } + + // @ts-expect-error + return res; + } + + const PREF_BRANCH = "extensions.dkim_verifier."; + + const prefs = Services.prefs.getBranch(PREF_BRANCH); + const isInternationalized = prefs.getBoolPref("internationalized.enable"); + + let that = { + /** + * @param {string} token + * @returns {string} + */ + get: function(token) { + if (isInternationalized && RfcParserI[token]) { + return RfcParserI[token]; + } + if (RfcParserStd[token]) { + return RfcParserStd[token]; + } + throw new Error(`Illegal RFC token: ${token}`); + }, + + TAG_PARSE_ERROR: TAG_PARSE_ERROR, + parseTagValueList: parseTagValueList, + parseTagValue: parseTagValue + }; + + return that; + +}()); \ No newline at end of file