mailauth 7.1.1 → 7.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/.c8rc.json +11 -0
  2. package/.prettierignore +3 -0
  3. package/.release-please-manifest.json +1 -1
  4. package/CHANGELOG.md +68 -0
  5. package/README.md +131 -31
  6. package/bin/mailauth.js +62 -0
  7. package/cli.md +150 -8
  8. package/docs/arc.md +62 -54
  9. package/docs/bimi.md +153 -54
  10. package/docs/dkim.md +37 -36
  11. package/docs/dkim2.md +154 -0
  12. package/docs/dmarc.md +4 -1
  13. package/docs/mta-sts.md +12 -9
  14. package/docs/spf.md +14 -13
  15. package/index.d.ts +583 -42
  16. package/lib/arc/index.d.ts +6 -2
  17. package/lib/arc/index.js +45 -15
  18. package/lib/bimi/index.js +362 -102
  19. package/lib/bimi/validate-svg.js +493 -60
  20. package/lib/commands/dkim2-hash.js +25 -0
  21. package/lib/commands/dkim2-sign.js +77 -0
  22. package/lib/commands/dkim2-verify.js +59 -0
  23. package/lib/commands/report.js +10 -35
  24. package/lib/commands/resolver.js +51 -0
  25. package/lib/commands/seal.js +10 -38
  26. package/lib/commands/spf.js +4 -35
  27. package/lib/dkim/dkim-signer.js +50 -30
  28. package/lib/dkim/dkim-verifier.js +79 -27
  29. package/lib/dkim/header/simple.js +2 -2
  30. package/lib/dkim/sign.js +7 -2
  31. package/lib/dkim/syntax.js +11 -0
  32. package/lib/dkim/verify.js +1 -0
  33. package/lib/dkim2/dkim2-parser.js +52 -0
  34. package/lib/dkim2/dkim2-signer.js +334 -0
  35. package/lib/dkim2/dkim2-verifier.js +572 -0
  36. package/lib/dkim2/fields.js +570 -0
  37. package/lib/dkim2/hash.d.ts +3 -0
  38. package/lib/dkim2/hash.js +51 -0
  39. package/lib/dkim2/key.js +203 -0
  40. package/lib/dkim2/message-state.js +181 -0
  41. package/lib/dkim2/recipe.js +134 -0
  42. package/lib/dkim2/sign.d.ts +3 -0
  43. package/lib/dkim2/sign.js +38 -0
  44. package/lib/dkim2/verify.d.ts +3 -0
  45. package/lib/dkim2/verify.js +19 -0
  46. package/lib/dmarc/author-domain.js +270 -5
  47. package/lib/dmarc/get-dmarc-record.js +24 -11
  48. package/lib/dmarc/verify.js +7 -3
  49. package/lib/mailauth.js +57 -14
  50. package/lib/mta-sts.js +37 -11
  51. package/lib/parse-received.js +214 -25
  52. package/lib/spf/index.js +21 -4
  53. package/lib/spf/macro.js +5 -3
  54. package/lib/spf/spf-verify.js +14 -5
  55. package/lib/spf/syntax.js +5 -0
  56. package/lib/tools.js +160 -21
  57. package/package.json +15 -6
  58. package/stryker.config.json +16 -0
@@ -1,93 +1,526 @@
1
1
  'use strict';
2
2
 
3
- const { XMLParser } = require('fast-xml-parser');
3
+ const { XMLParser, XMLValidator } = require('fast-xml-parser');
4
+ const { createError, addWarning } = require('../tools');
4
5
 
