@zone-eu/mailsplit 5.4.26 → 5.4.28

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/README.md CHANGED
@@ -29,9 +29,9 @@ let splitter = new Splitter(options);
29
29
 
30
30
  Where
31
31
 
32
- - **options** is an optional options object
33
- - **options.ignoreEmbedded** (boolean, defaults to false) if true then treat message/rfc822 node as normal leaf node and do not try to parse it
34
- - **options.maxHeadSize** (number, defaults to 1 MiB) limits the header size of a MIME node in bytes, every header line counting for at least 64 bytes
32
+ - **options** is an optional options object
33
+ - **options.ignoreEmbedded** (boolean, defaults to false) if true then treat message/rfc822 node as normal leaf node and do not try to parse it
34
+ - **options.maxHeadSize** (number, defaults to 1 MiB) limits the header size of a MIME node in bytes, every header line counting for at least 64 bytes
35
35
 
36
36
  #### Events
37
37
 
@@ -39,13 +39,13 @@ Where
39
39
 
40
40
  #### Data objects
41
41
 
42
- - **type**
43
- - `'node'` means that we reached the next mime node and the previous one is completely processed
44
- - `'data'` provides us multipart body parts, including boundaries. This data is not directly related to any specific multipart node, basically it includes everything between the end of one normal node and the header of next node
45
- - `'body'` provides us next chunk for the last seen `'node'` element
46
- - **value** is a buffer value for `'body'` and `'data'` parts
47
- - **getDecoder()** is a function that returns a stream object you can use to decode node contents. Write data from 'body' to decoder and read decoded Buffer value out from it
48
- - **getEncoder()** is a function that returns a stream object you can use to encode node contents. Write buffer data to encoder and read encoded object value out that you can pass to a Joiner
42
+ - **type**
43
+ - `'node'` means that we reached the next mime node and the previous one is completely processed
44
+ - `'data'` provides us multipart body parts, including boundaries. This data is not directly related to any specific multipart node, basically it includes everything between the end of one normal node and the header of next node
45
+ - `'body'` provides us next chunk for the last seen `'node'` element
46
+ - **value** is a buffer value for `'body'` and `'data'` parts
47
+ - **getDecoder()** is a function that returns a stream object you can use to decode node contents. Write data from 'body' to decoder and read decoded Buffer value out from it
48
+ - **getEncoder()** is a function that returns a stream object you can use to encode node contents. Write buffer data to encoder and read encoded object value out that you can pass to a Joiner
49
49
 
