mailauth 6.0.0 → 7.1.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 (47) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/CHANGELOG.md +25 -0
  3. package/README.md +113 -39
  4. package/bin/mailauth.js +15 -6
  5. package/cli.md +9 -3
  6. package/docs/arc.md +126 -43
  7. package/docs/bimi.md +54 -37
  8. package/docs/dkim.md +79 -40
  9. package/docs/dmarc.md +80 -33
  10. package/docs/mta-sts.md +140 -42
  11. package/docs/spf.md +69 -4
  12. package/index.d.ts +508 -43
  13. package/lib/arc/index.d.ts +10 -8
  14. package/lib/arc/index.js +505 -124
  15. package/lib/bimi/index.js +162 -81
  16. package/lib/commands/report.js +3 -1
  17. package/lib/commands/seal.js +35 -5
  18. package/lib/commands/sign.js +8 -1
  19. package/lib/commands/spf.js +8 -0
  20. package/lib/dkim/body/relaxed.js +6 -5
  21. package/lib/dkim/body/simple.js +1 -3
  22. package/lib/dkim/dkim-signer.js +219 -33
  23. package/lib/dkim/dkim-verifier.js +577 -231
  24. package/lib/dkim/header/relaxed.js +6 -43
  25. package/lib/dkim/header/signature-opts.js +87 -0
  26. package/lib/dkim/header/simple.js +6 -43
  27. package/lib/dkim/message-parser.js +18 -4
  28. package/lib/dkim/sign.d.ts +13 -7
  29. package/lib/dkim/sign.js +18 -2
  30. package/lib/dkim/syntax.js +31 -0
  31. package/lib/dkim/verify.js +1 -0
  32. package/lib/dmarc/author-domain.js +181 -0
  33. package/lib/dmarc/get-dmarc-record.js +154 -47
  34. package/lib/dmarc/index.d.ts +29 -2
  35. package/lib/dmarc/verify.js +120 -44
  36. package/lib/mailauth.js +77 -36
  37. package/lib/mta-sts.d.ts +77 -11
  38. package/lib/mta-sts.js +497 -182
  39. package/lib/parse-dkim-headers.js +376 -191
  40. package/lib/parse-received.js +104 -32
  41. package/lib/spf/index.d.ts +37 -1
  42. package/lib/spf/index.js +260 -28
  43. package/lib/spf/macro.js +50 -13
  44. package/lib/spf/spf-verify.js +440 -204
  45. package/lib/spf/syntax.js +366 -0
  46. package/lib/tools.js +522 -116
  47. package/package.json +1 -1
@@ -1,6 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  const { DANGEROUS_KEYS } = require('./safe-keys');
4
+ const { isIP } = require('node:net');
5
+
6
+ const TLS_COMMENT = /tls|cipher=|Google Transport Security/i;
4
7
 