5
- function validateSvg(logo) {
6
- const parser = new XMLParser({
7
- ignoreAttributes: false,
8
- attributeNamePrefix: '@_'
6
+ // Validates a BIMI Indicator against the SVG Tiny Portable/Secure profile (draft-svg-tiny-ps-abrotman).
7
+ // Elements are checked against an allowlist with resolved namespaces. Script, interactivity, linking,
8
+ // multimedia and animation are not allowed (section 2.3), and every reference must point into the
9
+ // document itself. The checks also cover markup that an HTML parser would read differently than an
10
+ // XML parser, as some clients insert the SVG into an HTML document.
11
+
12
+ const SVG_NS = 'http://www.w3.org/2000/svg';
13
+ const XML_NS = 'http://www.w3.org/XML/1998/namespace';
14
+ const XMLNS_NS = 'http://www.w3.org/2000/xmlns/';
15
+ // elements in these namespaces are rendered or executed by browsers
16
+ const ACTIVE_NAMESPACES = new Set(['http://www.w3.org/1999/xhtml', 'http://www.w3.org/1998/Math/MathML']);
17
+
18
+ // The element set of the validation RNC schema (section 7)
19
+ const TINY_PS_ELEMENTS = new Set([
20
+ 'svg',
21
+ 'title',
22
+ 'desc',
23
+ 'metadata',
24
+ 'path',
25
+ 'rect',
26
+ 'circle',
27
+ 'line',
28
+ 'ellipse',
29
+ 'polyline',
30
+ 'polygon',
31
+ 'solidColor',
32
+ 'textArea',
33
+ 'linearGradient',
34
+ 'radialGradient',
35
+ 'stop',
36
+ 'text',
37
+ 'g',
38
+ 'defs',
39
+ 'use',
40
+ 'font',
41
+ 'glyph',
42
+ 'hkern'
43
+ ]);
44
+
45
+ // Static rendering elements of SVG 1.1 that are not part of the profile, but are found in published
46
+ // logos. They do not run code or load anything, references are checked like everywhere else.
47
+ const STATIC_ELEMENTS = new Set([
48
+ 'style',
49
+ 'clipPath',
50
+ 'mask',
51
+ 'pattern',
52
+ 'symbol',
53
+ 'marker',
54
+ 'tspan',
55
+ 'tbreak',
56
+ 'textPath',
57
+ 'missing-glyph',
58
+ 'vkern',
59
+ 'font-face',
60
+ 'font-face-src',
61
+ 'font-face-name',
62
+ 'filter',
63
+ 'feBlend',
64
+ 'feColorMatrix',
65
+ 'feComponentTransfer',
66
+ 'feComposite',
67
+ 'feConvolveMatrix',
68
+ 'feDiffuseLighting',
69
+ 'feDisplacementMap',
70
+ 'feDistantLight',
71
+ 'feDropShadow',
72
+ 'feFlood',
73
+ 'feFuncA',
74
+ 'feFuncB',
75
+ 'feFuncG',
76
+ 'feFuncR',
77
+ 'feGaussianBlur',
78
+ 'feMerge',
79
+ 'feMergeNode',
80
+ 'feMorphology',
81
+ 'feOffset',
82
+ 'fePointLight',
83
+ 'feSpecularLighting',
84
+ 'feSpotLight',
85
+ 'feTile',
86
+ 'feTurbulence'
87
+ ]);
88
+
89
+ const MAX_DEPTH = 256;
90
+
91
+ // Attributes that SHOULD NOT be present, and if present MUST have this value (sections 2.2 and 2.3)
92
+ const FIXED_ATTRIBUTE_VALUES = {
93
+ zoomAndPan: 'disable',
94
+ externalResourcesRequired: 'false',
95
+ focusable: 'false',
96
+ snapshotTime: 'none',
97
+ playbackOrder: 'all',
98
+ timelineBegin: 'onLoad',
99
+ editable: 'none'
100
+ };
101
+
102
+ const PREDEFINED_ENTITIES = { lt: '<', gt: '>', amp: '&', quot: '"', apos: "'" };
103
+
104
+ const parser = new XMLParser({
105
+ ignoreAttributes: false,
106
+ attributeNamePrefix: '',
107
+ preserveOrder: true,
108
+ // entities are decoded by decodeEntities(), the parser would leave some character references as is
109
+ processEntities: false,
110
+ htmlEntities: false,
111
+ parseTagValue: false,
112
+ parseAttributeValue: false,
113
+ trimValues: false,
114
+ ignoreDeclaration: false,
115
+ ignorePiTags: false,
116
+ commentPropName: '#comment',
117
+ cdataPropName: '#cdata',
118
+ textNodeName: '#text'
119
+ });
120
+
121
+ const invalidXml = (message, details) => createError('Invalid SVG file', 'INVALID_XML_FILE', { details: Object.assign({ message }, details || {}) });
122
+
123
+ // a link or a CSS value that could load a resource from outside the document
124
+ const externalReference = details => createError('External reference found from file', 'LOGO_INCLUDES_REFERENCE', { details });
125
+
126
+ const isXmlChar = code =>
127
+ code === 0x09 ||
128
+ code === 0x0a ||
129
+ code === 0x0d ||
130
+ (code >= 0x20 && code <= 0xd7ff) ||
131
+ (code >= 0xe000 && code <= 0xfffd) ||
132
+ (code >= 0x10000 && code <= 0x10ffff);
133
+
134
+ // Decodes the predefined entities and character references. Other entities are only defined in a
135
+ // DTD, which is not allowed.
136
+ const decodeEntities = value =>
137
+ value.replace(/&([^;&]*);?/g, (match, ref) => {
138
+ if (match.charAt(match.length - 1) !== ';') {
139
+ throw invalidXml('Unterminated entity reference');
140
+ }
141
+ if (Object.prototype.hasOwnProperty.call(PREDEFINED_ENTITIES, ref)) {
142
+ return PREDEFINED_ENTITIES[ref];
143
+ }
144
+ let code = /^#x[0-9a-fA-F]+$/.test(ref) ? parseInt(ref.slice(2), 16) : /^#[0-9]+$/.test(ref) ? parseInt(ref.slice(1), 10) : NaN;
145
+ if (!isXmlChar(code)) {
146
+ throw invalidXml(`Invalid entity reference &${ref};`);
147
+ }
148
+ return String.fromCodePoint(code);
9
149
  });
10
150
 
151
+ // Returns the reason why a CSS value or a presentation attribute might load an external resource
152
+ const checkCss = value => {
153
+ if (value.indexOf('\\') >= 0) {
154
+ // escapes could hide function names and URLs from the checks below
155
+ return 'CSS escape';
156
+ }
157
+ if (/@import/i.test(value)) {
158
+ return 'CSS import';
159
+ }
160
+ if (/(?:src|image|image-set|cross-fade|element)\s*\(/i.test(value)) {
161
+ return 'CSS image function';
162
+ }
163
+ let urlRe = /url\s*\(\s*(['"]?)\s*([^'")]*)/gi;
164
+ let match;
165
+ while ((match = urlRe.exec(value))) {
166
+ if (match[2].charAt(0) !== '#') {
167
+ return 'external url()';
168
+ }
169
+ }
170
+ return false;
171
+ };
172
+
173
+ const splitName = name => {
174
+ let pos = name.indexOf(':');
175
+ return pos >= 0 ? { prefix: name.slice(0, pos), local: name.slice(pos + 1) } : { prefix: '', local: name };
176
+ };
177
+
178
+ /**
179
+ * Validates an SVG Indicator against the SVG Tiny PS profile
180
+ *
181
+ * @param {Buffer|String} logo SVG document
182
+ * @param {Object} [opts]
183
+ * @param {Boolean} [opts.strict=false] Apply the profile exactly: the version attribute, a single title as
184
+ * the first child, only the elements of the validation schema and the required attribute values
185
+ * @param {Array} [opts.warnings] Without strict, markers of the profile deviations that were accepted are added here
186
+ * @returns {Boolean} true, throws if the document is not valid
187
+ */
188
+ function validateSvg(logo, opts) {
189
+ opts = opts || {};
190
+ const strict = !!opts.strict;
191
+ const warnings = Array.isArray(opts.warnings) ? opts.warnings : [];
192
+
193
+ // a deviation from the profile that does not make the logo unsafe, an error only in strict mode
194
+ const lax = (marker, error) => {
195
+ if (strict) {
196
+ throw error;
197
+ }
198
+ addWarning(warnings, marker);
199
+ };
200
+ let source = Buffer.isBuffer(logo) ? logo.toString('utf-8') : String(logo || '');
201
+ if (source.charCodeAt(0) === 0xfeff) {
202
+ source = source.slice(1);
203
+ }
204
+
11
205
  let logoObj;
12
206
  try {
13
- logoObj = parser.parse(logo);
14
- if (!logoObj) {
15
- throw new Error('Emtpy file');
207
+ if (!source.trim()) {
208
+ throw new Error('Empty file');
209
+ }
210
+ logoObj = parser.parse(source);
211
+ if (!Array.isArray(logoObj)) {
212
+ throw new Error('Empty file');
16
213
  }
17
214
  } catch (err) {
18
- let error = new Error('Invalid SVG file');
19
- error._err = err;
20
- error.code = 'INVALID_XML_FILE';
21
- throw error;
215
+ throw createError('Invalid SVG file', 'INVALID_XML_FILE', { _err: err });
22
216
  }
23
217
 
24
- if (!logoObj.svg) {
25
- let error = new Error('Invalid SVG file');
26
- error.code = 'INVALID_SVG_FILE';
27
- throw error;
218
+ const nodeName = node => Object.keys(node).find(key => key !== ':@');
219
+
220
+ let root = logoObj.find(node => /^[^#?]/.test(nodeName(node) || ''));
221
+ if (!root || splitName(nodeName(root)).local !== 'svg') {
222
+ throw createError('Invalid SVG file', 'INVALID_SVG_FILE');
28
223
  }
29
224
 
30
- if (logoObj.svg['@_baseProfile'] !== 'tiny-ps') {
31
- let error = new Error('Not a Tiny PS profile');
32
- error.code = 'INVALID_BASE_PROFILE';
33
- throw error;
225
+ // the parser accepts mismatched tags, repeated attributes and similar errors, the validator does not
226
+ let validation = XMLValidator.validate(source);
227
+ if (validation !== true) {
228
+ throw invalidXml(validation?.err?.msg || 'Invalid XML');
34
229
  }
35
230
 
36
- if (!logoObj.svg.title) {
37
- let error = new Error('Logo file is missing title');
38
- error.code = 'LOGO_MISSING_TITLE';
39
- throw error;
231
+ const childText = node => (node[nodeName(node)] || []).map(entry => entry['#text'] || '').join('');
232
+
233
+ const checkComment = (node, path) => {
234
+ // "<!-->" and "--!>" end a comment in HTML
235
+ let text = childText(node);
236
+ if (/[<>]|--/.test(text) || /^-|-$/.test(text)) {
237
+ throw createError('Unexpected content in comment', 'LOGO_INVALID_CONTENT', { details: { path } });
238
+ }
239
+ };
240
+
241
+ // An internal DTD subset can declare entities and default attributes, the parser and a browser
242
+ // would not see the same document. The DOCTYPE of SVG 1.1 is allowed, it is ignored by browsers.
243
+ let doctypeRe = /<!doctype/gi;
244
+ let doctypeMatch;
245
+ while ((doctypeMatch = doctypeRe.exec(source))) {
246
+ let tail = source.slice(doctypeMatch.index);
247
+ if (!/^<!DOCTYPE\s+svg(?:\s+PUBLIC\s+"[^"<>[\]]*"\s+"[^"<>[\]]*"|\s+SYSTEM\s+"[^"<>[\]]*")?\s*>/.test(tail)) {
248
+ throw invalidXml('Unsupported DOCTYPE declaration');
249
+ }
250
+ }
251
+ if (/<!ENTITY/i.test(source)) {
252
+ throw invalidXml('Entity declarations are not allowed');
40
253
  }
41
254
 
42
- if ('@_x' in logoObj.svg || '@_y' in logoObj.svg) {
43
- let error = new Error('Logo root includes x/y attributes');
44
- error.code = 'LOGO_INVALID_ROOT_ATTRS';
45
- throw error;
255
+ let rootCount = 0;
256
+ for (let [i, node] of logoObj.entries()) {
257
+ let name = nodeName(node);
258
+ if (name === '#text') {
259
+ if ((node['#text'] || '').trim()) {
260
+ throw invalidXml('Text outside of the root element');
261
+ }
262
+ } else if (name === '#comment') {
263
+ checkComment(node, '');
264
+ } else if (name === '?xml') {
265
+ if (i !== 0) {
266
+ throw invalidXml('Misplaced XML declaration');
267
+ }
268
+ let attrs = node[':@'] || {};
269
+ for (let [key, value] of Object.entries(attrs)) {
270
+ if (
271
+ !(key === 'version' && /^1\.[0-9]+$/.test(value)) &&
272
+ !(key === 'encoding' && /^[A-Za-z][A-Za-z0-9._-]*$/.test(value)) &&
273
+ !(key === 'standalone' && /^(?:yes|no)$/.test(value))
274
+ ) {
275
+ throw invalidXml('Invalid XML declaration');
276
+ }
277
+ }
278
+ } else if (name && name.charAt(0) === '?') {
279
+ // for example xml-stylesheet, which loads an external style sheet
280
+ throw createError('Processing instructions are not allowed', 'LOGO_INVALID_CONTENT', { details: { instruction: name.slice(1) } });
281
+ } else if (name && ++rootCount > 1) {
282
+ throw invalidXml('Multiple root elements');
283
+ }
46
284
  }
47
285
 
48
- let walkElm = (node, name, path) => {
49
- if (!node) {
50
- return;
286
+ // returns the namespace of a prefix in the current scope
287
+ const resolvePrefix = (scope, prefix, path) => {
288
+ if (prefix === 'xml') {
289
+ return XML_NS;
51
290
  }
52
- if (Array.isArray(node)) {
53
- for (let entry of node) {
54
- walkElm(entry, name, path + '.' + name + '[]');
55
- }
56
- } else if (typeof node === 'object') {
57
- if (node['@_xlink:href'] && !/^#/.test(node['@_xlink:href'])) {
58
- let error = new Error('External reference found from file');
59
- error.details = {
60
- element: name,
61
- link: node['@_xlink:href'],
62
- path
63
- };
64
- error.code = 'LOGO_INCLUDES_REFERENCE';
65
- throw error;
291
+ if (prefix === 'xmlns') {
292
+ return XMLNS_NS;
293
+ }
294
+ if (!Object.prototype.hasOwnProperty.call(scope, prefix)) {
295
+ if (!prefix) {
296
+ return null;
66
297
  }
298
+ throw invalidXml(`Undeclared namespace prefix "${prefix}"`, { path });
299
+ }
300
+ return scope[prefix];
301
+ };
302
+
303
+ let rootAttrs = null;
304
+ let titleCount = 0;
305
+ let titleText = '';
306
+ let firstChild = true;
307
+
308
+ // checks the content of a text node or a CDATA section
309
+ const checkText = (value, path) => {
310
+ // Text of a CDATA section is not escaped. An HTML parser does not handle CDATA in title or
311
+ // desc, and would read a tag from it.
312
+ if (value.indexOf('<') >= 0) {
313
+ throw createError('Unexpected markup in text content', 'LOGO_INVALID_CONTENT', { details: { path } });
314
+ }
315
+ };
316
+
317
+ const walk = (node, scope, path, depth, inMetadata) => {
318
+ if (depth > MAX_DEPTH) {
319
+ throw createError('Document is nested too deeply', 'LOGO_INVALID_CONTENT', { details: { path } });
320
+ }
67
321
 
68
- for (let key of Object.keys(node)) {
69
- if (['script', 'animate', 'animatemotion', 'animatetransform', 'discard', 'set'].includes(key.toLowerCase())) {
70
- let error = new Error('Unallowed element found from file');
71
- error.details = {
72
- element: key,
73
- path: path + '.' + key
74
- };
75
- error.code = 'LOGO_INVALID_ELEMENT';
76
- throw error;
322
+ let qname = nodeName(node);
323
+ let attrs = node[':@'] || {};
324
+
325
+ // namespace declarations of this element
326
+ let localScope = scope;
327
+ for (let [key, value] of Object.entries(attrs)) {
328
+ if (key === 'xmlns' || key.indexOf('xmlns:') === 0) {
329
+ if (localScope === scope) {
330
+ localScope = Object.assign({}, scope);
77
331
  }
332
+ let decoded = decodeEntities(value);
333
+ localScope[key === 'xmlns' ? '' : key.slice(6)] = decoded || null;
334
+ }
335
+ }
336
+
337
+ let { prefix, local } = splitName(qname);
338
+ let namespace = resolvePrefix(localScope, prefix, path);
339
+ let elementPath = `${path}.${qname}`;
340
+
341
+ if (depth === 0 && namespace !== SVG_NS) {
342
+ // 2.1: xmlns="http://www.w3.org/2000/svg", in any other namespace this is not an SVG document
343
+ throw createError('Invalid SVG file', 'INVALID_SVG_FILE', { details: { namespace } });
344
+ }
345
+ let isSvg = namespace === SVG_NS;
346
+
347
+ // attributes are checked first, so an external reference is reported as such
348
+ for (let [key, rawValue] of Object.entries(attrs)) {
349
+ if (key === 'xmlns' || key.indexOf('xmlns:') === 0) {
350
+ continue;
351
+ }
352
+ let attr = splitName(key);
353
+ // an unprefixed attribute has no namespace
354
+ let attrNs = attr.prefix ? resolvePrefix(localScope, attr.prefix, elementPath) : null;
355
+ if (rawValue.indexOf('<') >= 0) {
356
+ throw invalidXml('Unescaped "<" in an attribute value', { path: elementPath, attribute: key });
357
+ }
358
+ let value = decodeEntities(rawValue);
78
359
 
79
- if (Array.isArray(node[key])) {
80
- for (let entry of node[key]) {
81
- walkElm(entry, key, path + '.' + key + '[]');
360
+ if (/^on/i.test(attr.local)) {
361
+ throw createError('Event handler attribute found from file', 'LOGO_INVALID_ATTRIBUTE', {
362
+ details: {
363
+ element: qname,
364
+ attribute: key,
365
+ path: elementPath
82
366
  }
83
- } else if (node[key] && typeof node[key] === 'object') {
84
- walkElm(node[key], key, path + '.' + key);
367
+ });
368
+ }
369
+
370
+ // href in the XLink namespace, without namespace (SVG 2), or with any prefix an HTML parser
371
+ // could map to XLink. Only same-document references are allowed (section 2.3, Linking).
372
+ if (attr.local.toLowerCase() === 'href' && value.trim().charAt(0) !== '#') {
373
+ throw externalReference({ element: qname, link: value, path: elementPath });
374
+ }
375
+
376
+ if (
377
+ isSvg &&
378
+ !attr.prefix &&
379
+ Object.prototype.hasOwnProperty.call(FIXED_ATTRIBUTE_VALUES, attr.local) &&
380
+ value.trim() !== FIXED_ATTRIBUTE_VALUES[attr.local]
381
+ ) {
382
+ lax(
383
+ 'svg-attribute-value',
384
+ createError(`${attr.local} must be "${FIXED_ATTRIBUTE_VALUES[attr.local]}"`, 'LOGO_INVALID_ATTRIBUTE', {
385
+ details: {
386
+ element: qname,
387
+ attribute: key,
388
+ value,
389
+ path: elementPath
390
+ }
391
+ })
392
+ );
393
+ }
394
+
395
+ if (attrNs === XML_NS && attr.local === 'base' && value.trim()) {
396
+ // a base URI could turn a fragment reference into an external one
397
+ throw createError('xml:base attribute found from file', 'LOGO_INVALID_ATTRIBUTE', {
398
+ details: { element: qname, attribute: key, path: elementPath }
399
+ });
400
+ }
401
+
402
+ // presentation attributes and the style attribute are parsed as CSS
403
+ let cssError = checkCss(value);
404
+ if (cssError) {
405
+ throw externalReference({ element: qname, attribute: key, link: value, reason: cssError, path: elementPath });
406
+ }
407
+ }
408
+
409
+ const invalidElement = reason =>
410
+ createError('Unallowed element found from file', 'LOGO_INVALID_ELEMENT', {
411
+ details: {
412
+ element: qname,
413
+ namespace,
414
+ reason,
415
+ path: elementPath
85
416
  }
417
+ });
418
+
419
+ // An HTML parser ignores namespace declarations and treats an unprefixed element in SVG
420
+ // content as an SVG element, so an unprefixed name must be allowed in any namespace.
421
+ if ((isSvg || !prefix) && !TINY_PS_ELEMENTS.has(local) && !STATIC_ELEMENTS.has(local)) {
422
+ throw invalidElement('not allowed in SVG Tiny PS');
423
+ }
424
+ if (namespace && ACTIVE_NAMESPACES.has(namespace)) {
425
+ throw invalidElement('not allowed in SVG Tiny PS');
426
+ }
427
+ if (!prefix && local === 'font' && ['color', 'face', 'size'].some(key => key in attrs)) {
428
+ // an HTML parser would end the SVG content here and continue in HTML
429
+ throw invalidElement('font element with HTML attributes');
430
+ }
431
+ if (inMetadata && isSvg) {
432
+ throw invalidElement('SVG element in metadata');
433
+ }
434
+
435
+ // the validation schema (section 7) does not allow these, they are not unsafe
436
+ if (inMetadata) {
437
+ // metadata only has text content in the schema
438
+ lax('svg-metadata-content', invalidElement('element in metadata'));
439
+ } else if (!isSvg) {
440
+ lax('svg-foreign-element', invalidElement('element in another namespace'));
441
+ } else if (STATIC_ELEMENTS.has(local)) {
442
+ lax('svg-element', invalidElement('not part of the SVG Tiny PS profile'));
443
+ }
444
+
445
+ if (depth === 0) {
446
+ rootAttrs = attrs;
447
+ } else if (isSvg && local === 'title' && depth === 1) {
448
+ // 2.1: one non-empty title, the schema has it as the first child of the root element
449
+ titleCount++;
450
+ if (titleCount === 1) {
451
+ titleText = (node[qname] || [])
452
+ .map(child => (child['#text'] !== undefined ? decodeEntities(child['#text']) : child['#cdata'] ? childText(child) : ''))
453
+ .join('');
454
+ if (!firstChild) {
455
+ lax(
456
+ 'svg-title-position',
457
+ createError('The title element must be the first child of the svg element', 'LOGO_INVALID_ELEMENT', { details: { path: elementPath } })
458
+ );
459
+ }
460
+ } else {
461
+ lax('svg-title-count', createError('Logo file has more than one title', 'LOGO_MULTIPLE_TITLES', { details: { path: elementPath } }));
462
+ }
463
+ } else if (isSvg && local === 'title') {
464
+ lax(
465
+ 'svg-title-position',
466
+ createError('The title element must be a child of the svg element', 'LOGO_INVALID_ELEMENT', { details: { path: elementPath } })
467
+ );
468
+ } else if (isSvg && local === 'desc' && !childText(node).trim()) {
469
+ // 2.1: if present, the contents of desc MUST NOT be empty
470
+ lax('svg-empty-desc', createError('The desc element is empty', 'LOGO_INVALID_CONTENT', { details: { path: elementPath } }));
471
+ }
472
+ if (depth === 1) {
473
+ firstChild = false;
474
+ }
475
+
476
+ let isStyle = isSvg && local === 'style';
477
+ const checkStyle = text => {
478
+ let cssError = isStyle && checkCss(text);
479
+ if (cssError) {
480
+ throw externalReference({ element: qname, link: text, reason: cssError, path: elementPath });
481
+ }
482
+ };
483
+ let childMetadata = inMetadata || (isSvg && local === 'metadata');
484
+
485
+ for (let child of node[qname] || []) {
486
+ let childName = nodeName(child);
487
+ if (childName === '#text') {
488
+ checkText(child['#text'] || '', elementPath);
489
+ checkStyle(decodeEntities(child['#text'] || ''));
490
+ } else if (childName === '#cdata') {
491
+ let text = childText(child);
492
+ checkText(text, elementPath);
493
+ checkStyle(text);
494
+ } else if (childName === '#comment') {
495
+ checkComment(child, elementPath);
496
+ } else if (childName && childName.charAt(0) === '?') {
497
+ throw createError('Processing instructions are not allowed', 'LOGO_INVALID_CONTENT', {
498
+ details: { instruction: childName.slice(1), path: elementPath }
499
+ });
500
+ } else if (childName) {
501
+ walk(child, localScope, elementPath, depth + 1, childMetadata);
86
502
  }
87
503
  }
88
504
  };
89
505
 
90
- walkElm(logoObj, 'root', '');
506
+ walk(root, {}, '', 0, false);
507
+
508
+ if (rootAttrs.baseProfile !== 'tiny-ps') {
509
+ throw createError('Not a Tiny PS profile', 'INVALID_BASE_PROFILE');
510
+ }
511
+
512
+ if (!titleCount || !titleText.trim()) {
513
+ throw createError('Logo file is missing title', 'LOGO_MISSING_TITLE');
514
+ }
515
+
516
+ if ('x' in rootAttrs || 'y' in rootAttrs) {
517
+ throw createError('Logo root includes x/y attributes', 'LOGO_INVALID_ROOT_ATTRS');
518
+ }
519
+
520
+ if (rootAttrs.version !== '1.2') {
521
+ // 2.1: version="1.2"
522
+ lax('svg-version', createError('SVG version must be 1.2', 'INVALID_SVG_VERSION', { details: { version: rootAttrs.version } }));
523
+ }
91
524
 
92
525
  // all validations passed
93
526
  return true;
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+
3
+ const { dkim2Hash } = require('../dkim2/hash');
4
+ const fs = require('node:fs');
5
+
6
+ const cmd = async argv => {
7
+ let source = argv.email;
8
+ if (argv.verbose) {
9
+ console.error(`Reading email message from ${source || 'standard input'}`);
10
+ }
11
+
12
+ let { headers, hashes } = await dkim2Hash(source ? fs.createReadStream(source) : process.stdin, { algorithms: argv.algo });
13
+
14
+ if (argv.verbose) {
15
+ console.error(`Hashed header fields: ${headers.join(', ') || '(none)'}`);
16
+ console.error('--------');
17
+ }
18
+
19
+ // the hash-sets of the h= tag of a Message-Instance (section 7.3)
20
+ for (let { algorithm, headerHash, bodyHash } of hashes) {
21
+ process.stdout.write(`${algorithm}:${headerHash}:${bodyHash}\n`);
22
+ }
23
+ };
24
+
25
+ module.exports = cmd;