mailfile 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +203 -0
  3. package/dist/mailfile.cjs +2035 -0
  4. package/dist/mailfile.js +2037 -0
  5. package/dist/mailfile.min.js +30 -0
  6. package/package.json +84 -0
  7. package/src/cfb/index.js +4 -0
  8. package/src/cfb/read.js +221 -0
  9. package/src/cfb/write.js +262 -0
  10. package/src/errors.js +49 -0
  11. package/src/index.js +86 -0
  12. package/src/mapi/bag.js +195 -0
  13. package/src/mapi/index.js +6 -0
  14. package/src/mapi/tags.js +147 -0
  15. package/src/message.js +199 -0
  16. package/src/mime/build.js +100 -0
  17. package/src/mime/encodings.js +188 -0
  18. package/src/mime/headers.js +344 -0
  19. package/src/mime/index.js +12 -0
  20. package/src/mime/parse.js +153 -0
  21. package/src/msg/read.js +178 -0
  22. package/src/msg/write.js +124 -0
  23. package/src/rtf/deencapsulate.js +155 -0
  24. package/src/rtf/index.js +3 -0
  25. package/src/rtf/lzfu.js +67 -0
  26. package/src/util.js +59 -0
  27. package/types/cfb/index.d.ts +2 -0
  28. package/types/cfb/read.d.ts +70 -0
  29. package/types/cfb/write.d.ts +67 -0
  30. package/types/errors.d.ts +36 -0
  31. package/types/index.d.ts +73 -0
  32. package/types/mapi/bag.d.ts +77 -0
  33. package/types/mapi/index.d.ts +2 -0
  34. package/types/mapi/tags.d.ts +137 -0
  35. package/types/message.d.ts +94 -0
  36. package/types/mime/build.d.ts +27 -0
  37. package/types/mime/encodings.d.ts +52 -0
  38. package/types/mime/headers.d.ts +111 -0
  39. package/types/mime/index.d.ts +4 -0
  40. package/types/mime/parse.d.ts +43 -0
  41. package/types/msg/read.d.ts +26 -0
  42. package/types/msg/write.d.ts +6 -0
  43. package/types/rtf/deencapsulate.d.ts +23 -0
  44. package/types/rtf/index.d.ts +2 -0
  45. package/types/rtf/lzfu.d.ts +8 -0
  46. package/types/util.d.ts +25 -0
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Compound File Binary (OLE2 / CFBF) writer — MS-CFB.
3
+ * Emits version 3 files (512-byte sectors), including the DIFAT path for
4
+ * files large enough to need more than 109 FAT sectors.
5
+ */
6
+
7
+ const SIG = [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1];
8
+ const FREESECT = 0xFFFFFFFF;
9
+ const ENDOFCHAIN = 0xFFFFFFFE;
10
+ const FATSECT = 0xFFFFFFFD;
11
+ const DIFSECT = 0xFFFFFFFC;
12
+ const NOSTREAM = 0xFFFFFFFF;
13
+ const MINI_CUTOFF = 4096;
14
+
15
+ /**
16
+ * A node in the storage tree being built.
17
+ *
18
+ * The `id`/`leftId`/`rightId`/`childId`/`startSector`/`streamSize` fields are
19
+ * assigned by {@link write} while laying the file out; callers never set them.
20
+ *
21
+ * @typedef {object} CfbNode
22
+ * @property {string} name
23
+ * @property {number} type 1 = storage, 2 = stream, 5 = root
24
+ * @property {Uint8Array|null} [clsid]
25
+ * @property {CfbNode[]} [children]
26
+ * @property {Uint8Array} [data]
27
+ * @property {number} [id]
28
+ * @property {number} [leftId]
29
+ * @property {number} [rightId]
30
+ * @property {number} [childId]
31
+ * @property {number} [startSector]
32
+ * @property {number} [streamSize]
33
+ */
34
+
35
+ /**
36
+ * Create an empty root storage.
37
+ * @param {Uint8Array|null} [clsid] 16-byte class id written to the root entry.
38
+ * @returns {CfbNode}
39
+ */
40
+ export function newRoot(clsid) {
41
+ return { name: 'Root Entry', type: 5, clsid: clsid || null, children: [] };
42
+ }
43
+
44
+ /**
45
+ * @param {CfbNode} parent
46
+ * @param {string} name
47
+ * @param {Uint8Array|null} [clsid]
48
+ * @returns {CfbNode}
49
+ */
50
+ export function addStorage(parent, name, clsid) {
51
+ const n = { name, type: 1, clsid: clsid || null, children: [] };
52
+ parent.children.push(n);
53
+ return n;
54
+ }
55
+
56
+ /**
57
+ * @param {CfbNode} parent
58
+ * @param {string} name
59
+ * @param {Uint8Array} [data]
60
+ * @returns {CfbNode}
61
+ */
62
+ export function addStream(parent, name, data) {
63
+ const n = { name, type: 2, data: data || new Uint8Array(0) };
64
+ parent.children.push(n);
65
+ return n;
66
+ }
67
+
68
+ // Directory ordering per MS-CFB: shorter names first, then case-insensitive UTF-16.
69
+ function cmpName(a, b) {
70
+ if (a.length !== b.length) return a.length - b.length;
71
+ const A = a.toUpperCase();
72
+ const B = b.toUpperCase();
73
+ for (let i = 0; i < A.length; i++) {
74
+ const d = A.charCodeAt(i) - B.charCodeAt(i);
75
+ if (d) return d;
76
+ }
77
+ return 0;
78
+ }
79
+
80
+ /**
81
+ * Serialise a storage tree to compound file bytes.
82
+ * @param {CfbNode} root
83
+ * @returns {Uint8Array}
84
+ */
85
+ export function write(root) {
86
+ const flat = [];
87
+ const assign = (node) => { node.id = flat.length; flat.push(node); };
88
+ assign(root);
89
+
90
+ function buildTree(kids, lo, hi) {
91
+ if (lo >= hi) return NOSTREAM;
92
+ const mid = (lo + hi) >> 1;
93
+ const n = kids[mid];
94
+ n.leftId = buildTree(kids, lo, mid);
95
+ n.rightId = buildTree(kids, mid + 1, hi);
96
+ return n.id;
97
+ }
98
+
99
+ function walk(node) {
100
+ if (!node.children || !node.children.length) { node.childId = NOSTREAM; return; }
101
+ const kids = node.children.slice().sort((a, b) => cmpName(a.name, b.name));
102
+ for (let i = 0; i < kids.length; i++) assign(kids[i]);
103
+ node.childId = buildTree(kids, 0, kids.length);
104
+ for (let j = 0; j < kids.length; j++) walk(kids[j]);
105
+ }
106
+ walk(root);
107
+
108
+ const SECTOR = 512;
109
+ const MINI = 64;
110
+ const PER = SECTOR / 4;
111
+
112
+ // Streams >= 4096 bytes get their own sectors; smaller ones go in the mini stream.
113
+ const bigStreams = [];
114
+ const miniStreams = [];
115
+ for (let i = 1; i < flat.length; i++) {
116
+ const n = flat[i];
117
+ if (n.type !== 2) { n.startSector = ENDOFCHAIN; n.streamSize = 0; continue; }
118
+ n.streamSize = n.data.length;
119
+ if (n.data.length === 0) n.startSector = ENDOFCHAIN;
120
+ else if (n.data.length >= MINI_CUTOFF) bigStreams.push(n);
121
+ else miniStreams.push(n);
122
+ }
123
+
124
+ let miniSectorCount = 0;
125
+ for (const ms of miniStreams) {
126
+ ms.startSector = miniSectorCount;
127
+ miniSectorCount += Math.ceil(ms.data.length / MINI);
128
+ }
129
+ const miniStreamSize = miniSectorCount * MINI;
130
+ const miniStreamSectors = Math.ceil(miniStreamSize / SECTOR);
131
+ const miniStreamData = new Uint8Array(miniStreamSectors * SECTOR);
132
+ for (const ms of miniStreams) miniStreamData.set(ms.data, ms.startSector * MINI);
133
+
134
+ const bigSectorCounts = bigStreams.map((s) => Math.ceil(s.data.length / SECTOR));
135
+ const bigTotal = bigSectorCounts.reduce((x, y) => x + y, 0);
136
+ const miniFatSectors = Math.ceil((miniSectorCount * 4) / SECTOR) || 0;
137
+ const dirSectors = Math.ceil(flat.length / 4);
138
+
139
+ // FAT and DIFAT sizes depend on the total sector count, which depends on
140
+ // them in turn. Iterate to a fixed point.
141
+ let fatSectors = 1;
142
+ let difatSectors = 0;
143
+ let total = 0;
144
+ for (let it = 0; it < 64; it++) {
145
+ total = bigTotal + miniStreamSectors + miniFatSectors + dirSectors + fatSectors + difatSectors;
146
+ const nf = Math.max(1, Math.ceil(total / PER));
147
+ const nd = nf <= 109 ? 0 : Math.ceil((nf - 109) / (PER - 1));
148
+ if (nf === fatSectors && nd === difatSectors) break;
149
+ fatSectors = nf;
150
+ difatSectors = nd;
151
+ }
152
+ total = bigTotal + miniStreamSectors + miniFatSectors + dirSectors + fatSectors + difatSectors;
153
+
154
+ let cursor = 0;
155
+ for (let i = 0; i < bigStreams.length; i++) {
156
+ bigStreams[i].startSector = cursor;
157
+ cursor += bigSectorCounts[i];
158
+ }
159
+ const miniStreamStart = miniStreamSectors ? cursor : ENDOFCHAIN;
160
+ cursor += miniStreamSectors;
161
+ const miniFatStart = miniFatSectors ? cursor : ENDOFCHAIN;
162
+ cursor += miniFatSectors;
163
+ const dirStart = cursor;
164
+ cursor += dirSectors;
165
+ const fatStart = cursor;
166
+ cursor += fatSectors;
167
+ const difatStart = difatSectors ? cursor : ENDOFCHAIN;
168
+ cursor += difatSectors;
169
+
170
+ const fat = new Uint32Array(fatSectors * PER);
171
+ fat.fill(FREESECT);
172
+ function runChain(start, count) {
173
+ for (let i = 0; i < count; i++) {
174
+ fat[start + i] = (i === count - 1) ? ENDOFCHAIN : start + i + 1;
175
+ }
176
+ }
177
+ for (let i = 0; i < bigStreams.length; i++) runChain(bigStreams[i].startSector, bigSectorCounts[i]);
178
+ if (miniStreamSectors) runChain(miniStreamStart, miniStreamSectors);
179
+ if (miniFatSectors) runChain(miniFatStart, miniFatSectors);
180
+ runChain(dirStart, dirSectors);
181
+ for (let i = 0; i < fatSectors; i++) fat[fatStart + i] = FATSECT;
182
+ for (let i = 0; i < difatSectors; i++) fat[difatStart + i] = DIFSECT;
183
+
184
+ const miniFat = new Uint32Array((miniFatSectors * SECTOR) / 4);
185
+ miniFat.fill(FREESECT);
186
+ for (const ms of miniStreams) {
187
+ const cnt = Math.ceil(ms.data.length / MINI);
188
+ for (let q = 0; q < cnt; q++) {
189
+ miniFat[ms.startSector + q] = (q === cnt - 1) ? ENDOFCHAIN : ms.startSector + q + 1;
190
+ }
191
+ }
192
+
193
+ root.startSector = miniStreamStart;
194
+ root.streamSize = miniStreamSize;
195
+
196
+ const out = new Uint8Array(SECTOR * (1 + total));
197
+ const dv = new DataView(out.buffer);
198
+ for (let i = 0; i < 8; i++) out[i] = SIG[i];
199
+ dv.setUint16(24, 0x003E, true); // minor version
200
+ dv.setUint16(26, 0x0003, true); // major version -> 512-byte sectors
201
+ dv.setUint16(28, 0xFFFE, true); // little endian
202
+ dv.setUint16(30, 9, true); // sector shift
203
+ dv.setUint16(32, 6, true); // mini sector shift
204
+ dv.setUint32(44, fatSectors, true);
205
+ dv.setUint32(48, dirStart, true);
206
+ dv.setUint32(56, MINI_CUTOFF, true);
207
+ dv.setUint32(60, miniFatStart, true);
208
+ dv.setUint32(64, miniFatSectors, true);
209
+ dv.setUint32(68, difatStart, true);
210
+ dv.setUint32(72, difatSectors, true);
211
+ for (let h = 0; h < 109; h++) {
212
+ dv.setUint32(76 + h * 4, h < fatSectors ? fatStart + h : FREESECT, true);
213
+ }
214
+
215
+ const off = (sector) => SECTOR * (1 + sector);
216
+
217
+ for (const s of bigStreams) out.set(s.data, off(s.startSector));
218
+ if (miniStreamSectors) out.set(miniStreamData, off(miniStreamStart));
219
+ if (miniFatSectors) {
220
+ for (let i = 0; i < miniFat.length; i++) {
221
+ dv.setUint32(off(miniFatStart) + i * 4, miniFat[i], true);
222
+ }
223
+ }
224
+ for (let i = 0; i < fat.length; i++) dv.setUint32(off(fatStart) + i * 4, fat[i], true);
225
+
226
+ for (let w = 0; w < difatSectors; w++) {
227
+ const dbase = off(difatStart + w);
228
+ for (let k = 0; k < PER - 1; k++) {
229
+ const idx = 109 + w * (PER - 1) + k;
230
+ dv.setUint32(dbase + k * 4, idx < fatSectors ? fatStart + idx : FREESECT, true);
231
+ }
232
+ dv.setUint32(dbase + SECTOR - 4,
233
+ w === difatSectors - 1 ? ENDOFCHAIN : difatStart + w + 1, true);
234
+ }
235
+
236
+ for (let e = 0; e < flat.length; e++) {
237
+ const node = flat[e];
238
+ const base = off(dirStart) + e * 128;
239
+ const nlen = Math.min(node.name.length, 31);
240
+ for (let p = 0; p < nlen; p++) dv.setUint16(base + p * 2, node.name.charCodeAt(p), true);
241
+ dv.setUint16(base + 64, nlen * 2 + 2, true);
242
+ dv.setUint8(base + 66, node.type);
243
+ dv.setUint8(base + 67, 1); // black
244
+ dv.setUint32(base + 68, node.leftId == null ? NOSTREAM : node.leftId, true);
245
+ dv.setUint32(base + 72, node.rightId == null ? NOSTREAM : node.rightId, true);
246
+ dv.setUint32(base + 76, node.childId == null ? NOSTREAM : node.childId, true);
247
+ if (node.clsid) out.set(node.clsid, base + 80);
248
+ dv.setUint32(base + 116, node.startSector == null ? ENDOFCHAIN : node.startSector, true);
249
+ const sz = node.streamSize || 0;
250
+ dv.setUint32(base + 120, sz >>> 0, true);
251
+ dv.setUint32(base + 124, Math.floor(sz / 4294967296), true);
252
+ }
253
+ for (let e = flat.length; e < dirSectors * 4; e++) {
254
+ const base = off(dirStart) + e * 128;
255
+ dv.setUint32(base + 68, NOSTREAM, true);
256
+ dv.setUint32(base + 72, NOSTREAM, true);
257
+ dv.setUint32(base + 76, NOSTREAM, true);
258
+ dv.setUint32(base + 116, ENDOFCHAIN, true);
259
+ }
260
+
261
+ return out;
262
+ }
package/src/errors.js ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Error codes thrown by mailfile. Callers should branch on `error.code`
3
+ * rather than on message text.
4
+ * @readonly
5
+ * @enum {string}
6
+ */
7
+ export const ErrorCode = {
8
+ /** The bytes are not an OLE2 compound file (wrong magic number). */
9
+ NOT_COMPOUND_FILE: 'NOT_COMPOUND_FILE',
10
+ /** The compound file's internal structure is inconsistent or damaged. */
11
+ CORRUPT_CFB: 'CORRUPT_CFB',
12
+ /** A stream ended before the declared length. */
13
+ TRUNCATED_STREAM: 'TRUNCATED_STREAM',
14
+ /** The compound file is valid but is not an Outlook message. */
15
+ NOT_A_MESSAGE: 'NOT_A_MESSAGE',
16
+ /** Input was empty. */
17
+ EMPTY_INPUT: 'EMPTY_INPUT',
18
+ /** A structure is well-formed but uses a feature mailfile cannot represent. */
19
+ UNSUPPORTED: 'UNSUPPORTED',
20
+ /** Strict mode only: the input deviates from the specification. */
21
+ MALFORMED: 'MALFORMED'
22
+ };
23
+
24
+ /**
25
+ * All errors raised by mailfile.
26
+ */
27
+ export class MailfileError extends Error {
28
+ /**
29
+ * @param {string} code One of {@link ErrorCode}.
30
+ * @param {string} message Human-readable description.
31
+ * @param {{cause?: unknown}} [options]
32
+ */
33
+ constructor(code, message, options) {
34
+ super(message);
35
+ this.name = 'MailfileError';
36
+ /** @type {string} */
37
+ this.code = code;
38
+ if (options && options.cause !== undefined) this.cause = options.cause;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * @param {string} code
44
+ * @param {string} message
45
+ * @returns {never}
46
+ */
47
+ export function fail(code, message) {
48
+ throw new MailfileError(code, message);
49
+ }
package/src/index.js ADDED
@@ -0,0 +1,86 @@
1
+ /**
2
+ * mailfile — read and write Outlook `.msg` and RFC 5322 `.eml` files.
3
+ * Zero dependencies; runs in browsers, Node, Deno, Bun and workers.
4
+ */
5
+ import { Message } from './message.js';
6
+ import { MailfileError, ErrorCode } from './errors.js';
7
+ import { replaceExt } from './util.js';
8
+
9
+ export { Message } from './message.js';
10
+ export { MailfileError, ErrorCode } from './errors.js';
11
+ export { Headers } from './mime/headers.js';
12
+
13
+ /** @type {string} */
14
+ export const version = '0.1.0';
15
+
16
+ const MSG_MAGIC = [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1];
17
+
18
+ /**
19
+ * Identify a message file from its content, falling back to the extension.
20
+ *
21
+ * Compound-file magic bytes are authoritative, so a `.msg` renamed to `.eml`
22
+ * is still detected correctly.
23
+ *
24
+ * @param {Uint8Array} bytes
25
+ * @param {string} [filename]
26
+ * @returns {'msg'|'eml'}
27
+ */
28
+ export function detect(bytes, filename) {
29
+ if (bytes && bytes.length >= 8) {
30
+ let match = true;
31
+ for (let i = 0; i < 8; i++) {
32
+ if (bytes[i] !== MSG_MAGIC[i]) { match = false; break; }
33
+ }
34
+ if (match) return 'msg';
35
+ }
36
+ const ext = (/\.([^.]+)$/.exec(filename || '') || [])[1];
37
+ if (ext && ext.toLowerCase() === 'msg') return 'msg';
38
+ return 'eml';
39
+ }
40
+
41
+ /**
42
+ * @typedef {object} ConvertResult
43
+ * @property {Uint8Array} data Converted bytes.
44
+ * @property {'msg'|'eml'} format Format of `data`.
45
+ * @property {'msg'|'eml'} sourceFormat Format that was detected on input.
46
+ * @property {string} mime Content type for `data`.
47
+ * @property {string} [filename] Suggested output name, when one was given.
48
+ * @property {Message} message The parsed message.
49
+ */
50
+
51
+ /**
52
+ * Convert a message to the other format, detecting the input automatically.
53
+ *
54
+ * @example
55
+ * const { data, format } = convert(bytes, { filename: 'note.msg' })
56
+ * // data is .eml bytes, format === 'eml'
57
+ *
58
+ * @param {Uint8Array} bytes
59
+ * @param {object} [opts]
60
+ * @param {string} [opts.filename] Used for detection and for naming the output.
61
+ * @param {'msg'|'eml'} [opts.to] Force the target format instead of flipping.
62
+ * @param {'msg'|'eml'} [opts.from] Skip detection and assume this input format.
63
+ * @param {boolean} [opts.lazy] Defer attachment extraction (`.msg` input only).
64
+ * @returns {ConvertResult}
65
+ */
66
+ export function convert(bytes, opts = {}) {
67
+ if (!bytes || !bytes.length) {
68
+ throw new MailfileError(ErrorCode.EMPTY_INPUT, 'File is empty');
69
+ }
70
+ const sourceFormat = opts.from || detect(bytes, opts.filename);
71
+ const target = opts.to || (sourceFormat === 'msg' ? 'eml' : 'msg');
72
+
73
+ const message = sourceFormat === 'msg'
74
+ ? Message.fromMsg(bytes, { lazy: opts.lazy })
75
+ : Message.fromEml(bytes);
76
+
77
+ const data = target === 'msg' ? message.toMsg() : message.toEml();
78
+ return {
79
+ data,
80
+ format: target,
81
+ sourceFormat,
82
+ mime: target === 'msg' ? 'application/vnd.ms-outlook' : 'message/rfc822',
83
+ filename: opts.filename ? replaceExt(opts.filename, '.' + target) : undefined,
84
+ message
85
+ };
86
+ }
@@ -0,0 +1,195 @@
1
+ /** Reading and writing MAPI property bags inside a compound file — MS-OXMSG. */
2
+ import { addStream } from '../cfb/write.js';
3
+ import { decodeBytes, utf16Decode, utf16Encode } from '../mime/encodings.js';
4
+ import {
5
+ CODEPAGES, PropId, PropType, filetimeToDate, dateToFiletime, hex4, tagName
6
+ } from './tags.js';
7
+
8
+ /**
9
+ * A read-only view of one object's MAPI properties.
10
+ *
11
+ * Variable-length properties live in `__substg1.0_*` streams; fixed-length
12
+ * ones live in `__properties_version1.0`.
13
+ */
14
+ export class PropertyBag {
15
+ /**
16
+ * @param {import('../cfb/read.js').CfbFile} cfb
17
+ * @param {object} storage Directory entry of the storage to read.
18
+ * @param {number} headerSize One of `HeaderSize.*`.
19
+ */
20
+ constructor(cfb, storage, headerSize) {
21
+ const kids = cfb.childrenOf(storage);
22
+ /** @type {Record<string, Uint8Array>} keyed by 8-hex-digit tag */
23
+ this.vars = Object.create(null);
24
+ /** @type {Record<number, {type:number, lo:number, hi:number, i32:number}>} */
25
+ this.fixed = Object.create(null);
26
+ /** @type {object[]} Nested storages (recipients, attachments, embedded messages). */
27
+ this.subStorages = [];
28
+ this.cfb = cfb;
29
+ this.childMap = kids.map;
30
+
31
+ for (const k of kids.list) {
32
+ const m = /^__substg1\.0_([0-9A-Fa-f]{8})$/.exec(k.name);
33
+ if (m) {
34
+ if (k.type === 2) this.vars[m[1].toUpperCase()] = cfb.readStream(k);
35
+ else this.subStorages.push(k);
36
+ continue;
37
+ }
38
+ if (k.type === 1) this.subStorages.push(k);
39
+ }
40
+
41
+ const propsEntry = kids.map['__properties_version1.0'];
42
+ if (propsEntry) {
43
+ const buf = cfb.readStream(propsEntry);
44
+ const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
45
+ for (let off = headerSize; off + 16 <= buf.length; off += 16) {
46
+ this.fixed[dv.getUint16(off + 2, true)] = {
47
+ type: dv.getUint16(off, true),
48
+ lo: dv.getUint32(off + 8, true),
49
+ hi: dv.getUint32(off + 12, true),
50
+ i32: dv.getInt32(off + 8, true)
51
+ };
52
+ }
53
+ }
54
+
55
+ const cpid = this.fixed[PropId.INTERNET_CPID]?.i32 ??
56
+ this.fixed[PropId.MESSAGE_CODEPAGE]?.i32 ?? 0;
57
+ /** Encoding used for 8-bit string properties in this object. */
58
+ this.charset = CODEPAGES[cpid] || 'windows-1252';
59
+ }
60
+
61
+ /**
62
+ * String property, preferring the Unicode variant.
63
+ * @param {number} id @returns {string}
64
+ */
65
+ str(id) {
66
+ const u = this.vars[hex4(id) + '001F'];
67
+ if (u) return utf16Decode(u);
68
+ const a = this.vars[hex4(id) + '001E'];
69
+ if (a) return decodeBytes(a, this.charset);
70
+ return '';
71
+ }
72
+
73
+ /** @param {number} id @returns {Uint8Array|null} */
74
+ bin(id) {
75
+ return this.vars[hex4(id) + '0102'] || null;
76
+ }
77
+
78
+ /** @param {number} id @returns {number|null} */
79
+ int(id) {
80
+ return this.fixed[id] ? this.fixed[id].i32 : null;
81
+ }
82
+
83
+ /** @param {number} id @returns {boolean|null} */
84
+ bool(id) {
85
+ return this.fixed[id] ? !!(this.fixed[id].lo & 1) : null;
86
+ }
87
+
88
+ /** @param {number} id @returns {Date|null} */
89
+ date(id) {
90
+ return this.fixed[id] ? filetimeToDate(this.fixed[id].lo, this.fixed[id].hi) : null;
91
+ }
92
+
93
+ /** @param {number} id @returns {boolean} */
94
+ has(id) {
95
+ const h = hex4(id);
96
+ return !!(this.fixed[id] || this.vars[h + '001F'] || this.vars[h + '001E'] ||
97
+ this.vars[h + '0102']);
98
+ }
99
+
100
+ /** Every property id present on this object. @returns {number[]} */
101
+ ids() {
102
+ const set = new Set(Object.keys(this.fixed).map(Number));
103
+ for (const tag of Object.keys(this.vars)) set.add(parseInt(tag.slice(0, 4), 16));
104
+ return [...set].sort((a, b) => a - b);
105
+ }
106
+
107
+ /** Nested storages whose names match a prefix, in directory order. */
108
+ storagesMatching(re) {
109
+ return this.subStorages.filter((s) => re.test(s.name))
110
+ .sort((a, b) => a.name.localeCompare(b.name));
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Accumulates properties and emits the streams for one object.
116
+ */
117
+ export class PropertyWriter {
118
+ /**
119
+ * @param {import('../cfb/write.js').CfbNode} node Storage to write into.
120
+ * @param {number} headerSize One of `HeaderSize.*`.
121
+ */
122
+ constructor(node, headerSize) {
123
+ this.node = node;
124
+ this.headerSize = headerSize;
125
+ this.entries = [];
126
+ }
127
+
128
+ _entry(id, type, size, lo, hi) {
129
+ this.entries.push({ id, type, size, lo: lo || 0, hi: hi || 0 });
130
+ }
131
+
132
+ /** @param {number} id @param {string} value */
133
+ str(id, value) {
134
+ if (value == null || value === '') return this;
135
+ const bytes = utf16Encode(String(value));
136
+ addStream(this.node, tagName(id, PropType.UNICODE), bytes);
137
+ // The declared size includes the terminating null, which is not stored.
138
+ this._entry(id, PropType.UNICODE, bytes.length + 2);
139
+ return this;
140
+ }
141
+
142
+ /** @param {number} id @param {Uint8Array} bytes */
143
+ bin(id, bytes) {
144
+ if (!bytes) return this;
145
+ addStream(this.node, tagName(id, PropType.BINARY), bytes);
146
+ this._entry(id, PropType.BINARY, bytes.length);
147
+ return this;
148
+ }
149
+
150
+ /** @param {number} id @param {number} v */
151
+ int32(id, v) {
152
+ if (v == null) return this;
153
+ this._entry(id, PropType.LONG, null, v >>> 0, 0);
154
+ return this;
155
+ }
156
+
157
+ /** @param {number} id @param {boolean} v */
158
+ bool(id, v) {
159
+ this._entry(id, PropType.BOOLEAN, null, v ? 1 : 0, 0);
160
+ return this;
161
+ }
162
+
163
+ /** @param {number} id @param {Date} date */
164
+ time(id, date) {
165
+ if (!date) return this;
166
+ const ft = dateToFiletime(date);
167
+ this._entry(id, PropType.SYSTIME, null, ft.lo, ft.hi);
168
+ return this;
169
+ }
170
+
171
+ /**
172
+ * Write `__properties_version1.0`.
173
+ * @param {(dv: DataView) => void} [headerFill] Fills the stream header.
174
+ */
175
+ finish(headerFill) {
176
+ const buf = new Uint8Array(this.headerSize + this.entries.length * 16);
177
+ const dv = new DataView(buf.buffer);
178
+ if (headerFill) headerFill(dv);
179
+ for (let i = 0; i < this.entries.length; i++) {
180
+ const e = this.entries[i];
181
+ const off = this.headerSize + i * 16;
182
+ dv.setUint16(off, e.type, true);
183
+ dv.setUint16(off + 2, e.id, true);
184
+ dv.setUint32(off + 4, 0x00000006, true); // readable | writable
185
+ if (e.size != null) {
186
+ dv.setUint32(off + 8, e.size, true);
187
+ dv.setUint32(off + 12, 0, true);
188
+ } else {
189
+ dv.setUint32(off + 8, e.lo, true);
190
+ dv.setUint32(off + 12, e.hi, true);
191
+ }
192
+ }
193
+ addStream(this.node, '__properties_version1.0', buf);
194
+ }
195
+ }
@@ -0,0 +1,6 @@
1
+ /** MAPI property tags and property-bag access over a compound file. */
2
+ export { PropertyBag, PropertyWriter } from './bag.js';
3
+ export {
4
+ PropId, PropType, RecipientType, AttachMethod, HeaderSize,
5
+ MSG_CLSID, CODEPAGES, hex4, hex8, tagName, filetimeToDate, dateToFiletime
6
+ } from './tags.js';