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.
- package/.c8rc.json +11 -0
- package/.prettierignore +3 -0
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +68 -0
- package/README.md +131 -31
- package/bin/mailauth.js +62 -0
- package/cli.md +150 -8
- package/docs/arc.md +62 -54
- package/docs/bimi.md +153 -54
- package/docs/dkim.md +37 -36
- package/docs/dkim2.md +154 -0
- package/docs/dmarc.md +4 -1
- package/docs/mta-sts.md +12 -9
- package/docs/spf.md +14 -13
- package/index.d.ts +583 -42
- package/lib/arc/index.d.ts +6 -2
- package/lib/arc/index.js +45 -15
- package/lib/bimi/index.js +362 -102
- package/lib/bimi/validate-svg.js +493 -60
- package/lib/commands/dkim2-hash.js +25 -0
- package/lib/commands/dkim2-sign.js +77 -0
- package/lib/commands/dkim2-verify.js +59 -0
- package/lib/commands/report.js +10 -35
- package/lib/commands/resolver.js +51 -0
- package/lib/commands/seal.js +10 -38
- package/lib/commands/spf.js +4 -35
- package/lib/dkim/dkim-signer.js +50 -30
- package/lib/dkim/dkim-verifier.js +79 -27
- package/lib/dkim/header/simple.js +2 -2
- package/lib/dkim/sign.js +7 -2
- package/lib/dkim/syntax.js +11 -0
- package/lib/dkim/verify.js +1 -0
- package/lib/dkim2/dkim2-parser.js +52 -0
- package/lib/dkim2/dkim2-signer.js +334 -0
- package/lib/dkim2/dkim2-verifier.js +572 -0
- package/lib/dkim2/fields.js +570 -0
- package/lib/dkim2/hash.d.ts +3 -0
- package/lib/dkim2/hash.js +51 -0
- package/lib/dkim2/key.js +203 -0
- package/lib/dkim2/message-state.js +181 -0
- package/lib/dkim2/recipe.js +134 -0
- package/lib/dkim2/sign.d.ts +3 -0
- package/lib/dkim2/sign.js +38 -0
- package/lib/dkim2/verify.d.ts +3 -0
- package/lib/dkim2/verify.js +19 -0
- package/lib/dmarc/author-domain.js +270 -5
- package/lib/dmarc/get-dmarc-record.js +24 -11
- package/lib/dmarc/verify.js +7 -3
- package/lib/mailauth.js +57 -14
- package/lib/mta-sts.js +37 -11
- package/lib/parse-received.js +214 -25
- package/lib/spf/index.js +21 -4
- package/lib/spf/macro.js +5 -3
- package/lib/spf/spf-verify.js +14 -5
- package/lib/spf/syntax.js +5 -0
- package/lib/tools.js +160 -21
- package/package.json +15 -6
- package/stryker.config.json +16 -0
package/lib/bimi/validate-svg.js
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
throw
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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 (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
}
|
|
84
|
-
|
|
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
|
-
|
|
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;
|