5
8
  const parseReceived = buf => {
6
9
  let header = (buf || '').toString();
@@ -31,6 +34,29 @@ const parseReceived = buf => {
31
34
 
32
35
  let curValue = nextValue();
33
36
 
37
+ // adds a character of a key or a value, starting the next one where needed
38
+ let appendChar = c => {
39
+ if (state === 'none') {
40
+ state = 'val';
41
+ switch (curKey) {
42
+ case '':
43
+ curKey = 'key';
44
+ break;
45
+ case 'key':
46
+ curKey = 'value';
47
+ break;
48
+ case 'value':
49
+ case 'comment':
50
+ curValue = nextValue();
51
+ curKey = 'key';
52
+ break;
53
+ }
54
+ } else if (curKey === 'comment' && c === '(') {
55
+ commentLevel++;
56
+ }
57
+ curValue[curKey] += c;
58
+ };
59
+
34
60
  for (let i = 0; i < headerValue.length; i++) {
35
61
  let c = headerValue.charAt(i);
36
62
 
@@ -46,12 +72,16 @@ const parseReceived = buf => {
46
72
  }
47
73
 
48
74
  if (quoted) {
49
- if (c === quoted) {
50
- quoted = false;
51
- state = 'none';
75
+ // RFC 5322 quoted-string, kept as it is, quotes included, so that an address
76
+ // such as <"x;y"@example.com> keeps its local-part
77
+ if (c === '\\' && i < headerValue.length - 1) {
78
+ curValue[curKey] += c + headerValue.charAt(++i);
52
79
  continue;
53
80
  }
54
81
  curValue[curKey] += c;
82
+ if (c === '"') {
83
+ quoted = false;
84
+ }
55
85
  continue;
56
86
  }
57
87
 
@@ -85,8 +115,9 @@ const parseReceived = buf => {
85
115
  state = 'none';
86
116
  break;
87
117
  case '"':
88
- case "'":
89
- // start quoting
118
+ // start quoting. Only the double quote quotes (RFC 5322 section 3.2.4), an
119
+ // apostrophe is an ordinary character, as in <o'brien@example.com>
120
+ appendChar(c);
90
121
  quoted = c;
91
122
  break;
92
123
  case '(':
@@ -102,30 +133,7 @@ const parseReceived = buf => {
102
133
  escaped = true;
103
134
  break;
104
135
  default:
105
- if (state === 'none') {
106
- state = 'val';
107
- switch (curKey) {
108
- case '':
109
- curKey = 'key';
110
- curValue[curKey] += c;
111
- break;
112
- case 'key':
113
- curKey = 'value';
114
- curValue[curKey] += c;
115
- break;
116
- case 'value':
117
- case 'comment':
118
- curValue = nextValue();
119
- curKey = 'key';
120
- curValue[curKey] += c;
121
- break;
122
- }
123
- } else {
124
- if (curKey === 'comment' && c === '(') {
125
- commentLevel++;
126
- }
127
- curValue[curKey] += c;
128
- }
136
+ appendChar(c);
129
137
  }
130
138
  }
131
139
 
@@ -148,13 +156,27 @@ const parseReceived = buf => {
148
156
  }
149
157
  }
150
158
 
159
+ // A from clause can have more than one comment after it. Postfix writes the TCP-info
160
+ // comment after the HELO name, and a HELO name with parentheses in it looks like a
161
+ // comment of its own, as in "from x([192.0.2.1]) (unknown [198.51.100.7])". The comments
162
+ // are kept together, in order, so that the TCP-info comment is always the last part
163
+ for (let i = 1; i < values.length; i++) {
164
+ let val = values[i];
165
+ let prev = values[i - 1];
166
+ if (!val.key && !val.value && val.comment && (prev.key || '').toLowerCase() === 'from' && !TLS_COMMENT.test(val.comment)) {
167
+ prev.comment = [prev.comment || []].concat(val.comment).join(' ');
168
+ values.splice(i, 1);
169
+ i--;
170
+ }
171
+ }
172
+
151
173
  for (let val of values) {
152
174
  if (val.comment) {
153
175
  val.comment = val.comment.replace(/\s+/g, ' ').trim();
154
176
  }
155
177
  if (val.key) {
156
178
  let key = val.key.toLowerCase();
157
- if (key !== 'from' && !result.tls && /tls|cipher=|Google Transport Security/i.test(val.comment)) {
179
+ if (key !== 'from' && !result.tls && TLS_COMMENT.test(val.comment)) {
158
180
  result.tls = { value: '', ...(val.comment && { comment: val.comment }) };
159
181
  val.comment = '';
160
182
  }
@@ -163,7 +185,7 @@ const parseReceived = buf => {
163
185
  continue;
164
186
  }
165
187
  result[key] = { value: val.value, ...(val.comment && { comment: val.comment }) };
166
- } else if (!result.tls && /tls|cipher=|Google Transport Security/i.test(val.comment)) {
188
+ } else if (!result.tls && TLS_COMMENT.test(val.comment)) {
167
189
  result.tls = { value: val.value, ...(val.comment && { comment: val.comment }) };
168
190
  }
169
191
  }
@@ -181,4 +203,54 @@ const parseReceived = buf => {
181
203
  return result;
182
204
  };
183
205
 
184
- module.exports = { parseReceived };
206
+ // An address literal (RFC 5321 section 4.1.3), with an optional port after it as Exim writes
207
+ // it. The content is checked with isIP separately
208
+ const ADDRESS_LITERAL = /\[(?:IPv6:)?([^\]\s]+)\](?::\d+)?/gi;
209
+ // "helo=" or "ehlo=" right before a literal, the client's own claim, not its address
210
+ const HELO_PREFIX = /(?:^|[\s(;,])(?:helo|ehlo)=\s*$/i;
211
+
212
+ /**
213
+ * Finds the IP address of the client in the from clause of a parsed Received header.
214
+ *
215
+ * RFC 5321 section 4.4 puts the address the connection came from into the TCP-info comment
216
+ * of the from clause, "from helo.name (rdns.name [192.0.2.1])", so it is the last address
217
+ * literal of that comment. The rest of the clause is the client's own claim: the HELO name,
218
+ * which can itself be an address literal, and in Exim's format a "helo=" value inside the
219
+ * comment. Those are never used. Exim writes "from [192.0.2.1] (helo=...)" for a client
220
+ * without reverse DNS, where the literal in front of the comment is the address of the
221
+ * connection, which is only used when the comment has no address literal of its own.
222
+ *
223
+ * @param {Object} from The "from" entry of parseReceived(), { value, comment }
224
+ * @returns {String|false} IP address, without an "IPv6:" prefix, or false if none was found
225
+ */
226
+ const getClientAddress = from => {
227
+ if (!from || typeof from !== 'object') {
228
+ return false;
229
+ }
230
+
231
+ let comment = typeof from.comment === 'string' ? from.comment : '';
232
+ let value = typeof from.value === 'string' ? from.value.trim() : '';
233
+
234
+ let address = false;
235
+ for (let match of comment.matchAll(ADDRESS_LITERAL)) {
236
+ if (HELO_PREFIX.test(comment.substring(0, match.index))) {
237
+ continue;
238
+ }
239
+ if (isIP(match[1])) {
240
+ address = match[1];
241
+ }
242
+ }
243
+
244
+ if (address) {
245
+ return address;
246
+ }
247
+
248
+ let valueMatch = value.match(/^\[(?:IPv6:)?([^\]\s]+)\](?::\d+)?$/i);
249
+ if (valueMatch && comment && isIP(valueMatch[1])) {
250
+ return valueMatch[1];
251
+ }
252
+
253
+ return false;
254
+ };
255
+
256
+ module.exports = { parseReceived, getClientAddress };
@@ -1,6 +1,42 @@
1
1
  // Type definitions for mailauth/lib/spf
2
2
 
3
- import { SPFOptions, SPFResult } from '../../index';
3
+ import { SPFOptions as BaseSPFOptions, SPFResult as BaseSPFResult } from '../../index';
4
+
5
+ /**
6
+ * SPF verification options
7
+ */
8
+ export interface SPFOptions extends BaseSPFOptions {
9
+ /**
10
+ * Follow RFC 7208 exactly instead of the lenient default (default: false). Strict mode
11
+ * validates the whole record before evaluation, rejects the c, r and t macros outside of
12
+ * explanation text, counts void lookups per RFC 7208 section 4.6.4, discards records with
13
+ * leading whitespace, and reports only the checked identity in Authentication-Results
14
+ */
15
+ strict?: boolean;
16
+
17
+ /**
18
+ * Maximum time in milliseconds for the whole evaluation. If exceeded, the result is
19
+ * temperror (RFC 7208 section 4.6.4). No limit by default
20
+ */
21
+ maxElapsedTime?: number;
22
+ }
23
+
24
+ /**
25
+ * SPF verification result
26
+ */
27
+ export interface SPFResult extends BaseSPFResult {
28
+ /**
29
+ * Explanation string published by the domain owner with the "exp" modifier
30
+ * (RFC 7208 section 6.2). Only set for a "fail" result. This is third party text
31
+ */
32
+ explanation?: string;
33
+
34
+ /**
35
+ * Set in the default mode when something was accepted that strict mode would have
36
+ * rejected: "syntax-error", "void-lookup-limit" or "non-ascii-local-part"
37
+ */
38
+ warnings?: string[];
39
+ }
4
40
 
5
41
  /**
6
42
  * Verifies SPF for a sender