50
50
  Element with type `'node'` has a bunch of header related methods and properties, see [below](#manipulating-headers).
51
51
 
@@ -82,31 +82,31 @@ someMessagStream.pipe(splitter);
82
82
 
83
83
  If the data object has `type='node'` then you can modify headers for that node (headers can be modified until the data object is passed over to a `Joiner`)
84
84
 
85
- - **node.getHeaders()** returns a Buffer value with generated headers. If you have not modified the headers object in any way then you should get the exact copy of the original. In case you have done something (for example removed a key, or added a new header key), then all linebreaks are forced to <CR><LF> even if the original headers used just <LF>
86
- - **node.setContentType(contentType)** sets or updates mime type for the node
87
- - **node.setCharset(charset)** sets or updates character set in the Content-Type header
88
- - **node.setFilename(filename)** sets or updates filename in the Content-Disposition header (unicode allowed)
85
+ - **node.getHeaders()** returns a Buffer value with generated headers. If you have not modified the headers object in any way then you should get the exact copy of the original. In case you have done something (for example removed a key, or added a new header key), then all linebreaks are forced to <CR><LF> even if the original headers used just <LF>
86
+ - **node.setContentType(contentType)** sets or updates mime type for the node
87
+ - **node.setCharset(charset)** sets or updates character set in the Content-Type header
88
+ - **node.setFilename(filename)** sets or updates filename in the Content-Disposition header (unicode allowed)
89
89
 
90
90
  You can manipulate specific header keys as well using the `headers` object
91
91
 
92
- - **node.headers.get(key)** returns an array of strings with all header rows for the selected key (these are full header lines, so key name is part of the row string, eg `["Subject: This is subject line"]`)
93
- - **node.headers.getFirst(key)** returns string value of the specified header key (eg `"This is subject line"`)
94
- - **node.headers.hasHeader(key)** returns boolean value if the specified header key exists
95
- - **node.headers.add(key, value [,index])** adds a new header value to the specified index or to the top of the header block if index is not specified
96
- - **node.headers.update(key, value, [,relativeKeyIndex])** replaces a header value to the specified relative key index (note that relative key index means relative to the same header key, eg if multiple exist and you specify `1` as the value, then it will update the second) or if no relative key index is specified, then it will remove all header value matches found for the key and append one at the last matching key index found with the specified value. If a relative key index is specified and it does not exist then it will be replaced (eg if there are two headers of `X-Foo-Bar` and you pass `2`, meaning it will update the third, no updates will be made since the third did not exist)
97
- - **node.headers.remove(key)** remove header value
98
- - **node.headers.mbox** If this is a MBOX formatted message then this value holds the prefix line (eg. "From MAILER-DAEMON Fri Jul 8 12:08:34 2011")
99
- - **node.headers.mbox** If this is a POST form-data then this value holds the HTTP prefix line (eg. "POST /upload.php HTTP/1.1")
92
+ - **node.headers.get(key)** returns an array of strings with all header rows for the selected key (these are full header lines, so key name is part of the row string, eg `["Subject: This is subject line"]`)
93
+ - **node.headers.getFirst(key)** returns string value of the specified header key (eg `"This is subject line"`)
94
+ - **node.headers.hasHeader(key)** returns boolean value if the specified header key exists
95
+ - **node.headers.add(key, value [,index])** adds a new header value to the specified index or to the top of the header block if index is not specified
96
+ - **node.headers.update(key, value, [,relativeKeyIndex])** replaces a header value to the specified relative key index (note that relative key index means relative to the same header key, eg if multiple exist and you specify `1` as the value, then it will update the second) or if no relative key index is specified, then it will remove all header value matches found for the key and append one at the last matching key index found with the specified value. If a relative key index is specified and it does not exist then it will be replaced (eg if there are two headers of `X-Foo-Bar` and you pass `2`, meaning it will update the third, no updates will be made since the third did not exist)
97
+ - **node.headers.remove(key)** remove header value
98
+ - **node.headers.mbox** If this is a MBOX formatted message then this value holds the prefix line (eg. "From MAILER-DAEMON Fri Jul 8 12:08:34 2011")
99
+ - **node.headers.mbox** If this is a POST form-data then this value holds the HTTP prefix line (eg. "POST /upload.php HTTP/1.1")
100
100
 
101
101
  Additionally you can check the details of the node with the following properties automatically parsed from the headers:
102
102
 
103
- - **node.root** if true then it means this is the message root, so this node should contain Subject, From, To etc. headers
104
- - **node.contentType** returns the mime type of the node (eg. 'text/html')
105
- - **node.disposition** either `'attachment'`, `'inline'` or `false` if not set
106
- - **node.charset** returns the charset of the node as defined in 'Content-Type' header (eg. 'UTF-8') or false if not defined
107
- - **node.encoding** returns the Transfer-Encoding value (eg. 'base64' or 'quoted-printable') or false if not defined
108
- - **node.multipart** if has value, then this is a multipart node (does not have 'body' parts)
109
- - **node.filename** is set if the headers contain a filename value. This is decoded to unicode, so it is a normal string or false if not found
103
+ - **node.root** if true then it means this is the message root, so this node should contain Subject, From, To etc. headers
104
+ - **node.contentType** returns the mime type of the node (eg. 'text/html')
105
+ - **node.disposition** either `'attachment'`, `'inline'` or `false` if not set
106
+ - **node.charset** returns the charset of the node as defined in 'Content-Type' header (eg. 'UTF-8') or false if not defined
107
+ - **node.encoding** returns the Transfer-Encoding value (eg. 'base64' or 'quoted-printable') or false if not defined
108
+ - **node.multipart** if has value, then this is a multipart node (does not have 'body' parts)
109
+ - **node.filename** is set if the headers contain a filename value. This is decoded to unicode, so it is a normal string or false if not found
110
110
 
111
111
  ### Join parsed message stream
112
112
 
@@ -118,10 +118,7 @@ let Joiner = require('mailsplit').Joiner;
118
118
  let splitter = new Splitter();
119
119
  let joiner = new Joiner();
120
120
  // pipe a message source to splitter, then joiner and finally to stdout
121
- someMessagStream
122
- .pipe(splitter)
123
- .pipe(joiner)
124
- .pipe(process.stdout);
121
+ someMessagStream.pipe(splitter).pipe(joiner).pipe(process.stdout);
125
122
  ```
126
123
 
127
124
  ### Rewrite specific nodes
@@ -130,14 +127,14 @@ someMessagStream
130
127
 
131
128
  Rewriter takes the following argument:
132
129
 
133
- - **filterFunc** gets the current node as argument and starts processing it if `filterFunc` returns true
130
+ - **filterFunc** gets the current node as argument and starts processing it if `filterFunc` returns true
134
131
 
135
132
  Once Rewriter finds a matching node, it emits the following event:
136
133
 
137
- - _'node'_ with an object argument `data`
138
- - `data.node` includes the current node with headers
139
- - `data.decoder` is the decoder stream that you can read data from
140
- - `data.encoder` is the encoder stream that you can write data to. Whatever you write into that stream will be encoded properly and inserted as the content of the current node
134
+ - _'node'_ with an object argument `data`
135
+ - `data.node` includes the current node with headers
136
+ - `data.decoder` is the decoder stream that you can read data from
137
+ - `data.encoder` is the encoder stream that you can write data to. Whatever you write into that stream will be encoded properly and inserted as the content of the current node
141
138
 
142
139
  ```javascript
143
140
  let Splitter = require('mailsplit').Splitter;
@@ -153,11 +150,7 @@ rewriter.on('node', data => {
153
150
  data.decoder.pipe(data.encoder);
154
151
  });
155
152
  // pipe a message source to splitter, then rewriter, then joiner and finally to stdout
156
- someMessagStream
157
- .pipe(splitter)
158
- .pipe(rewriter)
159
- .pipe(joiner)
160
- .pipe(process.stdout);
153
+ someMessagStream.pipe(splitter).pipe(rewriter).pipe(joiner).pipe(process.stdout);
161
154
  ```
162
155
 
163
156
  ### Stream specific nodes
@@ -166,14 +159,14 @@ someMessagStream
166
159
 
167
160
  Streamer takes the following argument:
168
161
 
169
- - **filterFunc** gets the current node as argument and starts processing it if `filterFunc` returns true
162
+ - **filterFunc** gets the current node as argument and starts processing it if `filterFunc` returns true
170
163
 
171
164
  Once Streamer finds a matching node, it emits the following event:
172
165
 
173
- - _'node'_ with an object argument `data`
174
- - `data.node` includes the current node with headers (informational only, you can't modify it)
175
- - `data.decoder` is the decoder stream that you can read data from
176
- - `data.done` is a function you must call once you have processed the stream
166
+ - _'node'_ with an object argument `data`
167
+ - `data.node` includes the current node with headers (informational only, you can't modify it)
168
+ - `data.decoder` is the decoder stream that you can read data from
169
+ - `data.done` is a function you must call once you have processed the stream
177
170
 
178
171
  ```javascript
179
172
  let Splitter = require('mailsplit').Splitter;
@@ -189,11 +182,7 @@ streamer.on('node', data => {
189
182
  data.done();
190
183
  });
191
184
  // pipe a message source to splitter, then streamer, then joiner and finally to stdout
192
- someMessagStream
193
- .pipe(splitter)
194
- .pipe(streamer)
195
- .pipe(joiner)
196
- .pipe(process.stdout);
185
+ someMessagStream.pipe(splitter).pipe(streamer).pipe(joiner).pipe(process.stdout);
197
186
  ```
198
187
 
199
188
  ### Benchmark
@@ -211,4 +200,4 @@ Done. 20000 messages [1244 MB] processed in 55.882 s. with average of 358 messag
211
200
 
212
201
  Dual licensed under **MIT** or **EUPLv1.1+**
213
202
 
214
- > `mailsplit` is part of the Zone Mail Suite (ZMS). Suite of programs and modules for an efficient, fast and modern email server.
203
+ > `mailsplit` is part of the Zone Mail Suite (ZMS). Suite of programs and modules for an efficient, fast and modern email server.
@@ -20,9 +20,17 @@ const MAX_PENDING_LINE_SIZE = 64 * 1024;
20
20
  // how many separate writes the pending line may be kept in before it is compacted
21
21
  const MAX_PENDING_LINE_CHUNKS = 1024;
22
22
 
23
+ // The most SP and HTAB a delimiter line may carry between the boundary value, or its closing
24
+ // "--", and the line ending. RFC 2046 5.1.1 allows the whitespace, and Python and postal-mime
25
+ // accept any amount of it, so a line that ends in a few spaces delimits the same parts here
26
+ // as there. The line still has to fit a header line (RFC 5322 2.1.1), so the pending line
27
+ // kept as a possible delimiter stays bounded
28
+ const BOUNDARY_LINE_TRAILING_WSP = 998;
29
+
23
30
  // what a delimiter line may carry after the boundary value: the "--" prefix, an optional
24
- // "--" suffix and the line terminator. This is the bound compareBoundary() accepts.
25
- const BOUNDARY_LINE_SUFFIX = 2 /* "--" prefix */ + 2 /* "--" suffix */ + 2; /* trailing <CR><LF> */
31
+ // "--" suffix, the trailing whitespace and the line terminator. This is the bound
32
+ // compareBoundary() accepts.
33
+ const BOUNDARY_LINE_SUFFIX = 2 /* "--" prefix */ + 2 /* "--" suffix */ + BOUNDARY_LINE_TRAILING_WSP + 2; /* trailing <CR><LF> */
26
34
 
27
35
  // checkBoundary() additionally allows a line ending in front of the delimiter, so this is
28
36
  // the longest a delimiter line can ever be once the boundary value is subtracted
@@ -389,7 +397,8 @@ class MessageSplitter extends Transform {
389
397
  * @returns {1 | 2 | false}
390
398
  */
391
399
  compareBoundary(line, startpos, boundary) {
392
- // --{boundary}\r\n or --{boundary}--\r\n
400
+ // --{boundary}\r\n or --{boundary}--\r\n, with SP and HTAB allowed in front of the
401
+ // line ending (RFC 2046 5.1.1), where Python and postal-mime accept them as well
393
402
  if (line.length < boundary.length + 3 + startpos || line.length > boundary.length + BOUNDARY_LINE_SUFFIX + startpos) {
394
403
  return false;
395
404
  }
@@ -399,34 +408,37 @@ class MessageSplitter extends Transform {
399
408
  }
400
409
  }
401
410
 
402
- let pos = 0;
403
- for (let i = boundary.length + 2 + startpos; i < line.length; i++) {
404
- let c = line[i];
405
- if (pos === 0 && (c === 0x0d || c === 0x0a)) {
406
- // 1: next node
407
- return 1;
408
- }
409
- if (pos === 0 && c !== 0x2d) {
410
- // expecting "-"
411
- return false;
412
- }
413
- if (pos === 1 && c !== 0x2d) {
414
- // expecting "-"
415
- return false;
416
- }
417
- if (pos === 2 && c !== 0x0d && c !== 0x0a) {
418
- // expecting line terminator, either <CR> or <LF>
411
+ let i = boundary.length + 2 + startpos;
412
+ // 1: next node, 2: multipart end
413
+ let result = /** @type {1 | 2} */ (1);
414
+ if (line[i] === 0x2d) {
415
+ if (line[i + 1] !== 0x2d) {
416
+ // expecting "--"
419
417
  return false;
420
418
  }
421
- if (pos === 3 && c !== 0x0a) {
422
- // expecting line terminator <LF>
423
- return false;
424
- }
425
- pos++;
419
+ result = 2;
420
+ i += 2;
421
+ }
422
+
423
+ while (i < line.length && (line[i] === 0x20 || line[i] === 0x09)) {
424
+ i++;
426
425
  }
427
426
 
428
- // 2: multipart end
429
- return 2;
427
+ if (i === line.length) {
428
+ // only the closing delimiter may end the input without a line ending
429
+ return result === 2 ? 2 : false;
430
+ }
431
+
432
+ // expecting the line terminator and nothing after it: <CR><LF>, <CR> or <LF>, where
433
+ // the <CR> may be a run of them, the artifact of a second CR to CRLF conversion,
434
+ // which postal-mime reads as the line ending as well
435
+ while (i < line.length && line[i] === 0x0d) {
436
+ i++;
437
+ }
438
+ if (i < line.length && line[i] === 0x0a) {
439
+ i++;
440
+ }
441
+ return i === line.length ? result : false;
430
442
  }
431
443
 
432
444
  /**
@@ -46,7 +46,7 @@ declare class MimeNode implements MimeNodeShape {
46
46
  /** Decoded filename from Content-Disposition or Content-Type parameters, or `false` when absent. */
47
47
  filename: string | false;
48
48
 
49
- /** Whether this node is `text/*` with `format=flowed`. */
49
+ /** Whether this node is `text/plain` with `format=flowed`. */
50
50
  flowed: boolean;
51
51
 
52
52
  /** Whether flowed text uses `delsp=yes`. */
package/lib/mime-node.js CHANGED
@@ -27,6 +27,9 @@ const HEADER_LINE_MIN_COST = 64;
27
27
  * `value.replace(/\(.*\)/g, '')`, but in linear time. The regex backtracks quadratically
28
28
  * on a value with many `(` characters and no closing `)`.
29
29
  *
30
+ * Used for the Content-Transfer-Encoding only. The MIME headers with parameters go through
31
+ * libmime's parseHeaderValue with stripComments, which knows where a comment may stand.
32
+ *
30
33
  * @param {string} value
31
34
  * @returns {string}
32
35
  */
@@ -73,7 +76,11 @@ function rfc2231ParamNames(value) {
73
76
  if (eq < 0) {
74
77
  return;
75
78
  }
76
- let match = segment.substring(0, eq).trim().toLowerCase().match(/^(.+?)\*(\d+\*?)?$/);
79
+ let match = segment
80
+ .substring(0, eq)
81
+ .trim()
82
+ .toLowerCase()
83
+ .match(/^(.+?)\*(\d+\*?)?$/);
77
84
  if (match) {
78
85
  names.add(match[1]);
79
86
  }
@@ -212,8 +219,14 @@ class MimeNode {
212
219
  this._headersLines = [];
213
220
  let headers = this.headers;
214
221
 
222
+ // The comments of the MIME headers are dropped, so that `boundary=b (comment)` names
223
+ // the boundary `b` that postal-mime and Python's email.policy.default split by. Read as
224
+ // content, it matched no delimiter line and the parts both of them find stayed hidden
225
+ // here. Python's legacy compat32 policy, the default of message_from_bytes() without a
226
+ // policy, keeps the comment and splits by `--b (comment)` lines instead: a split
227
+ // against it is the price of agreeing with the two readers a renderer is built on
215
228
  let dispositionHeader = headers.getFirst('Content-Disposition');
216
- this._parsedContentDisposition = this.libmime.parseHeaderValue(dispositionHeader);
229
+ this._parsedContentDisposition = this.libmime.parseHeaderValue(dispositionHeader, { stripComments: true });
217
230
  let parsedContentDisposition = /** @type {StructuredHeader} */ (this._parsedContentDisposition);
218
231
 
219
232
  // if content-type is missing default to plaintext
@@ -236,9 +249,12 @@ class MimeNode {
236
249
  }
237
250
  }
238
251
 
239
- this._parsedContentType = this.libmime.parseHeaderValue(contentHeader);
252
+ this._parsedContentType = this.libmime.parseHeaderValue(contentHeader, { stripComments: true });
240
253
  let parsedContentType = /** @type {StructuredHeader} */ (this._parsedContentType);
241
254
 
255
+ // The encoding is a bare token, and one written in quotes stays quoted, so that
256
+ // "base64" is not decoded, as Python and postal-mime do not decode it. parseHeaderValue
257
+ // would unquote it, so the transfer encoding keeps its own comment stripper
242
258
  this.encoding = stripComments(headers.getFirst('Content-Transfer-Encoding')).toLowerCase().trim();
243
259
  this.contentType = (parsedContentType.value || '').toLowerCase().trim() || false;
244
260
  this.charset = parsedContentType.params.charset || false;
@@ -248,14 +264,18 @@ class MimeNode {
248
264
  if (this.disposition) {
249
265
  try {
250
266
  this.disposition = this.libmime.decodeWords(this.disposition);
251
- } catch (E) {
267
+ } catch (_E) {
252
268
  // failed to parse disposition, keep as is (most probably an unknown charset is used)
253
269
  }
254
270
  }
255
271
 
256
272
  this.filename = parsedContentDisposition.params.filename || parsedContentType.params.name || false;
257
273
 
258
- if (parsedContentType.params.format && parsedContentType.params.format.toLowerCase().trim() === 'flowed') {
274
+ // format=flowed is defined for text/plain (RFC 3676), and a browser never unfolds an
275
+ // html part. Flagged on any text part, a text/html part with delsp=yes had its
276
+ // `<scri \r\npt>` joined into a script tag by the consumer that unfolded it, a tag
277
+ // neither Python nor a browser given the raw bytes sees
278
+ if (this.contentType === 'text/plain' && parsedContentType.params.format && parsedContentType.params.format.toLowerCase().trim() === 'flowed') {
259
279
  this.flowed = true;
260
280
  if (parsedContentType.params.delsp && parsedContentType.params.delsp.toLowerCase().trim() === 'yes') {
261
281
  this.delSp = true;
@@ -270,7 +290,7 @@ class MimeNode {
270
290
  if (!decoded) {
271
291
  try {
272
292
  this.filename = this.libmime.decodeWords(this.filename);
273
- } catch (E) {
293
+ } catch (_E) {
274
294
  // failed to parse filename, keep as is (most probably an unknown charset is used)
275
295
  }
276
296
  }
@@ -281,7 +301,16 @@ class MimeNode {
281
301
  this.contentType.substr(0, this.contentType.indexOf('/')) === 'multipart' &&
282
302
  this.contentType.substr(this.contentType.indexOf('/') + 1)) ||
283
303
  false;
284
- this._boundary = (parsedContentType.params.boundary && Buffer.from(parsedContentType.params.boundary)) || false;
304
+ // A boundary is ASCII and does not end in whitespace (RFC 2046 5.1.1, bchars), and
305
+ // Python matches no line against one that is not. One holding a non-ASCII character
306
+ // came out of a header read as latin1 or as UTF-8, and matched under `--ab\xC3\xA9cd`
307
+ // lines parts that a scanner built on Python never saw. Such a boundary counts as
308
+ // none, as a boundary parameter that is missing does. Trailing whitespace is dropped
309
+ // as Python drops it, a delimiter line may end in whitespace anyway
310
+ let boundary = (parsedContentType.params.boundary || '').trimEnd();
311
+ // a non-ASCII character takes more than one byte in UTF-8
312
+ let ascii = Buffer.byteLength(boundary) === boundary.length;
313
+ this._boundary = boundary && ascii ? Buffer.from(boundary) : false;
285
314
 
286
315
  this.rfc822 = this.contentType === 'message/rfc822';
287
316
 
package/lib/types.d.ts CHANGED
@@ -107,7 +107,7 @@ export interface MimeNode {
107
107
  /** Decoded filename from Content-Disposition or Content-Type parameters, or `false` when absent. */
108
108
  filename: string | false;
109
109
 
110
- /** Whether this node is `text/*` with `format=flowed`. */
110
+ /** Whether this node is `text/plain` with `format=flowed`. */
111
111
  flowed: boolean;
112
112
 
113
113
  /** Whether flowed text uses `delsp=yes`. */
package/package.json CHANGED
@@ -1,38 +1,43 @@
1
1
  {
2
2
  "name": "@zone-eu/mailsplit",
3
- "version": "5.4.26",
3
+ "version": "5.4.28",
4
4
  "description": "Split email messages into an object stream",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
7
+ "engines": {
8
+ "node": ">=20.0.0"
9
+ },
7
10
  "directories": {
8
11
  "test": "test"
9
12
  },
10
13
  "scripts": {
11
- "test": "grunt",
12
- "update": "rm -rf node_modules package-lock.json && ncu -u && npm install",
13
- "typecheck": "tsc -p tsconfig.json"
14
+ "test": "node --test test/*-test.js",
15
+ "test:cjs": "NODE_OPTIONS=--no-experimental-require-module npm test",
16
+ "test:coverage": "c8 npm test",
17
+ "lint": "npm run typecheck && eslint .",
18
+ "lint:fix": "eslint . --fix",
19
+ "format": "prettier --write .",
20
+ "format:check": "prettier --check .",
21
+ "typecheck": "tsc -p tsconfig.json",
22
+ "update": "rm -rf node_modules package-lock.json && ncu -u && npm install"
14
23
  },
15
24
  "author": "Andris Reinman",
16
25
  "license": "(MIT OR EUPL-1.1+)",
17
26
  "dependencies": {
18
27
  "libbase64": "1.4.1",
19
- "libmime": "5.5.1",
28
+ "libmime": "5.6.3",
20
29
  "libqp": "3.0.1"
21
30
  },
22
31
  "devDependencies": {
23
- "@types/eslint": "9.6.1",
24
- "@types/eslint-config-prettier": "6.11.3",
25
- "@types/grunt": "0.4.32",
32
+ "@eslint/js": "10.0.1",
26
33
  "@types/libmime": "5.3.0",
27
34
  "@types/libqp": "1.1.3",
28
35
  "@types/node": "26.6.5",
29
- "eslint": "8.29.0",
30
- "eslint-config-nodemailer": "1.2.0",
31
- "eslint-config-prettier": "9.1.0",
32
- "grunt": "1.6.3",
33
- "grunt-cli": "1.5.0",
34
- "grunt-contrib-nodeunit": "5.0.0",
35
- "grunt-eslint": "24.0.1",
36
+ "c8": "12.0.0",
37
+ "eslint": "10.13.0",
38
+ "eslint-config-prettier": "10.1.8",
39
+ "globals": "17.13.0",
40
+ "prettier": "3.9.10",
36
41
  "random-message": "1.1.0",
37
42
  "typescript": "7.0.2"
38
43
  },