@orkestrel/msg 0.0.1
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/LICENSE +21 -0
- package/README.md +91 -0
- package/dist/src/core/index.cjs +2548 -0
- package/dist/src/core/index.cjs.map +1 -0
- package/dist/src/core/index.d.cts +1350 -0
- package/dist/src/core/index.d.ts +1350 -0
- package/dist/src/core/index.js +2445 -0
- package/dist/src/core/index.js.map +1 -0
- package/package.json +83 -0
|
@@ -0,0 +1,2445 @@
|
|
|
1
|
+
//#region src/core/constants.ts
|
|
2
|
+
/**
|
|
3
|
+
* CFB magic header bytes (0xD0CF11E0A1B11AE1).
|
|
4
|
+
*/
|
|
5
|
+
var MSG_FILE_HEADER = new Uint8Array([
|
|
6
|
+
208,
|
|
7
|
+
207,
|
|
8
|
+
17,
|
|
9
|
+
224,
|
|
10
|
+
161,
|
|
11
|
+
177,
|
|
12
|
+
26,
|
|
13
|
+
225
|
|
14
|
+
]);
|
|
15
|
+
/**
|
|
16
|
+
* Sentinel for unused blocks in the FAT.
|
|
17
|
+
*/
|
|
18
|
+
var MSG_UNUSED_BLOCK = -1;
|
|
19
|
+
/**
|
|
20
|
+
* Sentinel for end-of-chain in the FAT.
|
|
21
|
+
*/
|
|
22
|
+
var MSG_END_OF_CHAIN = -2;
|
|
23
|
+
/**
|
|
24
|
+
* Small sector size (512 bytes).
|
|
25
|
+
*/
|
|
26
|
+
var MSG_S_BIG_BLOCK_SIZE = 512;
|
|
27
|
+
/**
|
|
28
|
+
* Small sector size mark in the header (byte at offset 30).
|
|
29
|
+
*/
|
|
30
|
+
var MSG_S_BIG_BLOCK_MARK = 9;
|
|
31
|
+
/**
|
|
32
|
+
* Large sector size (4096 bytes).
|
|
33
|
+
*/
|
|
34
|
+
var MSG_L_BIG_BLOCK_SIZE = 4096;
|
|
35
|
+
/**
|
|
36
|
+
* Large sector size mark in the header (byte at offset 30).
|
|
37
|
+
*/
|
|
38
|
+
var MSG_L_BIG_BLOCK_MARK = 12;
|
|
39
|
+
/**
|
|
40
|
+
* Mini-stream sector size (64 bytes).
|
|
41
|
+
*/
|
|
42
|
+
var MSG_SMALL_BLOCK_SIZE = 64;
|
|
43
|
+
/**
|
|
44
|
+
* Threshold below which data is stored in the mini-stream.
|
|
45
|
+
*/
|
|
46
|
+
var MSG_BIG_BLOCK_MIN_DOC_SIZE = 4096;
|
|
47
|
+
/**
|
|
48
|
+
* Header offset: property (directory) start sector.
|
|
49
|
+
*/
|
|
50
|
+
var MSG_HEADER_PROPERTY_START_OFFSET = 48;
|
|
51
|
+
/**
|
|
52
|
+
* Header offset: BAT sector array start.
|
|
53
|
+
*/
|
|
54
|
+
var MSG_HEADER_BAT_START_OFFSET = 76;
|
|
55
|
+
/**
|
|
56
|
+
* Header offset: BAT sector count.
|
|
57
|
+
*/
|
|
58
|
+
var MSG_HEADER_BAT_COUNT_OFFSET = 44;
|
|
59
|
+
/**
|
|
60
|
+
* Header offset: SBAT start sector.
|
|
61
|
+
*/
|
|
62
|
+
var MSG_HEADER_SBAT_START_OFFSET = 60;
|
|
63
|
+
/**
|
|
64
|
+
* Header offset: SBAT sector count.
|
|
65
|
+
*/
|
|
66
|
+
var MSG_HEADER_SBAT_COUNT_OFFSET = 64;
|
|
67
|
+
/**
|
|
68
|
+
* Header offset: XBAT (DIFAT) start sector.
|
|
69
|
+
*/
|
|
70
|
+
var MSG_HEADER_XBAT_START_OFFSET = 68;
|
|
71
|
+
/**
|
|
72
|
+
* Header offset: XBAT (DIFAT) sector count.
|
|
73
|
+
*/
|
|
74
|
+
var MSG_HEADER_XBAT_COUNT_OFFSET = 72;
|
|
75
|
+
/**
|
|
76
|
+
* No child/sibling index sentinel.
|
|
77
|
+
*/
|
|
78
|
+
var MSG_PROP_NO_INDEX = -1;
|
|
79
|
+
/**
|
|
80
|
+
* Maximum recursion depth accepted by the directory hierarchy builder
|
|
81
|
+
* (`MSGReader#buildHierarchy`). Defense-in-depth against a pathological
|
|
82
|
+
* or hostile directory tree — the sibling-chain and visited-set guards
|
|
83
|
+
* already bound each level, this caps the recursion depth itself.
|
|
84
|
+
*/
|
|
85
|
+
var MSG_MAX_HIERARCHY_DEPTH = 64;
|
|
86
|
+
/**
|
|
87
|
+
* Directory entry size in bytes.
|
|
88
|
+
*/
|
|
89
|
+
var MSG_PROPERTY_SIZE = 128;
|
|
90
|
+
/**
|
|
91
|
+
* Offset within a directory entry: name byte length.
|
|
92
|
+
*/
|
|
93
|
+
var MSG_PROP_NAME_SIZE_OFFSET = 64;
|
|
94
|
+
/**
|
|
95
|
+
* Offset within a directory entry: object type byte.
|
|
96
|
+
*/
|
|
97
|
+
var MSG_PROP_TYPE_OFFSET = 66;
|
|
98
|
+
/**
|
|
99
|
+
* Offset within a directory entry: left sibling index.
|
|
100
|
+
*/
|
|
101
|
+
var MSG_PROP_PREVIOUS_PROPERTY_OFFSET = 68;
|
|
102
|
+
/**
|
|
103
|
+
* Offset within a directory entry: right sibling index.
|
|
104
|
+
*/
|
|
105
|
+
var MSG_PROP_NEXT_PROPERTY_OFFSET = 72;
|
|
106
|
+
/**
|
|
107
|
+
* Offset within a directory entry: child index.
|
|
108
|
+
*/
|
|
109
|
+
var MSG_PROP_CHILD_PROPERTY_OFFSET = 76;
|
|
110
|
+
/**
|
|
111
|
+
* Offset within a directory entry: start sector of stream data.
|
|
112
|
+
*/
|
|
113
|
+
var MSG_PROP_START_BLOCK_OFFSET = 116;
|
|
114
|
+
/**
|
|
115
|
+
* Offset within a directory entry: stream byte length.
|
|
116
|
+
*/
|
|
117
|
+
var MSG_PROP_SIZE_OFFSET = 120;
|
|
118
|
+
/**
|
|
119
|
+
* Directory entry type: unallocated.
|
|
120
|
+
*/
|
|
121
|
+
var MSG_TYPE_UNALLOCATED = 0;
|
|
122
|
+
/**
|
|
123
|
+
* Directory entry type: storage (folder).
|
|
124
|
+
*/
|
|
125
|
+
var MSG_TYPE_DIRECTORY = 1;
|
|
126
|
+
/**
|
|
127
|
+
* Directory entry type: stream (document).
|
|
128
|
+
*/
|
|
129
|
+
var MSG_TYPE_DOCUMENT = 2;
|
|
130
|
+
/**
|
|
131
|
+
* Directory entry type: root storage.
|
|
132
|
+
*/
|
|
133
|
+
var MSG_TYPE_ROOT = 5;
|
|
134
|
+
/**
|
|
135
|
+
* Name prefix for attachment storage entries.
|
|
136
|
+
*/
|
|
137
|
+
var MSG_PREFIX_ATTACHMENT = "__attach_version1.0";
|
|
138
|
+
/**
|
|
139
|
+
* Name prefix for recipient storage entries.
|
|
140
|
+
*/
|
|
141
|
+
var MSG_PREFIX_RECIPIENT = "__recip_version1.0";
|
|
142
|
+
/**
|
|
143
|
+
* Name prefix for document (substg) stream entries.
|
|
144
|
+
*/
|
|
145
|
+
var MSG_PREFIX_DOCUMENT = "__substg1.";
|
|
146
|
+
/**
|
|
147
|
+
* Name prefix for named property mapping storage.
|
|
148
|
+
*/
|
|
149
|
+
var MSG_PREFIX_NAMEID = "__nameid_version1.0";
|
|
150
|
+
/**
|
|
151
|
+
* MAPI property tag to field name mapping.
|
|
152
|
+
*/
|
|
153
|
+
var MSG_FIELD_NAME_MAPPING = {
|
|
154
|
+
"001a": "messageClass",
|
|
155
|
+
"0037": "subject",
|
|
156
|
+
"0039": "clientSubmitTime",
|
|
157
|
+
"0070": "conversationTopic",
|
|
158
|
+
"007d": "headers",
|
|
159
|
+
"0c15": "recipientRole",
|
|
160
|
+
"0c1a": "senderName",
|
|
161
|
+
"0c1e": "senderAddressType",
|
|
162
|
+
"0c1f": "senderEmail",
|
|
163
|
+
"0e06": "messageDeliveryTime",
|
|
164
|
+
"0e07": "messageFlags",
|
|
165
|
+
"0e1d": "normalizedSubject",
|
|
166
|
+
"1000": "body",
|
|
167
|
+
"1009": "compressedRTF",
|
|
168
|
+
"1035": "messageId",
|
|
169
|
+
"3001": "name",
|
|
170
|
+
"3002": "addressType",
|
|
171
|
+
"3003": "email",
|
|
172
|
+
"3007": "creationTime",
|
|
173
|
+
"3008": "lastModificationTime",
|
|
174
|
+
"3703": "extension",
|
|
175
|
+
"3704": "fileNameShort",
|
|
176
|
+
"3707": "fileName",
|
|
177
|
+
"3712": "contentId",
|
|
178
|
+
"370e": "mimeType",
|
|
179
|
+
"39fe": "smtpAddress",
|
|
180
|
+
"3fd9": "preview",
|
|
181
|
+
"3fde": "internetCodepage",
|
|
182
|
+
"3ff1": "messageLocaleId",
|
|
183
|
+
"3ffa": "lastModifierName",
|
|
184
|
+
"3ffd": "messageCodepage",
|
|
185
|
+
"5d01": "senderSMTPAddress",
|
|
186
|
+
"5d02": "sentRepresentingSMTPAddress",
|
|
187
|
+
"5d0a": "creatorSMTPAddress",
|
|
188
|
+
"5d0b": "lastModifierSMTPAddress",
|
|
189
|
+
"7ffe": "attachmentHidden",
|
|
190
|
+
"3a05": "generation",
|
|
191
|
+
"3a06": "givenName",
|
|
192
|
+
"3a08": "businessPhone",
|
|
193
|
+
"3a09": "homePhone",
|
|
194
|
+
"3a0d": "location",
|
|
195
|
+
"3a11": "surname",
|
|
196
|
+
"3a15": "postalAddress",
|
|
197
|
+
"3a16": "companyName",
|
|
198
|
+
"3a17": "jobTitle",
|
|
199
|
+
"3a18": "departmentName",
|
|
200
|
+
"3a1c": "mobilePhone",
|
|
201
|
+
"3a24": "businessFax",
|
|
202
|
+
"3a26": "country",
|
|
203
|
+
"3a27": "homeAddressCity",
|
|
204
|
+
"3a28": "stateOrProvince",
|
|
205
|
+
"3a29": "streetAddress",
|
|
206
|
+
"3a2a": "postalCode",
|
|
207
|
+
"3a44": "middleName",
|
|
208
|
+
"3a45": "namePrefix",
|
|
209
|
+
"3a51": "businessHomePage"
|
|
210
|
+
};
|
|
211
|
+
/**
|
|
212
|
+
* Full 8-char property tag to field name mapping (for compound tags).
|
|
213
|
+
*/
|
|
214
|
+
var MSG_FIELD_FULL_NAME_MAPPING = {
|
|
215
|
+
"1013001f": "bodyHTML",
|
|
216
|
+
"10130102": "html"
|
|
217
|
+
};
|
|
218
|
+
/**
|
|
219
|
+
* MAPI property type tag to decode type mapping.
|
|
220
|
+
*/
|
|
221
|
+
var MSG_FIELD_TYPE_MAPPING = {
|
|
222
|
+
"001e": "string",
|
|
223
|
+
"001f": "unicode",
|
|
224
|
+
"0040": "time",
|
|
225
|
+
"0102": "binary",
|
|
226
|
+
"0003": "integer",
|
|
227
|
+
"000b": "boolean"
|
|
228
|
+
};
|
|
229
|
+
/**
|
|
230
|
+
* Attachment data class identifier.
|
|
231
|
+
*/
|
|
232
|
+
var MSG_FIELD_CLASS_ATTACHMENT_DATA = "3701";
|
|
233
|
+
/**
|
|
234
|
+
* Directory field type indicating an embedded MSG.
|
|
235
|
+
*/
|
|
236
|
+
var MSG_FIELD_DIR_TYPE_INNER_MSG = "000d";
|
|
237
|
+
/**
|
|
238
|
+
* MAPI recipient type: TO.
|
|
239
|
+
*/
|
|
240
|
+
var MSG_MAPI_RECIPIENT_TO = 1;
|
|
241
|
+
/**
|
|
242
|
+
* MAPI recipient type: CC.
|
|
243
|
+
*/
|
|
244
|
+
var MSG_MAPI_RECIPIENT_CC = 2;
|
|
245
|
+
/**
|
|
246
|
+
* MAPI recipient type: BCC.
|
|
247
|
+
*/
|
|
248
|
+
var MSG_MAPI_RECIPIENT_BCC = 3;
|
|
249
|
+
/**
|
|
250
|
+
* PidLid property set GUID to LID-to-field-name mapping.
|
|
251
|
+
* Maps well-known MAPI named property sets to their property
|
|
252
|
+
* long IDs and corresponding field names on MSGFieldData.
|
|
253
|
+
*/
|
|
254
|
+
var MSG_PIDLID_MAPPING = {
|
|
255
|
+
"00062008-0000-0000-c000-000000000046": {
|
|
256
|
+
34084: "votingResponse",
|
|
257
|
+
34176: "internetAccountName"
|
|
258
|
+
},
|
|
259
|
+
"00062002-0000-0000-c000-000000000046": {
|
|
260
|
+
33293: "appointmentStart",
|
|
261
|
+
33294: "appointmentEnd",
|
|
262
|
+
33288: "appointmentLocation",
|
|
263
|
+
33332: "timeZoneDescription",
|
|
264
|
+
33333: "clipStart",
|
|
265
|
+
33334: "clipEnd"
|
|
266
|
+
},
|
|
267
|
+
"00062004-0000-0000-c000-000000000046": {
|
|
268
|
+
32773: "fileUnder",
|
|
269
|
+
32784: "departmentName",
|
|
270
|
+
32795: "workAddress",
|
|
271
|
+
32811: "contactWebPage",
|
|
272
|
+
32812: "yomiFirstName",
|
|
273
|
+
32813: "yomiLastName",
|
|
274
|
+
32814: "yomiCompanyName",
|
|
275
|
+
32837: "workAddressStreet",
|
|
276
|
+
32838: "workAddressCity",
|
|
277
|
+
32839: "workAddressState",
|
|
278
|
+
32840: "workAddressPostalCode",
|
|
279
|
+
32841: "workAddressCountry",
|
|
280
|
+
32866: "instantMessagingAddress",
|
|
281
|
+
32896: "primaryEmailDisplayName",
|
|
282
|
+
32899: "primaryEmailAddress",
|
|
283
|
+
32900: "primaryEmailOriginalDisplayName",
|
|
284
|
+
32946: "fax1AddressType",
|
|
285
|
+
32947: "fax1EmailAddress",
|
|
286
|
+
32948: "fax1OriginalDisplayName",
|
|
287
|
+
32962: "fax2AddressType",
|
|
288
|
+
32963: "fax2EmailAddress",
|
|
289
|
+
32964: "fax2OriginalDisplayName",
|
|
290
|
+
32978: "fax3AddressType",
|
|
291
|
+
32979: "fax3EmailAddress",
|
|
292
|
+
32980: "fax3OriginalDisplayName",
|
|
293
|
+
32987: "workAddressCountryCode",
|
|
294
|
+
32989: "addressCountryCode"
|
|
295
|
+
},
|
|
296
|
+
"6ed8da90-450b-101b-98da-00aa003f1305": {
|
|
297
|
+
3: "globalAppointmentId",
|
|
298
|
+
40: "appointmentOldLocation"
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
/**
|
|
302
|
+
* Standard CFB sector size in bytes (512).
|
|
303
|
+
*/
|
|
304
|
+
var MSG_BURNER_SECTOR_SIZE = 512;
|
|
305
|
+
/**
|
|
306
|
+
* CFB mini-stream sector size in bytes (64).
|
|
307
|
+
*/
|
|
308
|
+
var MSG_BURNER_MINI_SECTOR_SIZE = 64;
|
|
309
|
+
/**
|
|
310
|
+
* Threshold below which streams are stored in the mini-stream (4096).
|
|
311
|
+
*/
|
|
312
|
+
var MSG_BURNER_MINI_STREAM_CUTOFF = 4096;
|
|
313
|
+
/**
|
|
314
|
+
* Number of 32-bit integers per standard sector (128).
|
|
315
|
+
*/
|
|
316
|
+
var MSG_BURNER_INTS_PER_SECTOR = 512 / 4;
|
|
317
|
+
/**
|
|
318
|
+
* Maximum DIFAT entries stored in the CFB header (109).
|
|
319
|
+
*/
|
|
320
|
+
var MSG_BURNER_DIFAT_HEADER_SLOTS = 109;
|
|
321
|
+
/**
|
|
322
|
+
* CFB directory entry size in bytes (128).
|
|
323
|
+
*/
|
|
324
|
+
var MSG_BURNER_DIR_ENTRY_SIZE = 128;
|
|
325
|
+
/**
|
|
326
|
+
* FAT sector marker: this sector holds FAT data (-3).
|
|
327
|
+
*/
|
|
328
|
+
var MSG_BURNER_FAT_SECTOR_MARKER = -3;
|
|
329
|
+
/**
|
|
330
|
+
* DIFAT sector marker: this sector holds DIFAT data (-4).
|
|
331
|
+
*/
|
|
332
|
+
var MSG_BURNER_DIFAT_SECTOR_MARKER = -4;
|
|
333
|
+
/**
|
|
334
|
+
* Maximum UTF-16 code units allowed in a CFB directory entry name (31).
|
|
335
|
+
* The fixed 64-byte name field holds 32 UTF-16 units including the
|
|
336
|
+
* NUL terminator, so the name itself is capped at 31 units.
|
|
337
|
+
*/
|
|
338
|
+
var MSG_BURNER_NAME_MAX = 31;
|
|
339
|
+
/**
|
|
340
|
+
* Root entry CLSID for MSG compound files.
|
|
341
|
+
*/
|
|
342
|
+
var MSG_BURNER_ROOT_CLSID = new Uint8Array([
|
|
343
|
+
11,
|
|
344
|
+
13,
|
|
345
|
+
2,
|
|
346
|
+
0,
|
|
347
|
+
0,
|
|
348
|
+
0,
|
|
349
|
+
0,
|
|
350
|
+
0,
|
|
351
|
+
192,
|
|
352
|
+
0,
|
|
353
|
+
0,
|
|
354
|
+
0,
|
|
355
|
+
0,
|
|
356
|
+
0,
|
|
357
|
+
0,
|
|
358
|
+
70
|
|
359
|
+
]);
|
|
360
|
+
/**
|
|
361
|
+
* File extensions recognized as RFC 2822 / MIME email files.
|
|
362
|
+
*/
|
|
363
|
+
var EML_EXTENSIONS = [".eml"];
|
|
364
|
+
/**
|
|
365
|
+
* File extensions recognized as Outlook binary email files.
|
|
366
|
+
*/
|
|
367
|
+
var MSG_EXTENSIONS = [".msg"];
|
|
368
|
+
/**
|
|
369
|
+
* MIME types recognized as RFC 2822 / MIME email files.
|
|
370
|
+
*/
|
|
371
|
+
var EML_MIME_TYPES = ["message/rfc822"];
|
|
372
|
+
/**
|
|
373
|
+
* MIME types recognized as Outlook binary email files.
|
|
374
|
+
*/
|
|
375
|
+
var MSG_MIME_TYPES = ["application/vnd.ms-outlook"];
|
|
376
|
+
/**
|
|
377
|
+
* Default charset for decoding MIME part bodies.
|
|
378
|
+
*/
|
|
379
|
+
var FALLBACK_CHARSET = "utf-8";
|
|
380
|
+
/**
|
|
381
|
+
* Default file name for attachments without an explicit name.
|
|
382
|
+
*/
|
|
383
|
+
var FALLBACK_ATTACHMENT_NAME = "attachment";
|
|
384
|
+
/**
|
|
385
|
+
* Common MIME types to file extensions mapping.
|
|
386
|
+
* Used for inferring the correct extension during file extraction.
|
|
387
|
+
*/
|
|
388
|
+
var MIME_EXTENSIONS = /* @__PURE__ */ new Map([
|
|
389
|
+
["image/jpeg", ".jpg"],
|
|
390
|
+
["image/jpg", ".jpg"],
|
|
391
|
+
["image/png", ".png"],
|
|
392
|
+
["image/gif", ".gif"],
|
|
393
|
+
["image/webp", ".webp"],
|
|
394
|
+
["application/pdf", ".pdf"],
|
|
395
|
+
["text/plain", ".txt"],
|
|
396
|
+
["text/csv", ".csv"],
|
|
397
|
+
["text/html", ".html"],
|
|
398
|
+
["application/json", ".json"],
|
|
399
|
+
["application/zip", ".zip"],
|
|
400
|
+
["application/vnd.ms-excel", ".xls"],
|
|
401
|
+
["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ".xlsx"],
|
|
402
|
+
["application/msword", ".doc"],
|
|
403
|
+
["application/vnd.openxmlformats-officedocument.wordprocessingml.document", ".docx"],
|
|
404
|
+
["application/vnd.ms-powerpoint", ".ppt"],
|
|
405
|
+
["application/vnd.openxmlformats-officedocument.presentationml.presentation", ".pptx"],
|
|
406
|
+
["message/rfc822", ".eml"],
|
|
407
|
+
["application/vnd.ms-outlook", ".msg"]
|
|
408
|
+
]);
|
|
409
|
+
/**
|
|
410
|
+
* Maximum multipart nesting depth accepted by `parseMIMEPart`.
|
|
411
|
+
* Guards against pathological or hostile MIME trees causing
|
|
412
|
+
* unbounded recursion.
|
|
413
|
+
*/
|
|
414
|
+
var MIME_MAX_DEPTH = 50;
|
|
415
|
+
/**
|
|
416
|
+
* Minimum valid code point for each UTF-8 sequence length, keyed by the
|
|
417
|
+
* number of continuation bytes (1, 2, or 3). Enforces the WHATWG
|
|
418
|
+
* requirement that a sequence encode the shortest possible form — an
|
|
419
|
+
* overlong encoding (a code point below its sequence's minimum) is
|
|
420
|
+
* rejected rather than accepted by `decodeUTF8`.
|
|
421
|
+
*/
|
|
422
|
+
var UTF8_SEQUENCE_MINIMUM = {
|
|
423
|
+
1: 128,
|
|
424
|
+
2: 2048,
|
|
425
|
+
3: 65536
|
|
426
|
+
};
|
|
427
|
+
/**
|
|
428
|
+
* Windows-1252 high-byte (0x80-0x9F) to Unicode code point lookup.
|
|
429
|
+
* Index `n` maps byte `0x80 + n` to its Unicode code point; entries
|
|
430
|
+
* that Windows-1252 leaves undefined map to the byte's own value
|
|
431
|
+
* (C1 control code passthrough) per the WHATWG encoding standard.
|
|
432
|
+
*/
|
|
433
|
+
var WINDOWS_1252_HIGH = [
|
|
434
|
+
8364,
|
|
435
|
+
129,
|
|
436
|
+
8218,
|
|
437
|
+
402,
|
|
438
|
+
8222,
|
|
439
|
+
8230,
|
|
440
|
+
8224,
|
|
441
|
+
8225,
|
|
442
|
+
710,
|
|
443
|
+
8240,
|
|
444
|
+
352,
|
|
445
|
+
8249,
|
|
446
|
+
338,
|
|
447
|
+
141,
|
|
448
|
+
381,
|
|
449
|
+
143,
|
|
450
|
+
144,
|
|
451
|
+
8216,
|
|
452
|
+
8217,
|
|
453
|
+
8220,
|
|
454
|
+
8221,
|
|
455
|
+
8226,
|
|
456
|
+
8211,
|
|
457
|
+
8212,
|
|
458
|
+
732,
|
|
459
|
+
8482,
|
|
460
|
+
353,
|
|
461
|
+
8250,
|
|
462
|
+
339,
|
|
463
|
+
157,
|
|
464
|
+
382,
|
|
465
|
+
376
|
|
466
|
+
];
|
|
467
|
+
//#endregion
|
|
468
|
+
//#region src/core/errors.ts
|
|
469
|
+
/**
|
|
470
|
+
* An error thrown or returned by the MSG/EML parsing and burning surfaces.
|
|
471
|
+
*
|
|
472
|
+
* @remarks
|
|
473
|
+
* Carries a machine-readable {@link MSGErrorCode} so a `catch` (or a
|
|
474
|
+
* `Failure.error` branch) can dispatch on `error.code` instead of parsing
|
|
475
|
+
* the message text. `context` carries whatever structured detail the
|
|
476
|
+
* throwing site has on hand (e.g. `{ offset, expected }`).
|
|
477
|
+
*/
|
|
478
|
+
var MSGError = class extends Error {
|
|
479
|
+
code;
|
|
480
|
+
context;
|
|
481
|
+
constructor(code, message, context) {
|
|
482
|
+
super(message);
|
|
483
|
+
this.name = "MSGError";
|
|
484
|
+
this.code = code;
|
|
485
|
+
this.context = context;
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
/**
|
|
489
|
+
* Narrow an unknown caught (or `Failure.error`) value to an {@link MSGError}.
|
|
490
|
+
*
|
|
491
|
+
* @param value - The value to test (typically a `catch` binding or a `Result.error`)
|
|
492
|
+
* @returns `true` when `value` is an {@link MSGError}
|
|
493
|
+
*
|
|
494
|
+
* @example
|
|
495
|
+
* ```ts
|
|
496
|
+
* import { isMSGError } from '@src/core'
|
|
497
|
+
*
|
|
498
|
+
* try {
|
|
499
|
+
* reader.parse()
|
|
500
|
+
* } catch (error) {
|
|
501
|
+
* if (isMSGError(error) && error.code === 'MALFORMED') return
|
|
502
|
+
* }
|
|
503
|
+
* ```
|
|
504
|
+
*/
|
|
505
|
+
function isMSGError(value) {
|
|
506
|
+
return value instanceof MSGError;
|
|
507
|
+
}
|
|
508
|
+
//#endregion
|
|
509
|
+
//#region src/core/helpers.ts
|
|
510
|
+
/**
|
|
511
|
+
* Construct a {@link Success} wrapping a value.
|
|
512
|
+
*
|
|
513
|
+
* @param value - The value to wrap
|
|
514
|
+
* @returns A `Success<T>` result
|
|
515
|
+
*
|
|
516
|
+
* @example
|
|
517
|
+
* ```ts
|
|
518
|
+
* const result = success(42) // { success: true, value: 42 }
|
|
519
|
+
* ```
|
|
520
|
+
*/
|
|
521
|
+
function success(value) {
|
|
522
|
+
return {
|
|
523
|
+
success: true,
|
|
524
|
+
value
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Construct a {@link Failure} wrapping an error.
|
|
529
|
+
*
|
|
530
|
+
* @param error - The error to wrap
|
|
531
|
+
* @returns A `Failure<E>` result
|
|
532
|
+
*
|
|
533
|
+
* @example
|
|
534
|
+
* ```ts
|
|
535
|
+
* const result = failure(new MSGError('MALFORMED', 'bad input'))
|
|
536
|
+
* ```
|
|
537
|
+
*/
|
|
538
|
+
function failure(error) {
|
|
539
|
+
return {
|
|
540
|
+
success: false,
|
|
541
|
+
error
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Narrow a Result to Success.
|
|
546
|
+
*
|
|
547
|
+
* @param result - Result to check
|
|
548
|
+
* @returns True when result is Success
|
|
549
|
+
*/
|
|
550
|
+
function isSuccess(result) {
|
|
551
|
+
return result.success;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Narrow a Result to Failure.
|
|
555
|
+
*
|
|
556
|
+
* @param result - Result to check
|
|
557
|
+
* @returns True when result is Failure
|
|
558
|
+
*/
|
|
559
|
+
function isFailure(result) {
|
|
560
|
+
return !result.success;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Narrow an unknown value to a plain record.
|
|
564
|
+
*
|
|
565
|
+
* @param value - Value to check
|
|
566
|
+
* @returns True when value is a non-null, non-array object
|
|
567
|
+
*/
|
|
568
|
+
function isRecord(value) {
|
|
569
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Validate that a DataView starts with the CFB magic header.
|
|
573
|
+
*
|
|
574
|
+
* @param view - DataView to check
|
|
575
|
+
* @returns True when the first 8 bytes match the CFB signature
|
|
576
|
+
*/
|
|
577
|
+
function isMSGFile(view) {
|
|
578
|
+
const header = [
|
|
579
|
+
208,
|
|
580
|
+
207,
|
|
581
|
+
17,
|
|
582
|
+
224,
|
|
583
|
+
161,
|
|
584
|
+
177,
|
|
585
|
+
26,
|
|
586
|
+
225
|
|
587
|
+
];
|
|
588
|
+
if (view.byteLength < header.length) return false;
|
|
589
|
+
for (let i = 0; i < header.length; i++) if (view.getUint8(i) !== header[i]) return false;
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Remove trailing null characters from a string.
|
|
594
|
+
*
|
|
595
|
+
* @param text - Input string
|
|
596
|
+
* @returns String with trailing nulls removed
|
|
597
|
+
*/
|
|
598
|
+
function removeTrailingNull(text) {
|
|
599
|
+
const index = text.indexOf("\0");
|
|
600
|
+
if (index !== -1) return text.substring(0, index);
|
|
601
|
+
return text;
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Read a UTF-16LE string from a DataView.
|
|
605
|
+
*
|
|
606
|
+
* @param view - DataView to read from
|
|
607
|
+
* @param offset - Byte offset to start reading
|
|
608
|
+
* @param charCount - Number of UTF-16 code units to read
|
|
609
|
+
* @returns Decoded string
|
|
610
|
+
* @throws {@link MSGError} with code `MALFORMED` when the requested range
|
|
611
|
+
* exceeds the view's bounds
|
|
612
|
+
*/
|
|
613
|
+
function readUTF16String(view, offset, charCount) {
|
|
614
|
+
const end = offset + charCount * 2;
|
|
615
|
+
if (offset < 0 || charCount < 0 || end > view.byteLength) throw new MSGError("MALFORMED", "UTF-16 string range exceeds view bounds", {
|
|
616
|
+
offset,
|
|
617
|
+
charCount,
|
|
618
|
+
byteLength: view.byteLength
|
|
619
|
+
});
|
|
620
|
+
let result = "";
|
|
621
|
+
for (let i = 0; i < charCount; i++) {
|
|
622
|
+
const code = view.getUint16(offset + i * 2, true);
|
|
623
|
+
result += String.fromCharCode(code);
|
|
624
|
+
}
|
|
625
|
+
return result;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Read a non-Unicode (PT_STRING8) string from a byte array using a
|
|
629
|
+
* pure-ES decoder — no `TextDecoder` dependency, so this stays usable
|
|
630
|
+
* in the core's DOM/Node-free environment.
|
|
631
|
+
*
|
|
632
|
+
* @param data - Binary data
|
|
633
|
+
* @param encoding - Encoding to decode with (default `'windows-1252'`)
|
|
634
|
+
* @returns Decoded string
|
|
635
|
+
*
|
|
636
|
+
* @example
|
|
637
|
+
* ```ts
|
|
638
|
+
* readANSIString(new Uint8Array([0x93, 0x41, 0x94])) // '“A”'
|
|
639
|
+
* ```
|
|
640
|
+
*/
|
|
641
|
+
function readANSIString(data, encoding) {
|
|
642
|
+
const resolved = encoding ?? "windows-1252";
|
|
643
|
+
if (resolved === "utf-16le") return readUTF16String(new DataView(data.buffer, data.byteOffset, data.byteLength), 0, Math.floor(data.length / 2));
|
|
644
|
+
if (resolved === "utf-8") return decodeUTF8(data);
|
|
645
|
+
if (resolved === "latin1") return decodeLatin1(data);
|
|
646
|
+
return decodeWindows1252(data);
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Convert a Windows FILETIME (100-ns intervals since 1601-01-01) to a UTC date string.
|
|
650
|
+
* Combines the low/high 32-bit halves with `BigInt` so the 64-bit interval
|
|
651
|
+
* count never loses precision to float64 rounding.
|
|
652
|
+
*
|
|
653
|
+
* @param low - Low 32 bits of FILETIME
|
|
654
|
+
* @param high - High 32 bits of FILETIME
|
|
655
|
+
* @returns UTC date string
|
|
656
|
+
*/
|
|
657
|
+
function fileTimeToUTCString(low, high) {
|
|
658
|
+
const unixMs = (BigInt(low >>> 0) + (BigInt(high >>> 0) << 32n) - 116444736000000000n) / 10000n;
|
|
659
|
+
return new Date(Number(unixMs)).toUTCString();
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Convert a number to a lowercase hex string with specified padding.
|
|
663
|
+
*
|
|
664
|
+
* @param value - Number to convert
|
|
665
|
+
* @param length - Minimum hex string length (zero-padded)
|
|
666
|
+
* @returns Lowercase hex string
|
|
667
|
+
*/
|
|
668
|
+
function toHexLower(value, length) {
|
|
669
|
+
const hex = "0123456789abcdef";
|
|
670
|
+
let result = "";
|
|
671
|
+
let remaining = value >>> 0;
|
|
672
|
+
for (let i = 0; i < length; i++) {
|
|
673
|
+
result = hex[remaining & 15] + result;
|
|
674
|
+
remaining = remaining >>> 4;
|
|
675
|
+
}
|
|
676
|
+
return result;
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Stringify a mixed-endian Microsoft UUID from a byte array.
|
|
680
|
+
*
|
|
681
|
+
* @param data - Byte array containing the UUID
|
|
682
|
+
* @param offset - Byte offset to start reading
|
|
683
|
+
* @returns UUID string in lowercase
|
|
684
|
+
*/
|
|
685
|
+
function msftUUIDStringify(data, offset) {
|
|
686
|
+
const hex = "0123456789abcdef";
|
|
687
|
+
const b = (i) => hex[data[offset + i] >> 4 & 15] + hex[data[offset + i] & 15];
|
|
688
|
+
return b(3) + b(2) + b(1) + b(0) + "-" + b(5) + b(4) + "-" + b(7) + b(6) + "-" + b(8) + b(9) + "-" + b(10) + b(11) + b(12) + b(13) + b(14) + b(15);
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Round a value up to the nearest multiple of a boundary.
|
|
692
|
+
*
|
|
693
|
+
* @param value - Number to round
|
|
694
|
+
* @param boundary - Must be a power of 2
|
|
695
|
+
* @returns Rounded value
|
|
696
|
+
*/
|
|
697
|
+
function roundUpToMultiple(value, boundary) {
|
|
698
|
+
return value + boundary - 1 & ~(boundary - 1);
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Compute how many sectors are needed to hold a given byte count.
|
|
702
|
+
*
|
|
703
|
+
* @param bytes - Total byte count
|
|
704
|
+
* @param sectorSize - Sector size in bytes
|
|
705
|
+
* @returns Number of sectors (0 when bytes ≤ 0)
|
|
706
|
+
*/
|
|
707
|
+
function sectorsNeeded(bytes, sectorSize) {
|
|
708
|
+
if (bytes <= 0) return 0;
|
|
709
|
+
return roundUpToMultiple(bytes, sectorSize) / sectorSize;
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* CFB-compliant directory name comparator.
|
|
713
|
+
* Compares by UTF-16 length first, then by uppercased code points.
|
|
714
|
+
*
|
|
715
|
+
* @param a - First name
|
|
716
|
+
* @param b - Second name
|
|
717
|
+
* @returns Negative, zero, or positive
|
|
718
|
+
*/
|
|
719
|
+
function compareCFBName(a, b) {
|
|
720
|
+
const diff = a.length - b.length;
|
|
721
|
+
if (diff !== 0) return diff;
|
|
722
|
+
const x = a.toUpperCase();
|
|
723
|
+
const y = b.toUpperCase();
|
|
724
|
+
if (x > y) return 1;
|
|
725
|
+
if (x < y) return -1;
|
|
726
|
+
return 0;
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Decode a Base64 string into raw bytes without relying on `atob`.
|
|
730
|
+
* Ignores ASCII whitespace and tolerates missing padding.
|
|
731
|
+
*
|
|
732
|
+
* @param text - Base64-encoded string
|
|
733
|
+
* @returns Decoded byte array
|
|
734
|
+
* @throws {@link MSGError} with code `MALFORMED` when the string contains
|
|
735
|
+
* a character outside the Base64 alphabet
|
|
736
|
+
*
|
|
737
|
+
* @example
|
|
738
|
+
* ```ts
|
|
739
|
+
* decodeBase64('SGVsbG8=') // Uint8Array [72, 101, 108, 108, 111]
|
|
740
|
+
* ```
|
|
741
|
+
*/
|
|
742
|
+
function decodeBase64(text) {
|
|
743
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
744
|
+
const cleaned = text.replace(/[ \t\n\r\f\v]+/g, "").replace(/=+$/, "");
|
|
745
|
+
const bytes = [];
|
|
746
|
+
let buffer = 0;
|
|
747
|
+
let bits = 0;
|
|
748
|
+
for (const char of cleaned) {
|
|
749
|
+
const index = alphabet.indexOf(char);
|
|
750
|
+
if (index === -1) throw new MSGError("MALFORMED", `Invalid Base64 character: ${char}`, { char });
|
|
751
|
+
buffer = buffer << 6 | index;
|
|
752
|
+
bits += 6;
|
|
753
|
+
if (bits >= 8) {
|
|
754
|
+
bits -= 8;
|
|
755
|
+
bytes.push(buffer >> bits & 255);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return new Uint8Array(bytes);
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Encode a string into UTF-8 bytes, handling surrogate pairs.
|
|
762
|
+
* A lone (unpaired) surrogate encodes as U+FFFD.
|
|
763
|
+
*
|
|
764
|
+
* @param text - String to encode
|
|
765
|
+
* @returns UTF-8 byte array
|
|
766
|
+
*
|
|
767
|
+
* @example
|
|
768
|
+
* ```ts
|
|
769
|
+
* encodeUTF8('A') // Uint8Array [65]
|
|
770
|
+
* ```
|
|
771
|
+
*/
|
|
772
|
+
function encodeUTF8(text) {
|
|
773
|
+
const bytes = [];
|
|
774
|
+
for (let i = 0; i < text.length; i++) {
|
|
775
|
+
let code = text.charCodeAt(i);
|
|
776
|
+
if (code >= 55296 && code <= 56319) {
|
|
777
|
+
const next = text.charCodeAt(i + 1);
|
|
778
|
+
if (next >= 56320 && next <= 57343) {
|
|
779
|
+
code = (code - 55296) * 1024 + (next - 56320) + 65536;
|
|
780
|
+
i++;
|
|
781
|
+
} else code = 65533;
|
|
782
|
+
} else if (code >= 56320 && code <= 57343) code = 65533;
|
|
783
|
+
if (code < 128) bytes.push(code);
|
|
784
|
+
else if (code < 2048) bytes.push(192 | code >> 6, 128 | code & 63);
|
|
785
|
+
else if (code < 65536) bytes.push(224 | code >> 12, 128 | code >> 6 & 63, 128 | code & 63);
|
|
786
|
+
else bytes.push(240 | code >> 18, 128 | code >> 12 & 63, 128 | code >> 6 & 63, 128 | code & 63);
|
|
787
|
+
}
|
|
788
|
+
return new Uint8Array(bytes);
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Decode UTF-8 bytes into a string, WHATWG-style: an invalid byte
|
|
792
|
+
* sequence decodes as U+FFFD rather than throwing. Rejects overlong
|
|
793
|
+
* encodings, surrogate code points (0xD800-0xDFFF), and code points
|
|
794
|
+
* beyond 0x10FFFF — each invalid sequence yields exactly one U+FFFD
|
|
795
|
+
* and decoding resumes at the next lead byte.
|
|
796
|
+
*
|
|
797
|
+
* @param bytes - UTF-8 byte array
|
|
798
|
+
* @returns Decoded string
|
|
799
|
+
*
|
|
800
|
+
* @example
|
|
801
|
+
* ```ts
|
|
802
|
+
* decodeUTF8(new Uint8Array([65])) // 'A'
|
|
803
|
+
* decodeUTF8(new Uint8Array([0xff])) // '�'
|
|
804
|
+
* ```
|
|
805
|
+
*/
|
|
806
|
+
function decodeUTF8(bytes) {
|
|
807
|
+
let result = "";
|
|
808
|
+
let i = 0;
|
|
809
|
+
while (i < bytes.length) {
|
|
810
|
+
const byte0 = bytes[i];
|
|
811
|
+
if (byte0 < 128) {
|
|
812
|
+
result += String.fromCharCode(byte0);
|
|
813
|
+
i++;
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
let length = 0;
|
|
817
|
+
let codePoint = 0;
|
|
818
|
+
if ((byte0 & 224) === 192) {
|
|
819
|
+
length = 1;
|
|
820
|
+
codePoint = byte0 & 31;
|
|
821
|
+
} else if ((byte0 & 240) === 224) {
|
|
822
|
+
length = 2;
|
|
823
|
+
codePoint = byte0 & 15;
|
|
824
|
+
} else if ((byte0 & 248) === 240) {
|
|
825
|
+
length = 3;
|
|
826
|
+
codePoint = byte0 & 7;
|
|
827
|
+
} else {
|
|
828
|
+
result += "�";
|
|
829
|
+
i++;
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
if (i + length >= bytes.length) {
|
|
833
|
+
result += "�";
|
|
834
|
+
i++;
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
let valid = true;
|
|
838
|
+
let value = codePoint;
|
|
839
|
+
for (let j = 1; j <= length; j++) {
|
|
840
|
+
const next = bytes[i + j];
|
|
841
|
+
if ((next & 192) !== 128) {
|
|
842
|
+
valid = false;
|
|
843
|
+
break;
|
|
844
|
+
}
|
|
845
|
+
value = value << 6 | next & 63;
|
|
846
|
+
}
|
|
847
|
+
if (!valid) {
|
|
848
|
+
result += "�";
|
|
849
|
+
i++;
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
const minimum = UTF8_SEQUENCE_MINIMUM[length];
|
|
853
|
+
if (minimum !== void 0 && value < minimum || value >= 55296 && value <= 57343 || value > 1114111) {
|
|
854
|
+
result += "�";
|
|
855
|
+
i += length + 1;
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
i += length + 1;
|
|
859
|
+
if (value >= 65536) {
|
|
860
|
+
value -= 65536;
|
|
861
|
+
result += String.fromCharCode(55296 + (value >> 10), 56320 + (value & 1023));
|
|
862
|
+
} else result += String.fromCharCode(value);
|
|
863
|
+
}
|
|
864
|
+
return result;
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Decode Latin-1 (ISO-8859-1) bytes into a string, byte-for-code-point.
|
|
868
|
+
*
|
|
869
|
+
* @param bytes - Latin-1 byte array
|
|
870
|
+
* @returns Decoded string
|
|
871
|
+
*
|
|
872
|
+
* @example
|
|
873
|
+
* ```ts
|
|
874
|
+
* decodeLatin1(new Uint8Array([0xe9])) // 'é'
|
|
875
|
+
* ```
|
|
876
|
+
*/
|
|
877
|
+
function decodeLatin1(bytes) {
|
|
878
|
+
let result = "";
|
|
879
|
+
for (let i = 0; i < bytes.length; i++) result += String.fromCharCode(bytes[i]);
|
|
880
|
+
return result;
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Decode Windows-1252 bytes into a string. Identical to {@link decodeLatin1}
|
|
884
|
+
* except for the 0x80-0x9F range, which maps through {@link WINDOWS_1252_HIGH}.
|
|
885
|
+
*
|
|
886
|
+
* @param bytes - Windows-1252 byte array
|
|
887
|
+
* @returns Decoded string
|
|
888
|
+
*
|
|
889
|
+
* @example
|
|
890
|
+
* ```ts
|
|
891
|
+
* decodeWindows1252(new Uint8Array([0x93])) // '“'
|
|
892
|
+
* ```
|
|
893
|
+
*/
|
|
894
|
+
function decodeWindows1252(bytes) {
|
|
895
|
+
let result = "";
|
|
896
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
897
|
+
const byte = bytes[i];
|
|
898
|
+
if (byte >= 128 && byte <= 159) result += String.fromCodePoint(WINDOWS_1252_HIGH[byte - 128]);
|
|
899
|
+
else result += String.fromCharCode(byte);
|
|
900
|
+
}
|
|
901
|
+
return result;
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Resolve a free-form charset label (as seen in a MIME `charset` parameter)
|
|
905
|
+
* to a supported {@link MSGEncoding}. Unknown or absent labels fall back
|
|
906
|
+
* to `'utf-8'`.
|
|
907
|
+
*
|
|
908
|
+
* @param label - Charset label to resolve (case-insensitive)
|
|
909
|
+
* @returns Resolved encoding
|
|
910
|
+
*
|
|
911
|
+
* @example
|
|
912
|
+
* ```ts
|
|
913
|
+
* resolveEncoding('ISO-8859-1') // 'latin1'
|
|
914
|
+
* resolveEncoding(undefined) // 'utf-8'
|
|
915
|
+
* ```
|
|
916
|
+
*/
|
|
917
|
+
function resolveEncoding(label) {
|
|
918
|
+
const lower = label?.trim().toLowerCase();
|
|
919
|
+
if (lower === "utf-8" || lower === "utf8") return "utf-8";
|
|
920
|
+
if (lower === "utf-16le" || lower === "utf-16") return "utf-16le";
|
|
921
|
+
if (lower === "windows-1252" || lower === "cp1252") return "windows-1252";
|
|
922
|
+
if (lower === "us-ascii" || lower === "ascii" || lower === "iso-8859-1" || lower === "iso8859-1" || lower === "latin1") return "latin1";
|
|
923
|
+
return "utf-8";
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Narrow an unknown value to a valid EmailFormat.
|
|
927
|
+
*
|
|
928
|
+
* @param value - Value to check
|
|
929
|
+
* @returns True when value is 'eml' or 'msg'
|
|
930
|
+
*/
|
|
931
|
+
function isEmailFormat(value) {
|
|
932
|
+
return value === "eml" || value === "msg";
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Derive the EmailFormat from a file name and/or MIME type.
|
|
936
|
+
* Returns undefined when the format cannot be determined.
|
|
937
|
+
*
|
|
938
|
+
* @param name - File name to inspect
|
|
939
|
+
* @param mime - MIME type to inspect
|
|
940
|
+
* @returns Detected format or undefined
|
|
941
|
+
*
|
|
942
|
+
* @example
|
|
943
|
+
* ```ts
|
|
944
|
+
* detectFormat('message.eml', undefined) // 'eml'
|
|
945
|
+
* detectFormat(undefined, 'application/vnd.ms-outlook') // 'msg'
|
|
946
|
+
* ```
|
|
947
|
+
*/
|
|
948
|
+
function detectFormat(name, mime) {
|
|
949
|
+
const lower = name?.toLowerCase();
|
|
950
|
+
if (lower?.endsWith(".eml") === true) return "eml";
|
|
951
|
+
if (lower?.endsWith(".msg") === true) return "msg";
|
|
952
|
+
if (mime === "message/rfc822") return "eml";
|
|
953
|
+
if (mime === "application/vnd.ms-outlook") return "msg";
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Parse headers from a raw RFC 2822 / MIME header text block.
|
|
957
|
+
*
|
|
958
|
+
* @param text - Raw header text
|
|
959
|
+
* @returns Map of parsed header objects
|
|
960
|
+
*/
|
|
961
|
+
function parseMIMEHeaders(text) {
|
|
962
|
+
const raw = [];
|
|
963
|
+
for (const line of text.split("\n")) {
|
|
964
|
+
if (line === "") continue;
|
|
965
|
+
if ((line.startsWith(" ") || line.startsWith(" ")) && raw.length > 0) {
|
|
966
|
+
const last = raw[raw.length - 1];
|
|
967
|
+
if (last !== void 0) raw[raw.length - 1] = {
|
|
968
|
+
name: last.name,
|
|
969
|
+
value: last.value + " " + line.trim()
|
|
970
|
+
};
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
const colon = line.indexOf(":");
|
|
974
|
+
if (colon === -1) continue;
|
|
975
|
+
raw.push({
|
|
976
|
+
name: line.slice(0, colon).trim().toLowerCase(),
|
|
977
|
+
value: line.slice(colon + 1).trim()
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
const map = /* @__PURE__ */ new Map();
|
|
981
|
+
for (const { name, value } of raw) if (!map.has(name)) {
|
|
982
|
+
const segments = value.split(";");
|
|
983
|
+
const val = (segments[0] ?? "").trim();
|
|
984
|
+
const params = /* @__PURE__ */ new Map();
|
|
985
|
+
for (let i = 1; i < segments.length; i++) {
|
|
986
|
+
const segment = (segments[i] ?? "").trim();
|
|
987
|
+
const eq = segment.indexOf("=");
|
|
988
|
+
if (eq === -1) continue;
|
|
989
|
+
const k = segment.slice(0, eq).trim().toLowerCase();
|
|
990
|
+
let v = segment.slice(eq + 1).trim();
|
|
991
|
+
if (v.startsWith("\"") && v.endsWith("\"")) v = v.slice(1, -1);
|
|
992
|
+
params.set(k, v);
|
|
993
|
+
}
|
|
994
|
+
map.set(name, {
|
|
995
|
+
value: val,
|
|
996
|
+
params
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
return map;
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Parse a raw RFC 2822 / MIME text string into a MIMEPart tree.
|
|
1003
|
+
* Line endings are normalised to \n before processing. Recursion is
|
|
1004
|
+
* capped at {@link MIME_MAX_DEPTH} to guard against a hostile or
|
|
1005
|
+
* pathological multipart nesting cycle.
|
|
1006
|
+
*
|
|
1007
|
+
* @param raw - Raw MIME text
|
|
1008
|
+
* @param depth - Current recursion depth (internal; callers omit this)
|
|
1009
|
+
* @returns Parsed MIMEPart tree
|
|
1010
|
+
* @throws {@link MSGError} with code `CYCLE` when nesting exceeds {@link MIME_MAX_DEPTH}
|
|
1011
|
+
*/
|
|
1012
|
+
function parseMIMEPart(raw, depth = 0) {
|
|
1013
|
+
if (depth > 50) throw new MSGError("CYCLE", "MIME multipart nesting exceeds maximum depth", {
|
|
1014
|
+
depth,
|
|
1015
|
+
max: 50
|
|
1016
|
+
});
|
|
1017
|
+
const normalised = raw.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1018
|
+
const split = normalised.indexOf("\n\n");
|
|
1019
|
+
const headerText = split === -1 ? normalised : normalised.slice(0, split);
|
|
1020
|
+
const body = split === -1 ? "" : normalised.slice(split + 2);
|
|
1021
|
+
const headers = parseMIMEHeaders(headerText);
|
|
1022
|
+
const contentType = headers.get("content-type");
|
|
1023
|
+
const primaryType = (contentType?.value ?? "").split(";")[0].trim().toLowerCase();
|
|
1024
|
+
const boundary = contentType?.params.get("boundary") ?? "";
|
|
1025
|
+
const parts = [];
|
|
1026
|
+
if (primaryType.startsWith("multipart/") && boundary !== "") {
|
|
1027
|
+
const delimiter = "--" + boundary;
|
|
1028
|
+
const lines = body.split("\n");
|
|
1029
|
+
let current = [];
|
|
1030
|
+
let inside = false;
|
|
1031
|
+
for (const line of lines) {
|
|
1032
|
+
const trimmed = line.trimEnd();
|
|
1033
|
+
if (trimmed === delimiter + "--") {
|
|
1034
|
+
if (inside && current.length > 0) parts.push(parseMIMEPart(current.join("\n"), depth + 1));
|
|
1035
|
+
inside = false;
|
|
1036
|
+
break;
|
|
1037
|
+
}
|
|
1038
|
+
if (trimmed === delimiter) {
|
|
1039
|
+
if (inside && current.length > 0) parts.push(parseMIMEPart(current.join("\n"), depth + 1));
|
|
1040
|
+
current = [];
|
|
1041
|
+
inside = true;
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
if (inside) current.push(line);
|
|
1045
|
+
}
|
|
1046
|
+
if (inside && current.length > 0) parts.push(parseMIMEPart(current.join("\n"), depth + 1));
|
|
1047
|
+
}
|
|
1048
|
+
return {
|
|
1049
|
+
headers,
|
|
1050
|
+
body,
|
|
1051
|
+
parts
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Decode a MIME-encoded body string into a raw byte array.
|
|
1056
|
+
*
|
|
1057
|
+
* @param body - Raw encoded string
|
|
1058
|
+
* @param encoding - Encoding type (e.g., 'base64', 'quoted-printable')
|
|
1059
|
+
* @returns Decoded byte array
|
|
1060
|
+
* @throws {@link MSGError} with code `MALFORMED` when `encoding` is `'base64'`
|
|
1061
|
+
* and `body` contains an invalid Base64 character
|
|
1062
|
+
*/
|
|
1063
|
+
function decodeMIMEEncoding(body, encoding) {
|
|
1064
|
+
const enc = encoding.toLowerCase();
|
|
1065
|
+
if (enc === "base64") return decodeBase64(body);
|
|
1066
|
+
if (enc === "quoted-printable") {
|
|
1067
|
+
const unwrapped = body.replace(/=\r?\n/g, "");
|
|
1068
|
+
const result = [];
|
|
1069
|
+
let i = 0;
|
|
1070
|
+
while (i < unwrapped.length) {
|
|
1071
|
+
if (unwrapped[i] === "=" && i + 2 < unwrapped.length) {
|
|
1072
|
+
const hex = unwrapped.slice(i + 1, i + 3);
|
|
1073
|
+
if (/^[0-9A-Fa-f]{2}$/.test(hex)) {
|
|
1074
|
+
result.push(parseInt(hex, 16));
|
|
1075
|
+
i += 3;
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
result.push(unwrapped.charCodeAt(i));
|
|
1080
|
+
i++;
|
|
1081
|
+
}
|
|
1082
|
+
return new Uint8Array(result);
|
|
1083
|
+
}
|
|
1084
|
+
return encodeUTF8(body);
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Decode a MIME-encoded body into a text string based on an arbitrary
|
|
1088
|
+
* charset label, resolved via {@link resolveEncoding}.
|
|
1089
|
+
*
|
|
1090
|
+
* @param body - Raw encoded string
|
|
1091
|
+
* @param encoding - Transfer encoding type
|
|
1092
|
+
* @param charset - Character set label (e.g., 'utf-8')
|
|
1093
|
+
* @returns Decoded string
|
|
1094
|
+
*/
|
|
1095
|
+
function decodeMIMEText(body, encoding, charset) {
|
|
1096
|
+
const enc = encoding.toLowerCase();
|
|
1097
|
+
if (enc !== "base64" && enc !== "quoted-printable") return body;
|
|
1098
|
+
return readANSIString(decodeMIMEEncoding(body, encoding), resolveEncoding(charset));
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Decode RFC 2047 encoded words in header values.
|
|
1102
|
+
* Handles both Base64 (B) and Quoted-Printable (Q) forms.
|
|
1103
|
+
*
|
|
1104
|
+
* @param text - Header value string potentially containing encoded words
|
|
1105
|
+
* @returns Decoded string
|
|
1106
|
+
*
|
|
1107
|
+
* @example
|
|
1108
|
+
* ```ts
|
|
1109
|
+
* decodeMIMEWords('=?UTF-8?B?SGVsbG8=?=') // 'Hello'
|
|
1110
|
+
* ```
|
|
1111
|
+
*/
|
|
1112
|
+
function decodeMIMEWords(text) {
|
|
1113
|
+
return text.replace(/(=\?[^?]+\?[BbQq]\?[^?]*\?=)[ \t\r\n]+(?==\?[^?]+\?[BbQq]\?[^?]*\?=)/g, "$1").replace(/=\?([^?]+)\?([BbQq])\?([^?]*)\?=/g, (_match, charset, enc, encoded) => {
|
|
1114
|
+
try {
|
|
1115
|
+
return readANSIString(enc.toUpperCase() === "B" ? decodeMIMEEncoding(encoded, "base64") : decodeMIMEEncoding(encoded.replace(/_/g, " "), "quoted-printable"), resolveEncoding(charset));
|
|
1116
|
+
} catch {
|
|
1117
|
+
return encoded;
|
|
1118
|
+
}
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Format a name and email into a standard composite address.
|
|
1123
|
+
*
|
|
1124
|
+
* @param name - Display name
|
|
1125
|
+
* @param email - Email address
|
|
1126
|
+
* @returns Formatted address string
|
|
1127
|
+
*/
|
|
1128
|
+
function formatEmailAddress(name, email) {
|
|
1129
|
+
const n = name?.trim() ?? "";
|
|
1130
|
+
const e = email?.trim() ?? "";
|
|
1131
|
+
if (n.length > 0 && e.length > 0) return `${n} <${e}>`;
|
|
1132
|
+
if (n.length > 0) return n;
|
|
1133
|
+
return e;
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* Extract a single EmailMessage from a parsed MSGReader.
|
|
1137
|
+
* Reads field data and attachments from the reader interface.
|
|
1138
|
+
*
|
|
1139
|
+
* Each attachment is read independently: a corrupt attachment throws
|
|
1140
|
+
* from `reader.attachment(i)` is caught and that attachment is skipped
|
|
1141
|
+
* so the rest of the message still parses. This containment keeps one
|
|
1142
|
+
* damaged attachment stream from failing the entire message extraction.
|
|
1143
|
+
*
|
|
1144
|
+
* @param reader - A parsed MSGReaderInterface instance
|
|
1145
|
+
* @returns Structured EmailMessage
|
|
1146
|
+
*/
|
|
1147
|
+
function extractMessageFromMSG(reader) {
|
|
1148
|
+
const data = reader.parse();
|
|
1149
|
+
const from = formatEmailAddress(data.senderName, data.senderSMTPAddress ?? data.senderEmail);
|
|
1150
|
+
const recipients = data.recipients ?? [];
|
|
1151
|
+
const to = recipients.filter((r) => r.recipientRole === "to").map((r) => formatEmailAddress(r.name, r.smtpAddress ?? r.email)).filter((s) => s.length > 0);
|
|
1152
|
+
const cc = recipients.filter((r) => r.recipientRole === "cc").map((r) => formatEmailAddress(r.name, r.smtpAddress ?? r.email)).filter((s) => s.length > 0);
|
|
1153
|
+
const rawDate = data.messageDeliveryTime ?? data.clientSubmitTime;
|
|
1154
|
+
let date;
|
|
1155
|
+
if (rawDate !== void 0) {
|
|
1156
|
+
const parsed = new Date(rawDate);
|
|
1157
|
+
date = isNaN(parsed.getTime()) ? void 0 : parsed;
|
|
1158
|
+
}
|
|
1159
|
+
const attachments = [];
|
|
1160
|
+
const attachmentFields = data.attachments ?? [];
|
|
1161
|
+
for (let i = 0; i < attachmentFields.length; i++) {
|
|
1162
|
+
const attachment = attachmentFields[i];
|
|
1163
|
+
if (attachment === void 0) continue;
|
|
1164
|
+
if (attachment.attachmentHidden === true) continue;
|
|
1165
|
+
if (attachment.innerMSGContent === true) continue;
|
|
1166
|
+
try {
|
|
1167
|
+
const extracted = reader.attachment(i);
|
|
1168
|
+
attachments.push({
|
|
1169
|
+
name: extracted.fileName,
|
|
1170
|
+
mimeType: attachment.mimeType ?? "application/octet-stream",
|
|
1171
|
+
size: extracted.content.length,
|
|
1172
|
+
bytes: extracted.content
|
|
1173
|
+
});
|
|
1174
|
+
} catch {
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
return {
|
|
1179
|
+
from,
|
|
1180
|
+
to,
|
|
1181
|
+
cc,
|
|
1182
|
+
subject: data.subject ?? "",
|
|
1183
|
+
date,
|
|
1184
|
+
text: data.body ?? "",
|
|
1185
|
+
html: data.bodyHTML ?? "",
|
|
1186
|
+
attachments
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Extract a single EmailMessage from a top-level MIMEPart.
|
|
1191
|
+
* Walks the full MIME tree to collect text, HTML, and attachments.
|
|
1192
|
+
*
|
|
1193
|
+
* @param part - Root MIMEPart from parseMIMEPart
|
|
1194
|
+
* @returns Structured EmailMessage
|
|
1195
|
+
*/
|
|
1196
|
+
function extractMessage(part) {
|
|
1197
|
+
const headerValue = (name) => decodeMIMEWords(part.headers.get(name)?.value ?? "");
|
|
1198
|
+
const splitAddresses = (raw) => raw.length === 0 ? [] : raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
1199
|
+
const rawDate = part.headers.get("date")?.value;
|
|
1200
|
+
let date;
|
|
1201
|
+
if (rawDate !== void 0) {
|
|
1202
|
+
const parsed = new Date(rawDate);
|
|
1203
|
+
date = isNaN(parsed.getTime()) ? void 0 : parsed;
|
|
1204
|
+
}
|
|
1205
|
+
const collectedText = [];
|
|
1206
|
+
const collectedHTML = [];
|
|
1207
|
+
const attachments = [];
|
|
1208
|
+
const walk = (p) => {
|
|
1209
|
+
const contentType = p.headers.get("content-type");
|
|
1210
|
+
const disposition = p.headers.get("content-disposition");
|
|
1211
|
+
const transferEncoding = p.headers.get("content-transfer-encoding");
|
|
1212
|
+
const primaryType = (contentType?.value ?? "text/plain").split(";")[0].trim().toLowerCase();
|
|
1213
|
+
const encoding = (transferEncoding?.value ?? "7bit").trim();
|
|
1214
|
+
const charset = contentType?.params.get("charset") ?? "utf-8";
|
|
1215
|
+
const dispositionKind = (disposition?.value ?? "").trim().toLowerCase();
|
|
1216
|
+
if (primaryType.startsWith("multipart/")) {
|
|
1217
|
+
for (const child of p.parts) walk(child);
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
if (dispositionKind === "attachment") {
|
|
1221
|
+
const name = disposition?.params.get("filename") ?? contentType?.params.get("name") ?? "attachment";
|
|
1222
|
+
const bytes = decodeMIMEEncoding(p.body, encoding);
|
|
1223
|
+
attachments.push({
|
|
1224
|
+
name: decodeMIMEWords(name),
|
|
1225
|
+
mimeType: primaryType,
|
|
1226
|
+
size: bytes.length,
|
|
1227
|
+
bytes
|
|
1228
|
+
});
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
if (primaryType === "text/plain") {
|
|
1232
|
+
collectedText.push(decodeMIMEText(p.body, encoding, charset));
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
if (primaryType === "text/html") {
|
|
1236
|
+
collectedHTML.push(decodeMIMEText(p.body, encoding, charset));
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
const inlineName = contentType?.params.get("name") ?? disposition?.params.get("filename");
|
|
1240
|
+
if (inlineName !== void 0) {
|
|
1241
|
+
const bytes = decodeMIMEEncoding(p.body, encoding);
|
|
1242
|
+
attachments.push({
|
|
1243
|
+
name: decodeMIMEWords(inlineName),
|
|
1244
|
+
mimeType: primaryType,
|
|
1245
|
+
size: bytes.length,
|
|
1246
|
+
bytes
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
};
|
|
1250
|
+
walk(part);
|
|
1251
|
+
return {
|
|
1252
|
+
from: headerValue("from"),
|
|
1253
|
+
to: splitAddresses(headerValue("to")),
|
|
1254
|
+
cc: splitAddresses(headerValue("cc")),
|
|
1255
|
+
subject: headerValue("subject"),
|
|
1256
|
+
date,
|
|
1257
|
+
text: collectedText.join(""),
|
|
1258
|
+
html: collectedHTML.join(""),
|
|
1259
|
+
attachments
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* Infers the file extension for an attachment based on its filename or MIME type.
|
|
1264
|
+
* Returns the extension including the dot (e.g., '.jpg').
|
|
1265
|
+
*
|
|
1266
|
+
* @param mimeType - MIME type to infer from
|
|
1267
|
+
* @param fileName - File name to infer from
|
|
1268
|
+
* @returns Inferred extension, or `.bin` when neither hint resolves
|
|
1269
|
+
*/
|
|
1270
|
+
function inferExtension(mimeType, fileName) {
|
|
1271
|
+
if (fileName !== void 0) {
|
|
1272
|
+
const lastDotIndex = fileName.lastIndexOf(".");
|
|
1273
|
+
if (lastDotIndex !== -1 && lastDotIndex < fileName.length - 1) {
|
|
1274
|
+
const ext = fileName.slice(lastDotIndex).toLowerCase();
|
|
1275
|
+
if (/^\.[a-z0-9]{1,10}$/.test(ext)) return ext;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
if (mimeType !== void 0) {
|
|
1279
|
+
const normalized = mimeType.split(";")[0]?.trim().toLowerCase();
|
|
1280
|
+
if (normalized !== void 0) {
|
|
1281
|
+
const ext = MIME_EXTENSIONS.get(normalized);
|
|
1282
|
+
if (ext !== void 0) return ext;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
return ".bin";
|
|
1286
|
+
}
|
|
1287
|
+
//#endregion
|
|
1288
|
+
//#region src/core/MSGBurner.ts
|
|
1289
|
+
/**
|
|
1290
|
+
* Reconstitutes a valid CFB (Compound Binary File) from a flat list of
|
|
1291
|
+
* {@link MSGBurnerEntry} descriptors — root storage at index 0, its
|
|
1292
|
+
* children reachable through `children` indices.
|
|
1293
|
+
*
|
|
1294
|
+
* @remarks
|
|
1295
|
+
* Builds a red-black directory tree, allocates FAT/mini-FAT/DIFAT
|
|
1296
|
+
* sectors, then writes the header, directory entries, and stream data
|
|
1297
|
+
* into a single binary. Used to extract embedded `.msg` attachments as
|
|
1298
|
+
* standalone CFB files.
|
|
1299
|
+
*/
|
|
1300
|
+
var MSGBurner = class {
|
|
1301
|
+
#liteEntries = [];
|
|
1302
|
+
#fat = [];
|
|
1303
|
+
#miniFat = [];
|
|
1304
|
+
/**
|
|
1305
|
+
* Burn a flat list of CFB entries into a valid compound binary file.
|
|
1306
|
+
*
|
|
1307
|
+
* @param entries - Flat entry list starting with Root Entry at index 0
|
|
1308
|
+
* @returns Complete CFB binary as Uint8Array
|
|
1309
|
+
* @throws {@link MSGError} with code `BURN` when an entry name exceeds
|
|
1310
|
+
* the {@link MSG_BURNER_NAME_MAX} UTF-16 code unit limit the CFB directory entry format allows
|
|
1311
|
+
*/
|
|
1312
|
+
burn(entries) {
|
|
1313
|
+
this.#liteEntries = entries.map((entry) => ({
|
|
1314
|
+
entry,
|
|
1315
|
+
left: -1,
|
|
1316
|
+
right: -1,
|
|
1317
|
+
child: -1,
|
|
1318
|
+
firstSector: 0,
|
|
1319
|
+
mini: entry.type === 2 && entry.length < 4096,
|
|
1320
|
+
red: false
|
|
1321
|
+
}));
|
|
1322
|
+
this.#fat = [];
|
|
1323
|
+
this.#miniFat = [];
|
|
1324
|
+
this.#buildTree(0);
|
|
1325
|
+
const dirSectorCount = sectorsNeeded(128 * this.#liteEntries.length, 512);
|
|
1326
|
+
const entriesFirstSector = this.#allocateFat(dirSectorCount);
|
|
1327
|
+
for (let i = 0; i < this.#liteEntries.length; i++) {
|
|
1328
|
+
const le = this.#liteEntries[i];
|
|
1329
|
+
if (le.entry.type === 2 && !le.mini) le.firstSector = le.entry.length === 0 ? -2 : this.#allocateFat(sectorsNeeded(le.entry.length, 512));
|
|
1330
|
+
}
|
|
1331
|
+
for (let i = 0; i < this.#liteEntries.length; i++) {
|
|
1332
|
+
const le = this.#liteEntries[i];
|
|
1333
|
+
if (le.entry.type === 2 && le.mini) le.firstSector = le.entry.length === 0 ? -2 : this.#allocateMiniFat(sectorsNeeded(le.entry.length, 64));
|
|
1334
|
+
}
|
|
1335
|
+
const numMiniFatSectors = sectorsNeeded(4 * this.#miniFat.length, 512);
|
|
1336
|
+
const firstMiniFatSector = numMiniFatSectors !== 0 ? this.#allocateFat(numMiniFatSectors) : -2;
|
|
1337
|
+
const bytesMiniFat = 64 * this.#miniFat.length;
|
|
1338
|
+
const firstMiniDataSector = bytesMiniFat > 0 ? this.#allocateFat(sectorsNeeded(bytesMiniFat, 512)) : -2;
|
|
1339
|
+
this.#liteEntries[0].firstSector = firstMiniDataSector === -2 ? -2 : firstMiniDataSector;
|
|
1340
|
+
const estimatedFatSectors = Math.max(1, sectorsNeeded(4 * (this.#fat.length + Math.ceil(this.#fat.length / 128) + 1), 512));
|
|
1341
|
+
const firstFatSector = this.#allocateFatAs(estimatedFatSectors, -3);
|
|
1342
|
+
const numFatSectors = this.#fat.length - firstFatSector;
|
|
1343
|
+
const numDifatSectors = numFatSectors > 109 ? sectorsNeeded(4 * Math.ceil((numFatSectors - 109) / 127 * 128), 512) : 0;
|
|
1344
|
+
const firstDifatSector = numDifatSectors !== 0 ? this.#allocateFatAs(numDifatSectors, -4) : -2;
|
|
1345
|
+
const totalSize = 512 * (1 + this.#fat.length);
|
|
1346
|
+
const buffer = new ArrayBuffer(totalSize);
|
|
1347
|
+
const view = new DataView(buffer);
|
|
1348
|
+
const bytes = new Uint8Array(buffer);
|
|
1349
|
+
while (this.#miniFat.length % 128 !== 0) this.#miniFat.push(-1);
|
|
1350
|
+
const difat1 = [];
|
|
1351
|
+
const difat2 = [];
|
|
1352
|
+
this.#buildDifat(difat1, difat2, numFatSectors, firstFatSector, firstDifatSector);
|
|
1353
|
+
this.#writeHeader(view, bytes, numFatSectors, entriesFirstSector, firstMiniFatSector, numMiniFatSectors, firstDifatSector, numDifatSectors, difat1);
|
|
1354
|
+
this.#writeDirectoryEntries(view, bytes, entriesFirstSector, bytesMiniFat);
|
|
1355
|
+
this.#writeLargeStreams(bytes);
|
|
1356
|
+
this.#writeMiniStreams(bytes, firstMiniDataSector);
|
|
1357
|
+
this.#writeMiniFat(view, firstMiniFatSector);
|
|
1358
|
+
this.#writeFat(view, firstFatSector);
|
|
1359
|
+
this.#writeDifat(view, difat2, firstDifatSector, numDifatSectors);
|
|
1360
|
+
return new Uint8Array(buffer);
|
|
1361
|
+
}
|
|
1362
|
+
#allocateFat(count) {
|
|
1363
|
+
const first = this.#fat.length;
|
|
1364
|
+
for (let i = 0; i < count; i++) {
|
|
1365
|
+
const next = i + 1 === count ? -2 : first + i + 1;
|
|
1366
|
+
this.#fat.push(next);
|
|
1367
|
+
}
|
|
1368
|
+
return first;
|
|
1369
|
+
}
|
|
1370
|
+
#allocateFatAs(count, value) {
|
|
1371
|
+
const first = this.#fat.length;
|
|
1372
|
+
for (let i = 0; i < count; i++) this.#fat.push(value);
|
|
1373
|
+
return first;
|
|
1374
|
+
}
|
|
1375
|
+
#allocateMiniFat(count) {
|
|
1376
|
+
const first = this.#miniFat.length;
|
|
1377
|
+
for (let i = 0; i < count; i++) {
|
|
1378
|
+
const next = i + 1 === count ? -2 : first + i + 1;
|
|
1379
|
+
this.#miniFat.push(next);
|
|
1380
|
+
}
|
|
1381
|
+
return first;
|
|
1382
|
+
}
|
|
1383
|
+
#buildTree(dirIndex) {
|
|
1384
|
+
const liteEntry = this.#liteEntries[dirIndex];
|
|
1385
|
+
const children = liteEntry.entry.children;
|
|
1386
|
+
if (children === void 0 || children.length === 0) return;
|
|
1387
|
+
const sorted = children.slice().sort((a, b) => compareCFBName(this.#liteEntries[a].entry.name, this.#liteEntries[b].entry.name));
|
|
1388
|
+
const mid = Math.floor(sorted.length / 2);
|
|
1389
|
+
const rootIndex = sorted[mid];
|
|
1390
|
+
const rootEntry = this.#liteEntries[rootIndex];
|
|
1391
|
+
rootEntry.red = false;
|
|
1392
|
+
rootEntry.left = this.#splitTree(sorted, 0, mid, true);
|
|
1393
|
+
rootEntry.right = this.#splitTree(sorted, mid + 1, sorted.length, true);
|
|
1394
|
+
liteEntry.child = rootIndex;
|
|
1395
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
1396
|
+
const idx = sorted[i];
|
|
1397
|
+
if (this.#liteEntries[idx].entry.type === 1) this.#buildTree(idx);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
#splitTree(sorted, start, end, red) {
|
|
1401
|
+
if (start >= end) return -1;
|
|
1402
|
+
const mid = Math.floor((start + end) / 2);
|
|
1403
|
+
const entryIndex = sorted[mid];
|
|
1404
|
+
const entry = this.#liteEntries[entryIndex];
|
|
1405
|
+
entry.red = red;
|
|
1406
|
+
entry.left = this.#splitTree(sorted, start, mid, !red);
|
|
1407
|
+
entry.right = this.#splitTree(sorted, mid + 1, end, !red);
|
|
1408
|
+
return entryIndex;
|
|
1409
|
+
}
|
|
1410
|
+
#buildDifat(difat1, difat2, numFatSectors, firstFatSector, firstDifatSector) {
|
|
1411
|
+
let x = 0;
|
|
1412
|
+
for (; x < 109 && x < numFatSectors; x++) difat1.push(firstFatSector + x);
|
|
1413
|
+
let nextDifatSector = firstDifatSector + 1;
|
|
1414
|
+
for (; x < numFatSectors; x++) {
|
|
1415
|
+
difat2.push(firstFatSector + x);
|
|
1416
|
+
if ((difat2.length & 127) === 127) {
|
|
1417
|
+
difat2.push(nextDifatSector);
|
|
1418
|
+
nextDifatSector++;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
while (difat2.length > 0 && (difat2.length & 127) !== 0) {
|
|
1422
|
+
const remain = difat2.length & 127;
|
|
1423
|
+
difat2.push(remain === 127 ? -2 : -1);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
#writeHeader(view, bytes, numFatSectors, entriesFirstSector, firstMiniFatSector, numMiniFatSectors, firstDifatSector, numDifatSectors, difat1) {
|
|
1427
|
+
bytes.set(MSG_FILE_HEADER, 0);
|
|
1428
|
+
view.setUint16(24, 62, true);
|
|
1429
|
+
view.setUint16(26, 3, true);
|
|
1430
|
+
view.setUint16(28, 65534, true);
|
|
1431
|
+
view.setUint16(30, 9, true);
|
|
1432
|
+
view.setUint16(32, 6, true);
|
|
1433
|
+
view.setInt32(44, numFatSectors, true);
|
|
1434
|
+
view.setInt32(48, entriesFirstSector, true);
|
|
1435
|
+
view.setInt32(56, MSG_BURNER_MINI_STREAM_CUTOFF, true);
|
|
1436
|
+
view.setInt32(60, firstMiniFatSector, true);
|
|
1437
|
+
view.setInt32(64, numMiniFatSectors, true);
|
|
1438
|
+
view.setInt32(68, firstDifatSector, true);
|
|
1439
|
+
view.setInt32(72, numDifatSectors, true);
|
|
1440
|
+
let offset = 76;
|
|
1441
|
+
for (let i = 0; i < difat1.length; i++) {
|
|
1442
|
+
view.setInt32(offset, difat1[i], true);
|
|
1443
|
+
offset += 4;
|
|
1444
|
+
}
|
|
1445
|
+
for (let i = difat1.length; i < 109; i++) {
|
|
1446
|
+
view.setInt32(offset, -1, true);
|
|
1447
|
+
offset += 4;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
#writeDirectoryEntries(view, bytes, entriesFirstSector, bytesMiniFat) {
|
|
1451
|
+
for (let x = 0; x < this.#liteEntries.length; x++) {
|
|
1452
|
+
const le = this.#liteEntries[x];
|
|
1453
|
+
const pos = 512 * (1 + entriesFirstSector) + 128 * x;
|
|
1454
|
+
const name = le.entry.name;
|
|
1455
|
+
if (name.length > 31) throw new MSGError("BURN", `directory entry name exceeds 31 characters`, { name });
|
|
1456
|
+
for (let i = 0; i < name.length; i++) view.setUint16(pos + i * 2, name.charCodeAt(i), true);
|
|
1457
|
+
view.setUint16(pos + name.length * 2, 0, true);
|
|
1458
|
+
view.setUint16(pos + 64, (name.length + 1) * 2, true);
|
|
1459
|
+
bytes[pos + 66] = le.entry.type;
|
|
1460
|
+
bytes[pos + 67] = le.red ? 0 : 1;
|
|
1461
|
+
view.setInt32(pos + 68, le.left, true);
|
|
1462
|
+
view.setInt32(pos + 72, le.right, true);
|
|
1463
|
+
view.setInt32(pos + 76, le.child, true);
|
|
1464
|
+
if (x === 0) bytes.set(MSG_BURNER_ROOT_CLSID, pos + 80);
|
|
1465
|
+
const length = x === 0 ? bytesMiniFat : le.entry.length;
|
|
1466
|
+
const firstSector = length !== 0 ? le.firstSector : le.entry.type === 1 ? 0 : -2;
|
|
1467
|
+
view.setInt32(pos + 116, firstSector, true);
|
|
1468
|
+
view.setInt32(pos + 120, length, true);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
#writeLargeStreams(bytes) {
|
|
1472
|
+
for (let i = 0; i < this.#liteEntries.length; i++) {
|
|
1473
|
+
const le = this.#liteEntries[i];
|
|
1474
|
+
if (le.entry.type === 2 && !le.mini && le.entry.binaryProvider !== void 0) {
|
|
1475
|
+
const data = le.entry.binaryProvider();
|
|
1476
|
+
bytes.set(data, 512 * (1 + le.firstSector));
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
#writeMiniStreams(bytes, firstMiniDataSector) {
|
|
1481
|
+
if (firstMiniDataSector === -2) return;
|
|
1482
|
+
for (let i = 0; i < this.#liteEntries.length; i++) {
|
|
1483
|
+
const le = this.#liteEntries[i];
|
|
1484
|
+
if (le.entry.type === 2 && le.mini && le.entry.binaryProvider !== void 0) {
|
|
1485
|
+
const data = le.entry.binaryProvider();
|
|
1486
|
+
bytes.set(data, 512 * (1 + firstMiniDataSector) + 64 * le.firstSector);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
#writeMiniFat(view, firstMiniFatSector) {
|
|
1491
|
+
if (firstMiniFatSector === -2) return;
|
|
1492
|
+
let offset = 512 * (1 + firstMiniFatSector);
|
|
1493
|
+
for (let i = 0; i < this.#miniFat.length; i++) {
|
|
1494
|
+
view.setInt32(offset, this.#miniFat[i], true);
|
|
1495
|
+
offset += 4;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
#writeFat(view, firstFatSector) {
|
|
1499
|
+
while (this.#fat.length % 128 !== 0) this.#fat.push(-1);
|
|
1500
|
+
let offset = 512 * (1 + firstFatSector);
|
|
1501
|
+
for (let i = 0; i < this.#fat.length; i++) {
|
|
1502
|
+
view.setInt32(offset, this.#fat[i], true);
|
|
1503
|
+
offset += 4;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
#writeDifat(view, difat2, firstDifatSector, numDifatSectors) {
|
|
1507
|
+
if (numDifatSectors < 1) return;
|
|
1508
|
+
let offset = 512 * (1 + firstDifatSector);
|
|
1509
|
+
for (let i = 0; i < difat2.length; i++) {
|
|
1510
|
+
view.setInt32(offset, difat2[i], true);
|
|
1511
|
+
offset += 4;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1515
|
+
//#endregion
|
|
1516
|
+
//#region src/core/MSGReader.ts
|
|
1517
|
+
/**
|
|
1518
|
+
* Parses Microsoft Outlook .msg files (CFB/OLE2 compound binary format).
|
|
1519
|
+
* Every parsing step treats the input as untrusted: sector and property
|
|
1520
|
+
* chains are cycle-guarded and length-capped, every raw byte range is
|
|
1521
|
+
* bounds-checked before a view is constructed over it, and every failure
|
|
1522
|
+
* surfaces as a typed {@link MSGError} rather than a raw `RangeError` or
|
|
1523
|
+
* `TypeError`.
|
|
1524
|
+
*/
|
|
1525
|
+
var MSGReader = class {
|
|
1526
|
+
#view;
|
|
1527
|
+
#byteLength;
|
|
1528
|
+
#totalSectors;
|
|
1529
|
+
#options;
|
|
1530
|
+
#bigBlockSize = 0;
|
|
1531
|
+
#bigBlockLength = 0;
|
|
1532
|
+
#xBlockLength = 0;
|
|
1533
|
+
#batCount = 0;
|
|
1534
|
+
#propertyStart = 0;
|
|
1535
|
+
#sbatStart = 0;
|
|
1536
|
+
#sbatCount = 0;
|
|
1537
|
+
#xbatStart = 0;
|
|
1538
|
+
#xbatCount = 0;
|
|
1539
|
+
#bat = [];
|
|
1540
|
+
#sbat = [];
|
|
1541
|
+
#properties = [];
|
|
1542
|
+
#bigBlockTable = [];
|
|
1543
|
+
#fields;
|
|
1544
|
+
#privatePidToKeyed = {};
|
|
1545
|
+
#innerMSGBurners = {};
|
|
1546
|
+
/**
|
|
1547
|
+
* Create a reader over a raw MSG file buffer.
|
|
1548
|
+
*
|
|
1549
|
+
* @param input - Raw MSG file bytes, as an `ArrayBuffer` or a `Uint8Array` view
|
|
1550
|
+
* @param options - Reader configuration
|
|
1551
|
+
*/
|
|
1552
|
+
constructor(input, options) {
|
|
1553
|
+
if (input instanceof Uint8Array) {
|
|
1554
|
+
this.#view = new DataView(input.buffer, input.byteOffset, input.byteLength);
|
|
1555
|
+
this.#byteLength = input.byteLength;
|
|
1556
|
+
} else {
|
|
1557
|
+
this.#view = new DataView(input);
|
|
1558
|
+
this.#byteLength = input.byteLength;
|
|
1559
|
+
}
|
|
1560
|
+
this.#options = options ?? {};
|
|
1561
|
+
this.#totalSectors = Math.ceil(this.#byteLength / 512);
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* Parse the MSG file and return extracted field data.
|
|
1565
|
+
*
|
|
1566
|
+
* @returns Root message field data with nested attachments and recipients
|
|
1567
|
+
* @throws {@link MSGError} with code `UNSUPPORTED`, `MALFORMED`, `CYCLE`,
|
|
1568
|
+
* or `RANGE` when the compound file cannot be parsed
|
|
1569
|
+
*/
|
|
1570
|
+
parse() {
|
|
1571
|
+
if (this.#fields !== void 0) return this.#fields;
|
|
1572
|
+
this.#parseHeader();
|
|
1573
|
+
this.#bat = this.#readBat();
|
|
1574
|
+
if (this.#xbatCount > 0) this.#readXbat();
|
|
1575
|
+
this.#sbat = this.#readSbat();
|
|
1576
|
+
this.#properties = this.#readProperties(this.#propertyStart);
|
|
1577
|
+
this.#bigBlockTable = this.#buildBigBlockTable();
|
|
1578
|
+
this.#privatePidToKeyed = {};
|
|
1579
|
+
this.#innerMSGBurners = {};
|
|
1580
|
+
const fields = this.#extractFields();
|
|
1581
|
+
this.#fields = fields;
|
|
1582
|
+
return fields;
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* Read attachment binary content by index.
|
|
1586
|
+
*
|
|
1587
|
+
* @param index - Zero-based index into the parsed attachment list
|
|
1588
|
+
* @returns File name and raw binary content
|
|
1589
|
+
* @throws {@link MSGError} with code `RANGE` when the index is out of bounds
|
|
1590
|
+
*/
|
|
1591
|
+
attachment(index) {
|
|
1592
|
+
const attachments = this.parse().attachments;
|
|
1593
|
+
if (attachments === void 0 || index < 0 || index >= attachments.length) throw new MSGError("RANGE", `Attachment index ${index} out of range`, { index });
|
|
1594
|
+
const attach = attachments[index];
|
|
1595
|
+
if (attach.innerMSGContent === true && typeof attach.folderId === "number") {
|
|
1596
|
+
const name = typeof attach.name === "string" ? attach.name : "embedded";
|
|
1597
|
+
const burner = this.#innerMSGBurners[attach.folderId];
|
|
1598
|
+
const content = burner !== void 0 ? burner() : /* @__PURE__ */ new Uint8Array(0);
|
|
1599
|
+
return {
|
|
1600
|
+
fileName: name + ".msg",
|
|
1601
|
+
content
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
if (typeof attach.dataId !== "number" || attach.dataId < 0 || attach.dataId >= this.#properties.length) throw new MSGError("RANGE", "Attachment has no valid data reference", { index });
|
|
1605
|
+
const entry = this.#properties[attach.dataId];
|
|
1606
|
+
const content = this.#readEntry(entry);
|
|
1607
|
+
return {
|
|
1608
|
+
fileName: typeof attach.fileName === "string" ? attach.fileName : typeof attach.fileNameShort === "string" ? attach.fileNameShort : typeof attach.name === "string" ? attach.name : "unknown",
|
|
1609
|
+
content
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
/**
|
|
1613
|
+
* Rebuild the parsed MSG as a standalone CFB/.msg binary.
|
|
1614
|
+
*
|
|
1615
|
+
* @returns Complete CFB byte stream
|
|
1616
|
+
* @throws {@link MSGError} with code `BURN` when the parsed structure
|
|
1617
|
+
* cannot be reconstituted
|
|
1618
|
+
*/
|
|
1619
|
+
burn() {
|
|
1620
|
+
const parsed = this.parse();
|
|
1621
|
+
if (parsed.kind !== "msg") throw new MSGError("BURN", "Unable to burn a non-message field data structure", { kind: parsed.kind });
|
|
1622
|
+
const root = this.#properties[0];
|
|
1623
|
+
if (root === void 0) throw new MSGError("BURN", "Unable to burn MSG file without a root entry");
|
|
1624
|
+
return this.#burnFolder(root, false, false);
|
|
1625
|
+
}
|
|
1626
|
+
#parseHeader() {
|
|
1627
|
+
if (!isMSGFile(this.#view)) throw new MSGError("UNSUPPORTED", "Input is not a recognized CFB/MSG file");
|
|
1628
|
+
if (this.#byteLength < 512) throw new MSGError("MALFORMED", "File is smaller than the minimum CFB header size", { byteLength: this.#byteLength });
|
|
1629
|
+
const v = this.#view;
|
|
1630
|
+
const sectorMark = v.getUint8(30);
|
|
1631
|
+
this.#bigBlockSize = sectorMark === 12 ? MSG_L_BIG_BLOCK_SIZE : 512;
|
|
1632
|
+
this.#bigBlockLength = this.#bigBlockSize / 4;
|
|
1633
|
+
this.#xBlockLength = this.#bigBlockLength - 1;
|
|
1634
|
+
const batCount = v.getInt32(44, true);
|
|
1635
|
+
const propertyStart = v.getInt32(48, true);
|
|
1636
|
+
const sbatStart = v.getInt32(60, true);
|
|
1637
|
+
const sbatCount = v.getInt32(64, true);
|
|
1638
|
+
const xbatStart = v.getInt32(68, true);
|
|
1639
|
+
const xbatCount = v.getInt32(72, true);
|
|
1640
|
+
this.#validateHeaderField("batCount", batCount);
|
|
1641
|
+
this.#validateHeaderField("sbatCount", sbatCount);
|
|
1642
|
+
this.#validateHeaderField("xbatCount", xbatCount);
|
|
1643
|
+
this.#validateHeaderField("propertyStart", propertyStart);
|
|
1644
|
+
this.#batCount = batCount;
|
|
1645
|
+
this.#propertyStart = propertyStart;
|
|
1646
|
+
this.#sbatStart = sbatStart;
|
|
1647
|
+
this.#sbatCount = sbatCount;
|
|
1648
|
+
this.#xbatStart = xbatStart;
|
|
1649
|
+
this.#xbatCount = xbatCount;
|
|
1650
|
+
}
|
|
1651
|
+
#validateHeaderField(name, value) {
|
|
1652
|
+
if (value < 0 || value > this.#totalSectors) throw new MSGError("MALFORMED", `CFB header field '${name}' is out of range`, {
|
|
1653
|
+
field: name,
|
|
1654
|
+
value
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
#assertBounds(start, length) {
|
|
1658
|
+
if (start < 0 || length < 0 || start + length > this.#byteLength) throw new MSGError("MALFORMED", "Computed byte range exceeds file bounds", {
|
|
1659
|
+
start,
|
|
1660
|
+
length,
|
|
1661
|
+
byteLength: this.#byteLength
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
#trackSector(visited, sector, label, limit, capacityLabel) {
|
|
1665
|
+
if (visited.has(sector)) throw new MSGError("CYCLE", `${label} chain revisits sector ${sector}`, { sector });
|
|
1666
|
+
if (visited.size >= limit) throw new MSGError("CYCLE", `${label} chain exceeds ${capacityLabel}`, { limit });
|
|
1667
|
+
visited.add(sector);
|
|
1668
|
+
}
|
|
1669
|
+
#batCountInHeader() {
|
|
1670
|
+
return Math.min(this.#batCount, 436 / 4);
|
|
1671
|
+
}
|
|
1672
|
+
#readBat() {
|
|
1673
|
+
const count = this.#batCountInHeader();
|
|
1674
|
+
const result = new Array(count);
|
|
1675
|
+
let offset = 76;
|
|
1676
|
+
for (let i = 0; i < count; i++) {
|
|
1677
|
+
result[i] = this.#view.getInt32(offset, true);
|
|
1678
|
+
offset += 4;
|
|
1679
|
+
}
|
|
1680
|
+
return result;
|
|
1681
|
+
}
|
|
1682
|
+
#blockOffset(sector) {
|
|
1683
|
+
if (sector < 0 || sector >= this.#totalSectors) throw new MSGError("MALFORMED", `Sector index out of range: ${sector}`, {
|
|
1684
|
+
sector,
|
|
1685
|
+
totalSectors: this.#totalSectors
|
|
1686
|
+
});
|
|
1687
|
+
return (sector + 1) * this.#bigBlockSize;
|
|
1688
|
+
}
|
|
1689
|
+
#blockValueAt(sector, index) {
|
|
1690
|
+
const offset = this.#blockOffset(sector) + 4 * index;
|
|
1691
|
+
this.#assertBounds(offset, 4);
|
|
1692
|
+
return this.#view.getInt32(offset, true);
|
|
1693
|
+
}
|
|
1694
|
+
#nextBlockInner(offset, table) {
|
|
1695
|
+
const block = Math.floor(offset / this.#bigBlockLength);
|
|
1696
|
+
const index = offset % this.#bigBlockLength;
|
|
1697
|
+
const sector = table[block];
|
|
1698
|
+
if (sector === void 0) return -2;
|
|
1699
|
+
return this.#blockValueAt(sector, index);
|
|
1700
|
+
}
|
|
1701
|
+
#nextBlock(offset) {
|
|
1702
|
+
return this.#nextBlockInner(offset, this.#bat);
|
|
1703
|
+
}
|
|
1704
|
+
#nextBlockSmall(offset) {
|
|
1705
|
+
return this.#nextBlockInner(offset, this.#sbat);
|
|
1706
|
+
}
|
|
1707
|
+
#readSbat() {
|
|
1708
|
+
const result = [];
|
|
1709
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1710
|
+
let startIndex = this.#sbatStart;
|
|
1711
|
+
for (let i = 0; i < this.#sbatCount && startIndex !== 0 && startIndex !== -2; i++) {
|
|
1712
|
+
this.#trackSector(visited, startIndex, "SBAT", this.#totalSectors, "total sector count");
|
|
1713
|
+
result.push(startIndex);
|
|
1714
|
+
startIndex = this.#nextBlock(startIndex);
|
|
1715
|
+
}
|
|
1716
|
+
return result;
|
|
1717
|
+
}
|
|
1718
|
+
#readXbat() {
|
|
1719
|
+
const headerBatCount = this.#batCountInHeader();
|
|
1720
|
+
let remaining = this.#batCount - headerBatCount;
|
|
1721
|
+
let nextSector = this.#xbatStart;
|
|
1722
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1723
|
+
for (let i = 0; i < this.#xbatCount; i++) {
|
|
1724
|
+
this.#trackSector(visited, nextSector, "XBAT", this.#totalSectors, "total sector count");
|
|
1725
|
+
const blockOffset = this.#blockOffset(nextSector);
|
|
1726
|
+
this.#assertBounds(blockOffset, this.#bigBlockSize);
|
|
1727
|
+
const toProcess = Math.min(remaining, this.#xBlockLength);
|
|
1728
|
+
for (let j = 0; j < toProcess; j++) {
|
|
1729
|
+
const sector = this.#view.getInt32(blockOffset + j * 4, true);
|
|
1730
|
+
if (sector === -1 || sector === -2) break;
|
|
1731
|
+
this.#bat.push(sector);
|
|
1732
|
+
}
|
|
1733
|
+
remaining -= toProcess;
|
|
1734
|
+
nextSector = this.#view.getInt32(blockOffset + this.#xBlockLength * 4, true);
|
|
1735
|
+
if (nextSector === -1 || nextSector === -2) break;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
#readEntryName(offset) {
|
|
1739
|
+
const nameBytes = this.#view.getUint16(offset + 64, true);
|
|
1740
|
+
if (nameBytes < 2) return "";
|
|
1741
|
+
const charCount = Math.min(nameBytes / 2 - 1, 31);
|
|
1742
|
+
return removeTrailingNull(readUTF16String(this.#view, offset, charCount));
|
|
1743
|
+
}
|
|
1744
|
+
#readDirectoryEntry(offset) {
|
|
1745
|
+
const v = this.#view;
|
|
1746
|
+
return {
|
|
1747
|
+
type: v.getUint8(offset + 66),
|
|
1748
|
+
name: this.#readEntryName(offset),
|
|
1749
|
+
previousProperty: v.getInt32(offset + 68, true),
|
|
1750
|
+
nextProperty: v.getInt32(offset + 72, true),
|
|
1751
|
+
childProperty: v.getInt32(offset + 76, true),
|
|
1752
|
+
startBlock: v.getInt32(offset + 116, true),
|
|
1753
|
+
sizeBlock: v.getInt32(offset + 120, true)
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
#readProperties(propertyStart) {
|
|
1757
|
+
const props = [];
|
|
1758
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1759
|
+
let currentSector = propertyStart;
|
|
1760
|
+
while (currentSector !== -2) {
|
|
1761
|
+
this.#trackSector(visited, currentSector, "Property", this.#totalSectors, "total sector count");
|
|
1762
|
+
const entryCount = this.#bigBlockSize / 128;
|
|
1763
|
+
let offset = this.#blockOffset(currentSector);
|
|
1764
|
+
for (let i = 0; i < entryCount; i++) {
|
|
1765
|
+
if (offset + 128 > this.#byteLength) break;
|
|
1766
|
+
const entryType = this.#view.getUint8(offset + 66);
|
|
1767
|
+
if (entryType === 5 || entryType === 1 || entryType === 2) props.push(this.#readDirectoryEntry(offset));
|
|
1768
|
+
else props.push({
|
|
1769
|
+
type: entryType,
|
|
1770
|
+
name: "",
|
|
1771
|
+
previousProperty: -1,
|
|
1772
|
+
nextProperty: -1,
|
|
1773
|
+
childProperty: -1,
|
|
1774
|
+
startBlock: 0,
|
|
1775
|
+
sizeBlock: 0
|
|
1776
|
+
});
|
|
1777
|
+
offset += 128;
|
|
1778
|
+
}
|
|
1779
|
+
currentSector = this.#nextBlock(currentSector);
|
|
1780
|
+
}
|
|
1781
|
+
if (props.length > 0) this.#buildHierarchy(props, 0, /* @__PURE__ */ new Set(), 0);
|
|
1782
|
+
return props;
|
|
1783
|
+
}
|
|
1784
|
+
#buildHierarchy(props, nodeIndex, visited, depth) {
|
|
1785
|
+
const node = props[nodeIndex];
|
|
1786
|
+
if (node === void 0 || node.childProperty === -1) return;
|
|
1787
|
+
if (depth > 64) throw new MSGError("CYCLE", "Directory hierarchy nesting exceeds maximum depth", {
|
|
1788
|
+
depth,
|
|
1789
|
+
max: 64
|
|
1790
|
+
});
|
|
1791
|
+
if (visited.has(nodeIndex)) throw new MSGError("CYCLE", `Directory hierarchy revisits property index ${nodeIndex}`, { index: nodeIndex });
|
|
1792
|
+
if (visited.size >= props.length) throw new MSGError("CYCLE", "Directory hierarchy traversal exceeds total property count", { limit: props.length });
|
|
1793
|
+
visited.add(nodeIndex);
|
|
1794
|
+
node.children = [];
|
|
1795
|
+
const siblingVisited = /* @__PURE__ */ new Set([node.childProperty]);
|
|
1796
|
+
const stack = [{
|
|
1797
|
+
mode: "walk",
|
|
1798
|
+
index: node.childProperty
|
|
1799
|
+
}];
|
|
1800
|
+
while (stack.length > 0) {
|
|
1801
|
+
const item = stack.pop();
|
|
1802
|
+
if (item === void 0) break;
|
|
1803
|
+
const current = props[item.index];
|
|
1804
|
+
if (current === void 0) continue;
|
|
1805
|
+
if (item.mode === "push") node.children.push(item.index);
|
|
1806
|
+
else {
|
|
1807
|
+
if (current.type === 1) this.#buildHierarchy(props, item.index, visited, depth + 1);
|
|
1808
|
+
if (current.nextProperty !== -1) {
|
|
1809
|
+
if (siblingVisited.has(current.nextProperty)) throw new MSGError("CYCLE", `Directory sibling chain revisits property index ${current.nextProperty}`, { index: current.nextProperty });
|
|
1810
|
+
siblingVisited.add(current.nextProperty);
|
|
1811
|
+
stack.push({
|
|
1812
|
+
mode: "walk",
|
|
1813
|
+
index: current.nextProperty
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
stack.push({
|
|
1817
|
+
mode: "push",
|
|
1818
|
+
index: item.index
|
|
1819
|
+
});
|
|
1820
|
+
if (current.previousProperty !== -1) {
|
|
1821
|
+
if (siblingVisited.has(current.previousProperty)) throw new MSGError("CYCLE", `Directory sibling chain revisits property index ${current.previousProperty}`, { index: current.previousProperty });
|
|
1822
|
+
siblingVisited.add(current.previousProperty);
|
|
1823
|
+
stack.push({
|
|
1824
|
+
mode: "walk",
|
|
1825
|
+
index: current.previousProperty
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
#buildBigBlockTable() {
|
|
1832
|
+
const rootProp = this.#properties[0];
|
|
1833
|
+
if (rootProp === void 0) return [];
|
|
1834
|
+
const table = [];
|
|
1835
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1836
|
+
let nextBlock = rootProp.startBlock;
|
|
1837
|
+
while (nextBlock !== -2) {
|
|
1838
|
+
this.#trackSector(visited, nextBlock, "Big block", this.#totalSectors, "total sector count");
|
|
1839
|
+
table.push(nextBlock);
|
|
1840
|
+
nextBlock = this.#nextBlock(nextBlock);
|
|
1841
|
+
}
|
|
1842
|
+
return table;
|
|
1843
|
+
}
|
|
1844
|
+
#readSmallBlockData(startBlock, blockSize, dest, destOffset) {
|
|
1845
|
+
const byteOffset = startBlock * 64;
|
|
1846
|
+
const bigBlockNumber = Math.floor(byteOffset / this.#bigBlockSize);
|
|
1847
|
+
const bigBlockOffset = byteOffset % this.#bigBlockSize;
|
|
1848
|
+
const sector = this.#bigBlockTable[bigBlockNumber];
|
|
1849
|
+
if (sector === void 0) return;
|
|
1850
|
+
const start = this.#blockOffset(sector) + bigBlockOffset;
|
|
1851
|
+
this.#assertBounds(start, blockSize);
|
|
1852
|
+
const src = new Uint8Array(this.#view.buffer, this.#view.byteOffset + start, blockSize);
|
|
1853
|
+
dest.set(src, destOffset);
|
|
1854
|
+
}
|
|
1855
|
+
#readSmallChainData(entry, chain) {
|
|
1856
|
+
const result = new Uint8Array(entry.sizeBlock);
|
|
1857
|
+
let idx = 0;
|
|
1858
|
+
for (let i = 0; i < chain.length; i++) {
|
|
1859
|
+
const readLen = Math.min(result.length - idx, 64);
|
|
1860
|
+
this.#readSmallBlockData(chain[i], readLen, result, idx);
|
|
1861
|
+
idx += readLen;
|
|
1862
|
+
}
|
|
1863
|
+
return result;
|
|
1864
|
+
}
|
|
1865
|
+
#smallBlockChain(entry) {
|
|
1866
|
+
const chain = [];
|
|
1867
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1868
|
+
const capacity = this.#sbat.length * this.#bigBlockLength;
|
|
1869
|
+
let next = entry.startBlock;
|
|
1870
|
+
while (next !== -2) {
|
|
1871
|
+
if (next < 0 || next >= capacity) throw new MSGError("MALFORMED", `Small block index ${next} exceeds mini-FAT capacity`, {
|
|
1872
|
+
sector: next,
|
|
1873
|
+
capacity
|
|
1874
|
+
});
|
|
1875
|
+
this.#trackSector(visited, next, "Small block", capacity, "mini-FAT capacity");
|
|
1876
|
+
chain.push(next);
|
|
1877
|
+
next = this.#nextBlockSmall(next);
|
|
1878
|
+
}
|
|
1879
|
+
return chain;
|
|
1880
|
+
}
|
|
1881
|
+
#readEntry(entry) {
|
|
1882
|
+
if (entry.sizeBlock <= 0) return /* @__PURE__ */ new Uint8Array(0);
|
|
1883
|
+
if (entry.sizeBlock > this.#byteLength) throw new MSGError("MALFORMED", "Directory entry size exceeds file bounds", {
|
|
1884
|
+
sizeBlock: entry.sizeBlock,
|
|
1885
|
+
byteLength: this.#byteLength
|
|
1886
|
+
});
|
|
1887
|
+
if (entry.sizeBlock < 4096) {
|
|
1888
|
+
const chain = this.#smallBlockChain(entry);
|
|
1889
|
+
if (chain.length === 1) {
|
|
1890
|
+
const result = new Uint8Array(entry.sizeBlock);
|
|
1891
|
+
this.#readSmallBlockData(entry.startBlock, entry.sizeBlock, result, 0);
|
|
1892
|
+
return result;
|
|
1893
|
+
} else if (chain.length > 1) return this.#readSmallChainData(entry, chain);
|
|
1894
|
+
return /* @__PURE__ */ new Uint8Array(0);
|
|
1895
|
+
}
|
|
1896
|
+
let nextBlock = entry.startBlock;
|
|
1897
|
+
let remaining = entry.sizeBlock;
|
|
1898
|
+
let position = 0;
|
|
1899
|
+
const result = new Uint8Array(entry.sizeBlock);
|
|
1900
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1901
|
+
while (remaining >= 1) {
|
|
1902
|
+
this.#trackSector(visited, nextBlock, "Entry block", this.#totalSectors, "total sector count");
|
|
1903
|
+
const start = this.#blockOffset(nextBlock);
|
|
1904
|
+
const partSize = Math.min(remaining, this.#bigBlockSize);
|
|
1905
|
+
this.#assertBounds(start, partSize);
|
|
1906
|
+
const src = new Uint8Array(this.#view.buffer, this.#view.byteOffset + start, partSize);
|
|
1907
|
+
result.set(src, position);
|
|
1908
|
+
position += partSize;
|
|
1909
|
+
remaining -= partSize;
|
|
1910
|
+
nextBlock = this.#nextBlock(nextBlock);
|
|
1911
|
+
}
|
|
1912
|
+
return result;
|
|
1913
|
+
}
|
|
1914
|
+
#extractFields() {
|
|
1915
|
+
const root = this.#properties[0];
|
|
1916
|
+
if (root === void 0) throw new MSGError("MALFORMED", "MSG file has no root directory entry");
|
|
1917
|
+
const fields = {
|
|
1918
|
+
kind: "msg",
|
|
1919
|
+
attachments: [],
|
|
1920
|
+
recipients: []
|
|
1921
|
+
};
|
|
1922
|
+
this.#processDirectory(root, fields, "root");
|
|
1923
|
+
return this.#toFieldData(fields);
|
|
1924
|
+
}
|
|
1925
|
+
#str(mutable, key) {
|
|
1926
|
+
const value = mutable[key];
|
|
1927
|
+
return typeof value === "string" ? value : void 0;
|
|
1928
|
+
}
|
|
1929
|
+
#num(mutable, key) {
|
|
1930
|
+
const value = mutable[key];
|
|
1931
|
+
return typeof value === "number" ? value : void 0;
|
|
1932
|
+
}
|
|
1933
|
+
#bool(mutable, key) {
|
|
1934
|
+
const value = mutable[key];
|
|
1935
|
+
return typeof value === "boolean" ? value : void 0;
|
|
1936
|
+
}
|
|
1937
|
+
#bin(mutable, key) {
|
|
1938
|
+
const value = mutable[key];
|
|
1939
|
+
return value instanceof Uint8Array ? value : void 0;
|
|
1940
|
+
}
|
|
1941
|
+
#role(mutable, key) {
|
|
1942
|
+
const value = mutable[key];
|
|
1943
|
+
return value === "to" || value === "cc" || value === "bcc" ? value : void 0;
|
|
1944
|
+
}
|
|
1945
|
+
#toFieldData(mutable) {
|
|
1946
|
+
const attachmentsSource = mutable.attachments;
|
|
1947
|
+
const attachments = attachmentsSource === void 0 ? void 0 : attachmentsSource.map((entry) => this.#toFieldData(entry));
|
|
1948
|
+
const recipientsSource = mutable.recipients;
|
|
1949
|
+
const recipients = recipientsSource === void 0 ? void 0 : recipientsSource.map((entry) => this.#toFieldData(entry));
|
|
1950
|
+
const innerSource = mutable.innerMSGContentFields;
|
|
1951
|
+
const innerMSGContentFields = innerSource === void 0 ? void 0 : this.#toFieldData(innerSource);
|
|
1952
|
+
return {
|
|
1953
|
+
kind: mutable.kind,
|
|
1954
|
+
subject: this.#str(mutable, "subject"),
|
|
1955
|
+
senderName: this.#str(mutable, "senderName"),
|
|
1956
|
+
senderEmail: this.#str(mutable, "senderEmail"),
|
|
1957
|
+
senderAddressType: this.#str(mutable, "senderAddressType"),
|
|
1958
|
+
senderSMTPAddress: this.#str(mutable, "senderSMTPAddress"),
|
|
1959
|
+
sentRepresentingSMTPAddress: this.#str(mutable, "sentRepresentingSMTPAddress"),
|
|
1960
|
+
body: this.#str(mutable, "body"),
|
|
1961
|
+
headers: this.#str(mutable, "headers"),
|
|
1962
|
+
bodyHTML: this.#str(mutable, "bodyHTML"),
|
|
1963
|
+
html: this.#bin(mutable, "html"),
|
|
1964
|
+
compressedRTF: this.#bin(mutable, "compressedRTF"),
|
|
1965
|
+
messageClass: this.#str(mutable, "messageClass"),
|
|
1966
|
+
messageFlags: this.#num(mutable, "messageFlags"),
|
|
1967
|
+
messageId: this.#str(mutable, "messageId"),
|
|
1968
|
+
internetCodepage: this.#num(mutable, "internetCodepage"),
|
|
1969
|
+
messageCodepage: this.#num(mutable, "messageCodepage"),
|
|
1970
|
+
messageLocaleId: this.#num(mutable, "messageLocaleId"),
|
|
1971
|
+
clientSubmitTime: this.#str(mutable, "clientSubmitTime"),
|
|
1972
|
+
messageDeliveryTime: this.#str(mutable, "messageDeliveryTime"),
|
|
1973
|
+
creationTime: this.#str(mutable, "creationTime"),
|
|
1974
|
+
lastModificationTime: this.#str(mutable, "lastModificationTime"),
|
|
1975
|
+
lastModifierName: this.#str(mutable, "lastModifierName"),
|
|
1976
|
+
creatorSMTPAddress: this.#str(mutable, "creatorSMTPAddress"),
|
|
1977
|
+
lastModifierSMTPAddress: this.#str(mutable, "lastModifierSMTPAddress"),
|
|
1978
|
+
preview: this.#str(mutable, "preview"),
|
|
1979
|
+
conversationTopic: this.#str(mutable, "conversationTopic"),
|
|
1980
|
+
normalizedSubject: this.#str(mutable, "normalizedSubject"),
|
|
1981
|
+
name: this.#str(mutable, "name"),
|
|
1982
|
+
email: this.#str(mutable, "email"),
|
|
1983
|
+
addressType: this.#str(mutable, "addressType"),
|
|
1984
|
+
smtpAddress: this.#str(mutable, "smtpAddress"),
|
|
1985
|
+
recipientRole: this.#role(mutable, "recipientRole"),
|
|
1986
|
+
extension: this.#str(mutable, "extension"),
|
|
1987
|
+
fileNameShort: this.#str(mutable, "fileNameShort"),
|
|
1988
|
+
fileName: this.#str(mutable, "fileName"),
|
|
1989
|
+
contentId: this.#str(mutable, "contentId"),
|
|
1990
|
+
attachmentHidden: this.#bool(mutable, "attachmentHidden"),
|
|
1991
|
+
mimeType: this.#str(mutable, "mimeType"),
|
|
1992
|
+
contentLength: mutable.contentLength,
|
|
1993
|
+
dataId: mutable.dataId,
|
|
1994
|
+
folderId: mutable.folderId,
|
|
1995
|
+
innerMSGContent: mutable.innerMSGContent,
|
|
1996
|
+
innerMSGContentFields,
|
|
1997
|
+
attachments,
|
|
1998
|
+
recipients,
|
|
1999
|
+
departmentName: this.#str(mutable, "departmentName"),
|
|
2000
|
+
middleName: this.#str(mutable, "middleName"),
|
|
2001
|
+
generation: this.#str(mutable, "generation"),
|
|
2002
|
+
surname: this.#str(mutable, "surname"),
|
|
2003
|
+
givenName: this.#str(mutable, "givenName"),
|
|
2004
|
+
companyName: this.#str(mutable, "companyName"),
|
|
2005
|
+
jobTitle: this.#str(mutable, "jobTitle"),
|
|
2006
|
+
location: this.#str(mutable, "location"),
|
|
2007
|
+
postalAddress: this.#str(mutable, "postalAddress"),
|
|
2008
|
+
streetAddress: this.#str(mutable, "streetAddress"),
|
|
2009
|
+
postalCode: this.#str(mutable, "postalCode"),
|
|
2010
|
+
country: this.#str(mutable, "country"),
|
|
2011
|
+
stateOrProvince: this.#str(mutable, "stateOrProvince"),
|
|
2012
|
+
homePhone: this.#str(mutable, "homePhone"),
|
|
2013
|
+
mobilePhone: this.#str(mutable, "mobilePhone"),
|
|
2014
|
+
businessPhone: this.#str(mutable, "businessPhone"),
|
|
2015
|
+
businessFax: this.#str(mutable, "businessFax"),
|
|
2016
|
+
businessHomePage: this.#str(mutable, "businessHomePage"),
|
|
2017
|
+
namePrefix: this.#str(mutable, "namePrefix"),
|
|
2018
|
+
homeAddressCity: this.#str(mutable, "homeAddressCity"),
|
|
2019
|
+
appointmentStart: this.#str(mutable, "appointmentStart"),
|
|
2020
|
+
appointmentEnd: this.#str(mutable, "appointmentEnd"),
|
|
2021
|
+
clipStart: this.#str(mutable, "clipStart"),
|
|
2022
|
+
clipEnd: this.#str(mutable, "clipEnd"),
|
|
2023
|
+
timeZoneDescription: this.#str(mutable, "timeZoneDescription"),
|
|
2024
|
+
appointmentLocation: this.#str(mutable, "appointmentLocation"),
|
|
2025
|
+
appointmentOldLocation: this.#str(mutable, "appointmentOldLocation"),
|
|
2026
|
+
globalAppointmentId: this.#str(mutable, "globalAppointmentId"),
|
|
2027
|
+
votingResponse: this.#str(mutable, "votingResponse"),
|
|
2028
|
+
internetAccountName: this.#str(mutable, "internetAccountName"),
|
|
2029
|
+
yomiFirstName: this.#str(mutable, "yomiFirstName"),
|
|
2030
|
+
yomiLastName: this.#str(mutable, "yomiLastName"),
|
|
2031
|
+
yomiCompanyName: this.#str(mutable, "yomiCompanyName"),
|
|
2032
|
+
primaryEmailAddress: this.#str(mutable, "primaryEmailAddress"),
|
|
2033
|
+
primaryEmailDisplayName: this.#str(mutable, "primaryEmailDisplayName"),
|
|
2034
|
+
primaryEmailOriginalDisplayName: this.#str(mutable, "primaryEmailOriginalDisplayName"),
|
|
2035
|
+
fileUnder: this.#str(mutable, "fileUnder"),
|
|
2036
|
+
workAddressCity: this.#str(mutable, "workAddressCity"),
|
|
2037
|
+
workAddressStreet: this.#str(mutable, "workAddressStreet"),
|
|
2038
|
+
workAddressState: this.#str(mutable, "workAddressState"),
|
|
2039
|
+
workAddressPostalCode: this.#str(mutable, "workAddressPostalCode"),
|
|
2040
|
+
workAddressCountry: this.#str(mutable, "workAddressCountry"),
|
|
2041
|
+
workAddressCountryCode: this.#str(mutable, "workAddressCountryCode"),
|
|
2042
|
+
addressCountryCode: this.#str(mutable, "addressCountryCode"),
|
|
2043
|
+
contactWebPage: this.#str(mutable, "contactWebPage"),
|
|
2044
|
+
workAddress: this.#str(mutable, "workAddress"),
|
|
2045
|
+
instantMessagingAddress: this.#str(mutable, "instantMessagingAddress"),
|
|
2046
|
+
fax1AddressType: this.#str(mutable, "fax1AddressType"),
|
|
2047
|
+
fax1EmailAddress: this.#str(mutable, "fax1EmailAddress"),
|
|
2048
|
+
fax1OriginalDisplayName: this.#str(mutable, "fax1OriginalDisplayName"),
|
|
2049
|
+
fax2AddressType: this.#str(mutable, "fax2AddressType"),
|
|
2050
|
+
fax2EmailAddress: this.#str(mutable, "fax2EmailAddress"),
|
|
2051
|
+
fax2OriginalDisplayName: this.#str(mutable, "fax2OriginalDisplayName"),
|
|
2052
|
+
fax3AddressType: this.#str(mutable, "fax3AddressType"),
|
|
2053
|
+
fax3EmailAddress: this.#str(mutable, "fax3EmailAddress"),
|
|
2054
|
+
fax3OriginalDisplayName: this.#str(mutable, "fax3OriginalDisplayName")
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
#processDirectory(entry, fields, subClass) {
|
|
2058
|
+
const children = entry.children;
|
|
2059
|
+
if (children === void 0) return;
|
|
2060
|
+
for (let i = 0; i < children.length; i++) {
|
|
2061
|
+
const childIndex = children[i];
|
|
2062
|
+
const child = this.#properties[childIndex];
|
|
2063
|
+
if (child === void 0) continue;
|
|
2064
|
+
if (child.type === 1) this.#processSubDirectory(child, childIndex, fields);
|
|
2065
|
+
}
|
|
2066
|
+
for (let i = 0; i < children.length; i++) {
|
|
2067
|
+
const childIndex = children[i];
|
|
2068
|
+
const child = this.#properties[childIndex];
|
|
2069
|
+
if (child === void 0) continue;
|
|
2070
|
+
if (child.type === 2) {
|
|
2071
|
+
if (child.name.indexOf("__substg1.") === 0) this.#processDocument(child, childIndex, fields);
|
|
2072
|
+
else if (child.name === "__properties_version1.0") {
|
|
2073
|
+
if (subClass === "recip" || subClass === "attachment" || subClass === "sub") this.#processPropertyStream(child, 8, fields);
|
|
2074
|
+
else if (subClass === "root") this.#processPropertyStream(child, 32, fields);
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
#processSubDirectory(child, childIndex, fields) {
|
|
2080
|
+
if (child.name.indexOf("__attach_version1.0") === 0) {
|
|
2081
|
+
const attachmentField = { kind: "attachment" };
|
|
2082
|
+
if (fields.attachments === void 0) fields.attachments = [];
|
|
2083
|
+
fields.attachments.push(attachmentField);
|
|
2084
|
+
this.#processDirectory(child, attachmentField, "attachment");
|
|
2085
|
+
} else if (child.name.indexOf("__recip_version1.0") === 0) {
|
|
2086
|
+
const recipientField = { kind: "recipient" };
|
|
2087
|
+
if (fields.recipients === void 0) fields.recipients = [];
|
|
2088
|
+
fields.recipients.push(recipientField);
|
|
2089
|
+
this.#processDirectory(child, recipientField, "recip");
|
|
2090
|
+
} else if (child.name.indexOf("__nameid_version1.0") === 0) this.#processNameIdDirectory(child);
|
|
2091
|
+
else if (this.#getDirectoryFieldType(child) === "000d") {
|
|
2092
|
+
const innerFields = {
|
|
2093
|
+
kind: "msg",
|
|
2094
|
+
attachments: [],
|
|
2095
|
+
recipients: []
|
|
2096
|
+
};
|
|
2097
|
+
this.#processDirectory(child, innerFields, "sub");
|
|
2098
|
+
fields.innerMSGContentFields = innerFields;
|
|
2099
|
+
fields.innerMSGContent = true;
|
|
2100
|
+
fields.folderId = childIndex;
|
|
2101
|
+
this.#innerMSGBurners[childIndex] = () => this.#burnFolder(child, true, true);
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
#getDirectoryFieldType(entry) {
|
|
2105
|
+
return entry.name.substring(12).toLowerCase().substring(4, 8);
|
|
2106
|
+
}
|
|
2107
|
+
#processDocument(entry, entryIndex, fields) {
|
|
2108
|
+
const value = entry.name.substring(12).toLowerCase();
|
|
2109
|
+
const fieldClass = value.substring(0, 4);
|
|
2110
|
+
const fieldType = value.substring(4, 8);
|
|
2111
|
+
if (fieldClass === "3701") {
|
|
2112
|
+
fields.dataId = entryIndex;
|
|
2113
|
+
fields.contentLength = entry.sizeBlock;
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
const data = this.#readEntry(entry);
|
|
2117
|
+
this.#decodeAndAssign(fieldClass, fieldType, data, fields, false);
|
|
2118
|
+
}
|
|
2119
|
+
#processPropertyStream(entry, headerSize, fields) {
|
|
2120
|
+
const data = this.#readEntry(entry);
|
|
2121
|
+
if (data.length <= headerSize) return;
|
|
2122
|
+
const propView = new DataView(data.buffer, data.byteOffset + headerSize, data.length - headerSize);
|
|
2123
|
+
let offset = 0;
|
|
2124
|
+
while (offset + 16 <= propView.byteLength) {
|
|
2125
|
+
const propertyTag = propView.getUint32(offset, true);
|
|
2126
|
+
if (propertyTag === 0) break;
|
|
2127
|
+
offset += 8;
|
|
2128
|
+
const valueBytes = new Uint8Array(data.buffer, data.byteOffset + headerSize + offset, 8);
|
|
2129
|
+
offset += 8;
|
|
2130
|
+
const fieldClass = toHexLower(propertyTag >>> 16 & 65535, 4);
|
|
2131
|
+
const fieldType = toHexLower(propertyTag & 65535, 4);
|
|
2132
|
+
this.#decodeAndAssign(fieldClass, fieldType, valueBytes, fields, true);
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
#decodeAndAssign(fieldClass, fieldType, data, fields, insideProps) {
|
|
2136
|
+
let key = MSG_FIELD_FULL_NAME_MAPPING[`${fieldClass}${fieldType}`] ?? MSG_FIELD_NAME_MAPPING[fieldClass];
|
|
2137
|
+
const classValue = parseInt(fieldClass, 16);
|
|
2138
|
+
if (!Number.isNaN(classValue) && classValue >= 32768) {
|
|
2139
|
+
const keyed = this.#privatePidToKeyed[classValue];
|
|
2140
|
+
if (keyed !== void 0) {
|
|
2141
|
+
if (keyed.useName) key = keyed.name;
|
|
2142
|
+
else if (keyed.propertySet !== void 0 && keyed.propertyLid !== void 0) {
|
|
2143
|
+
const resolved = this.#resolvePidLid(keyed.propertySet, keyed.propertyLid);
|
|
2144
|
+
if (resolved !== void 0) key = resolved;
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
const decodeAs = MSG_FIELD_TYPE_MAPPING[fieldType];
|
|
2149
|
+
let value = data;
|
|
2150
|
+
if (decodeAs === "string") {
|
|
2151
|
+
value = removeTrailingNull(readANSIString(data, this.#options.encoding));
|
|
2152
|
+
if (insideProps) key = void 0;
|
|
2153
|
+
} else if (decodeAs === "unicode") {
|
|
2154
|
+
value = removeTrailingNull(readUTF16String(new DataView(data.buffer, data.byteOffset, data.length), 0, Math.floor(data.length / 2)));
|
|
2155
|
+
if (insideProps) key = void 0;
|
|
2156
|
+
} else if (decodeAs === "binary") {
|
|
2157
|
+
if (insideProps) key = void 0;
|
|
2158
|
+
} else if (decodeAs === "integer") {
|
|
2159
|
+
if (data.length >= 4) value = new DataView(data.buffer, data.byteOffset, data.length).getUint32(0, true);
|
|
2160
|
+
} else if (decodeAs === "boolean") {
|
|
2161
|
+
if (data.length >= 2) value = new DataView(data.buffer, data.byteOffset, data.length).getUint16(0, true) !== 0;
|
|
2162
|
+
} else if (decodeAs === "time") {
|
|
2163
|
+
if (data.length >= 8) {
|
|
2164
|
+
const dv = new DataView(data.buffer, data.byteOffset, data.length);
|
|
2165
|
+
value = fileTimeToUTCString(dv.getUint32(0, true), dv.getUint32(4, true));
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
if (key === "recipientRole" && typeof value === "number") {
|
|
2169
|
+
if (value === 1) value = "to";
|
|
2170
|
+
else if (value === 2) value = "cc";
|
|
2171
|
+
else if (value === 3) value = "bcc";
|
|
2172
|
+
}
|
|
2173
|
+
if (key !== void 0) fields[key] = value;
|
|
2174
|
+
}
|
|
2175
|
+
#resolvePidLid(propertySet, propertyLid) {
|
|
2176
|
+
const setMapping = MSG_PIDLID_MAPPING[propertySet];
|
|
2177
|
+
if (setMapping === void 0) return void 0;
|
|
2178
|
+
return setMapping[propertyLid];
|
|
2179
|
+
}
|
|
2180
|
+
#processNameIdDirectory(dirEntry) {
|
|
2181
|
+
const children = dirEntry.children;
|
|
2182
|
+
if (children === void 0) return;
|
|
2183
|
+
let guidTable;
|
|
2184
|
+
let entryTable;
|
|
2185
|
+
let stringTable;
|
|
2186
|
+
for (let i = 0; i < children.length; i++) {
|
|
2187
|
+
const childIndex = children[i];
|
|
2188
|
+
const child = this.#properties[childIndex];
|
|
2189
|
+
if (child === void 0 || child.type !== 2) continue;
|
|
2190
|
+
if (child.name.indexOf("__substg1.") !== 0) continue;
|
|
2191
|
+
const value = child.name.substring(12).toLowerCase();
|
|
2192
|
+
const fieldClass = value.substring(0, 4);
|
|
2193
|
+
const fieldType = value.substring(4, 8);
|
|
2194
|
+
if (fieldClass === "0002" && fieldType === "0102") guidTable = this.#readEntry(child);
|
|
2195
|
+
else if (fieldClass === "0003" && fieldType === "0102") entryTable = this.#readEntry(child);
|
|
2196
|
+
else if (fieldClass === "0004" && fieldType === "0102") stringTable = this.#readEntry(child);
|
|
2197
|
+
}
|
|
2198
|
+
if (guidTable === void 0 || entryTable === void 0 || stringTable === void 0) return;
|
|
2199
|
+
this.#parseEntryStream(entryTable, guidTable, stringTable);
|
|
2200
|
+
}
|
|
2201
|
+
#parseEntryStream(entryTable, guidTable, stringTable) {
|
|
2202
|
+
const view = new DataView(entryTable.buffer, entryTable.byteOffset, entryTable.length);
|
|
2203
|
+
const entryCount = Math.floor(entryTable.length / 8);
|
|
2204
|
+
for (let i = 0; i < entryCount; i++) {
|
|
2205
|
+
const offset = i * 8;
|
|
2206
|
+
const nameIdOrStringOffset = view.getUint32(offset, true);
|
|
2207
|
+
const indexAndKind = view.getUint16(offset + 4, true);
|
|
2208
|
+
const propertyIndex = view.getUint16(offset + 6, true);
|
|
2209
|
+
const guidIndex = indexAndKind >>> 1 & 32767;
|
|
2210
|
+
if ((nameIdOrStringOffset & 1) !== 0) {
|
|
2211
|
+
const strView = new DataView(stringTable.buffer, stringTable.byteOffset, stringTable.length);
|
|
2212
|
+
const strOffset = nameIdOrStringOffset >>> 0;
|
|
2213
|
+
if (strOffset + 4 <= stringTable.length) {
|
|
2214
|
+
const numTextBytes = strView.getUint32(strOffset, true);
|
|
2215
|
+
const charCount = Math.floor(numTextBytes / 2);
|
|
2216
|
+
if (strOffset + 4 + numTextBytes <= stringTable.length) {
|
|
2217
|
+
const name = readUTF16String(strView, strOffset + 4, charCount);
|
|
2218
|
+
this.#privatePidToKeyed[32768 | propertyIndex] = {
|
|
2219
|
+
useName: true,
|
|
2220
|
+
name
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
} else {
|
|
2225
|
+
let propertySet;
|
|
2226
|
+
if (guidIndex === 1) propertySet = "00020328-0000-0000-c000-000000000046";
|
|
2227
|
+
else if (guidIndex === 2) propertySet = "00020329-0000-0000-c000-000000000046";
|
|
2228
|
+
else {
|
|
2229
|
+
const guidOffset = 16 * (guidIndex - 3);
|
|
2230
|
+
if (guidOffset >= 0 && guidOffset + 16 <= guidTable.length) propertySet = msftUUIDStringify(guidTable, guidOffset);
|
|
2231
|
+
}
|
|
2232
|
+
if (propertySet !== void 0) this.#privatePidToKeyed[32768 | propertyIndex] = {
|
|
2233
|
+
useName: false,
|
|
2234
|
+
propertySet,
|
|
2235
|
+
propertyLid: nameIdOrStringOffset
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
#burnFolder(folder, padTopLevelPropertyStream, includeRootNameId) {
|
|
2241
|
+
const entries = [{
|
|
2242
|
+
name: "Root Entry",
|
|
2243
|
+
type: 5,
|
|
2244
|
+
children: [],
|
|
2245
|
+
length: 0
|
|
2246
|
+
}];
|
|
2247
|
+
this.#registerBurnerFolder(entries, 0, folder, padTopLevelPropertyStream, includeRootNameId);
|
|
2248
|
+
return new MSGBurner().burn(entries);
|
|
2249
|
+
}
|
|
2250
|
+
#registerBurnerFolder(entries, parentIndex, folder, padPropertyStream, includeRootNameId) {
|
|
2251
|
+
const children = folder.children;
|
|
2252
|
+
if (children === void 0) return;
|
|
2253
|
+
const parentChildren = entries[parentIndex].children;
|
|
2254
|
+
if (parentChildren === void 0) return;
|
|
2255
|
+
for (let i = 0; i < children.length; i++) {
|
|
2256
|
+
const childIndex = children[i];
|
|
2257
|
+
const child = this.#properties[childIndex];
|
|
2258
|
+
if (child === void 0 || child.type !== 2) continue;
|
|
2259
|
+
let provider = () => this.#readEntry(child);
|
|
2260
|
+
let length = child.sizeBlock;
|
|
2261
|
+
if (padPropertyStream && child.name === "__properties_version1.0") {
|
|
2262
|
+
const originalProvider = provider;
|
|
2263
|
+
provider = () => {
|
|
2264
|
+
const src = originalProvider();
|
|
2265
|
+
const dst = new Uint8Array(src.length + 8);
|
|
2266
|
+
dst.set(src.subarray(0, 24), 0);
|
|
2267
|
+
dst.set(src.subarray(24), 32);
|
|
2268
|
+
return dst;
|
|
2269
|
+
};
|
|
2270
|
+
length = length + 8;
|
|
2271
|
+
}
|
|
2272
|
+
const subIndex = entries.length;
|
|
2273
|
+
parentChildren.push(subIndex);
|
|
2274
|
+
entries.push({
|
|
2275
|
+
name: child.name,
|
|
2276
|
+
type: 2,
|
|
2277
|
+
binaryProvider: provider,
|
|
2278
|
+
length
|
|
2279
|
+
});
|
|
2280
|
+
}
|
|
2281
|
+
if (includeRootNameId) {
|
|
2282
|
+
const rootProp = this.#properties[0];
|
|
2283
|
+
if (rootProp !== void 0 && rootProp.children !== void 0) for (let i = 0; i < rootProp.children.length; i++) {
|
|
2284
|
+
const rootChildIndex = rootProp.children[i];
|
|
2285
|
+
const rootChild = this.#properties[rootChildIndex];
|
|
2286
|
+
if (rootChild !== void 0 && rootChild.type === 1 && rootChild.name === "__nameid_version1.0") {
|
|
2287
|
+
const subIndex = entries.length;
|
|
2288
|
+
parentChildren.push(subIndex);
|
|
2289
|
+
entries.push({
|
|
2290
|
+
name: rootChild.name,
|
|
2291
|
+
type: 1,
|
|
2292
|
+
children: [],
|
|
2293
|
+
length: 0
|
|
2294
|
+
});
|
|
2295
|
+
this.#registerBurnerFolder(entries, subIndex, rootChild, false, false);
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
for (let i = 0; i < children.length; i++) {
|
|
2300
|
+
const childIndex = children[i];
|
|
2301
|
+
const child = this.#properties[childIndex];
|
|
2302
|
+
if (child === void 0 || child.type !== 1) continue;
|
|
2303
|
+
const subIndex = entries.length;
|
|
2304
|
+
parentChildren.push(subIndex);
|
|
2305
|
+
entries.push({
|
|
2306
|
+
name: child.name,
|
|
2307
|
+
type: 1,
|
|
2308
|
+
children: [],
|
|
2309
|
+
length: 0
|
|
2310
|
+
});
|
|
2311
|
+
this.#registerBurnerFolder(entries, subIndex, child, false, false);
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
};
|
|
2315
|
+
//#endregion
|
|
2316
|
+
//#region src/core/EmailParser.ts
|
|
2317
|
+
/**
|
|
2318
|
+
* Parses raw .eml or .msg file bytes into a structured {@link EmailChain}.
|
|
2319
|
+
* Synchronous and dependency-free: format detection falls back to sniffing
|
|
2320
|
+
* the CFB magic header when neither `name` nor `mime` resolves it, and every
|
|
2321
|
+
* parse failure is contained into a `Result` rather than thrown.
|
|
2322
|
+
*
|
|
2323
|
+
* @example
|
|
2324
|
+
* ```ts
|
|
2325
|
+
* import { createEmailParser, isSuccess } from '@src/core'
|
|
2326
|
+
*
|
|
2327
|
+
* const parser = createEmailParser()
|
|
2328
|
+
* const result = parser.parse({ bytes, name: 'message.eml' })
|
|
2329
|
+
* if (isSuccess(result)) {
|
|
2330
|
+
* const { messages } = result.value
|
|
2331
|
+
* console.log(messages[0].text)
|
|
2332
|
+
* console.log(messages[0].attachments)
|
|
2333
|
+
* }
|
|
2334
|
+
* ```
|
|
2335
|
+
*/
|
|
2336
|
+
var EmailParser = class {
|
|
2337
|
+
#options;
|
|
2338
|
+
constructor(options = {}) {
|
|
2339
|
+
this.#options = { ...options };
|
|
2340
|
+
}
|
|
2341
|
+
/**
|
|
2342
|
+
* Parse raw email bytes into a structured chain.
|
|
2343
|
+
*
|
|
2344
|
+
* @param input - Raw bytes plus optional name/MIME hints
|
|
2345
|
+
* @returns A {@link Success} wrapping the parsed {@link EmailChain}, or a
|
|
2346
|
+
* {@link Failure} wrapping an {@link MSGError} (code `UNSUPPORTED` when
|
|
2347
|
+
* the format cannot be determined, `MALFORMED` when parsing fails)
|
|
2348
|
+
*
|
|
2349
|
+
* @example
|
|
2350
|
+
* ```ts
|
|
2351
|
+
* const result = parser.parse({ bytes, name: 'message.msg' })
|
|
2352
|
+
* ```
|
|
2353
|
+
*/
|
|
2354
|
+
parse(input) {
|
|
2355
|
+
try {
|
|
2356
|
+
const format = detectFormat(input.name, input.mime) ?? (isMSGFile(new DataView(input.bytes.buffer, input.bytes.byteOffset, input.bytes.byteLength)) ? "msg" : void 0);
|
|
2357
|
+
if (format === void 0) return failure(new MSGError("UNSUPPORTED", `"${input.name ?? "input"}" — only .eml and .msg files are supported`, {
|
|
2358
|
+
name: input.name,
|
|
2359
|
+
mime: input.mime
|
|
2360
|
+
}));
|
|
2361
|
+
if (format === "msg") return success({
|
|
2362
|
+
format,
|
|
2363
|
+
messages: [extractMessageFromMSG(new MSGReader(input.bytes, { encoding: this.#options.encoding }))]
|
|
2364
|
+
});
|
|
2365
|
+
return success({
|
|
2366
|
+
format,
|
|
2367
|
+
messages: [extractMessage(parseMIMEPart(decodeUTF8(input.bytes)))]
|
|
2368
|
+
});
|
|
2369
|
+
} catch (cause) {
|
|
2370
|
+
if (isMSGError(cause)) return failure(cause);
|
|
2371
|
+
if (cause instanceof Error) return failure(new MSGError("MALFORMED", cause.message));
|
|
2372
|
+
return failure(new MSGError("MALFORMED", "Failed to parse email input"));
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
/**
|
|
2376
|
+
* Current parser configuration.
|
|
2377
|
+
*
|
|
2378
|
+
* @returns A copy of the configured {@link EmailParserOptions} — the
|
|
2379
|
+
* internal reference is never leaked
|
|
2380
|
+
*/
|
|
2381
|
+
get options() {
|
|
2382
|
+
return { ...this.#options };
|
|
2383
|
+
}
|
|
2384
|
+
};
|
|
2385
|
+
//#endregion
|
|
2386
|
+
//#region src/core/factories.ts
|
|
2387
|
+
/**
|
|
2388
|
+
* Create a new .msg file reader.
|
|
2389
|
+
*
|
|
2390
|
+
* @param buffer - Raw .msg file bytes
|
|
2391
|
+
* @param options - Optional reader configuration
|
|
2392
|
+
* @returns A working {@link MSGReaderInterface}
|
|
2393
|
+
*
|
|
2394
|
+
* @example
|
|
2395
|
+
* ```ts
|
|
2396
|
+
* import { createMSGReader } from '@src/core'
|
|
2397
|
+
*
|
|
2398
|
+
* const reader = createMSGReader(buffer)
|
|
2399
|
+
* const data = reader.parse()
|
|
2400
|
+
* console.log(data.kind)
|
|
2401
|
+
* ```
|
|
2402
|
+
*/
|
|
2403
|
+
function createMSGReader(buffer, options) {
|
|
2404
|
+
return new MSGReader(buffer, options);
|
|
2405
|
+
}
|
|
2406
|
+
/**
|
|
2407
|
+
* Create a new CFB binary writer for reconstituting .msg files.
|
|
2408
|
+
*
|
|
2409
|
+
* @returns A working {@link MSGBurnerInterface}
|
|
2410
|
+
*
|
|
2411
|
+
* @example
|
|
2412
|
+
* ```ts
|
|
2413
|
+
* import { createMSGBurner } from '@src/core'
|
|
2414
|
+
*
|
|
2415
|
+
* const burner = createMSGBurner()
|
|
2416
|
+
* const binary = burner.burn(entries)
|
|
2417
|
+
* ```
|
|
2418
|
+
*/
|
|
2419
|
+
function createMSGBurner() {
|
|
2420
|
+
return new MSGBurner();
|
|
2421
|
+
}
|
|
2422
|
+
/**
|
|
2423
|
+
* Create a new email file parser for .eml and .msg input.
|
|
2424
|
+
*
|
|
2425
|
+
* @param options - Optional parser configuration
|
|
2426
|
+
* @returns A working {@link EmailParserInterface}
|
|
2427
|
+
*
|
|
2428
|
+
* @example
|
|
2429
|
+
* ```ts
|
|
2430
|
+
* import { createEmailParser, isSuccess } from '@src/core'
|
|
2431
|
+
*
|
|
2432
|
+
* const parser = createEmailParser()
|
|
2433
|
+
* const result = parser.parse({ bytes, name: 'message.eml' })
|
|
2434
|
+
* if (isSuccess(result)) {
|
|
2435
|
+
* console.log(result.value.messages[0].subject)
|
|
2436
|
+
* }
|
|
2437
|
+
* ```
|
|
2438
|
+
*/
|
|
2439
|
+
function createEmailParser(options) {
|
|
2440
|
+
return new EmailParser(options);
|
|
2441
|
+
}
|
|
2442
|
+
//#endregion
|
|
2443
|
+
export { EML_EXTENSIONS, EML_MIME_TYPES, EmailParser, FALLBACK_ATTACHMENT_NAME, FALLBACK_CHARSET, MIME_EXTENSIONS, MIME_MAX_DEPTH, MSGBurner, MSGError, MSGReader, MSG_BIG_BLOCK_MIN_DOC_SIZE, MSG_BURNER_DIFAT_HEADER_SLOTS, MSG_BURNER_DIFAT_SECTOR_MARKER, MSG_BURNER_DIR_ENTRY_SIZE, MSG_BURNER_FAT_SECTOR_MARKER, MSG_BURNER_INTS_PER_SECTOR, MSG_BURNER_MINI_SECTOR_SIZE, MSG_BURNER_MINI_STREAM_CUTOFF, MSG_BURNER_NAME_MAX, MSG_BURNER_ROOT_CLSID, MSG_BURNER_SECTOR_SIZE, MSG_END_OF_CHAIN, MSG_EXTENSIONS, MSG_FIELD_CLASS_ATTACHMENT_DATA, MSG_FIELD_DIR_TYPE_INNER_MSG, MSG_FIELD_FULL_NAME_MAPPING, MSG_FIELD_NAME_MAPPING, MSG_FIELD_TYPE_MAPPING, MSG_FILE_HEADER, MSG_HEADER_BAT_COUNT_OFFSET, MSG_HEADER_BAT_START_OFFSET, MSG_HEADER_PROPERTY_START_OFFSET, MSG_HEADER_SBAT_COUNT_OFFSET, MSG_HEADER_SBAT_START_OFFSET, MSG_HEADER_XBAT_COUNT_OFFSET, MSG_HEADER_XBAT_START_OFFSET, MSG_L_BIG_BLOCK_MARK, MSG_L_BIG_BLOCK_SIZE, MSG_MAPI_RECIPIENT_BCC, MSG_MAPI_RECIPIENT_CC, MSG_MAPI_RECIPIENT_TO, MSG_MAX_HIERARCHY_DEPTH, MSG_MIME_TYPES, MSG_PIDLID_MAPPING, MSG_PREFIX_ATTACHMENT, MSG_PREFIX_DOCUMENT, MSG_PREFIX_NAMEID, MSG_PREFIX_RECIPIENT, MSG_PROPERTY_SIZE, MSG_PROP_CHILD_PROPERTY_OFFSET, MSG_PROP_NAME_SIZE_OFFSET, MSG_PROP_NEXT_PROPERTY_OFFSET, MSG_PROP_NO_INDEX, MSG_PROP_PREVIOUS_PROPERTY_OFFSET, MSG_PROP_SIZE_OFFSET, MSG_PROP_START_BLOCK_OFFSET, MSG_PROP_TYPE_OFFSET, MSG_SMALL_BLOCK_SIZE, MSG_S_BIG_BLOCK_MARK, MSG_S_BIG_BLOCK_SIZE, MSG_TYPE_DIRECTORY, MSG_TYPE_DOCUMENT, MSG_TYPE_ROOT, MSG_TYPE_UNALLOCATED, MSG_UNUSED_BLOCK, UTF8_SEQUENCE_MINIMUM, WINDOWS_1252_HIGH, compareCFBName, createEmailParser, createMSGBurner, createMSGReader, decodeBase64, decodeLatin1, decodeMIMEEncoding, decodeMIMEText, decodeMIMEWords, decodeUTF8, decodeWindows1252, detectFormat, encodeUTF8, extractMessage, extractMessageFromMSG, failure, fileTimeToUTCString, formatEmailAddress, inferExtension, isEmailFormat, isFailure, isMSGError, isMSGFile, isRecord, isSuccess, msftUUIDStringify, parseMIMEHeaders, parseMIMEPart, readANSIString, readUTF16String, removeTrailingNull, resolveEncoding, roundUpToMultiple, sectorsNeeded, success, toHexLower };
|
|
2444
|
+
|
|
2445
|
+
//# sourceMappingURL=index.js.map
|