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,147 @@
1
+ /** MAPI property tags, types and code pages — MS-OXPROPS / MS-OXMSG. */
2
+
3
+ /**
4
+ * Property identifiers referenced by mailfile. This is deliberately a subset:
5
+ * use `message.props` to read anything not listed here.
6
+ * @readonly
7
+ */
8
+ export const PropId = {
9
+ SUBJECT: 0x0037,
10
+ NORMALIZED_SUBJECT: 0x0E1D,
11
+ BODY: 0x1000,
12
+ RTF_COMPRESSED: 0x1009,
13
+ HTML: 0x1013,
14
+ MESSAGE_CLASS: 0x001A,
15
+ TRANSPORT_HEADERS: 0x007D,
16
+ CLIENT_SUBMIT_TIME: 0x0039,
17
+ DELIVERY_TIME: 0x0E06,
18
+ LAST_MODIFICATION_TIME: 0x3008,
19
+ CREATION_TIME: 0x3007,
20
+ INTERNET_MESSAGE_ID: 0x1035,
21
+ IN_REPLY_TO_ID: 0x1042,
22
+ INTERNET_REFERENCES: 0x1039,
23
+ DISPLAY_TO: 0x0E04,
24
+ DISPLAY_CC: 0x0E03,
25
+ DISPLAY_BCC: 0x0E02,
26
+ SENDER_NAME: 0x0C1A,
27
+ SENDER_EMAIL: 0x0C1F,
28
+ SENDER_ADDRTYPE: 0x0C1E,
29
+ SENDER_SMTP: 0x5D01,
30
+ SENT_REP_NAME: 0x0042,
31
+ SENT_REP_EMAIL: 0x0065,
32
+ SENT_REP_ADDRTYPE: 0x0064,
33
+ SENT_REP_SMTP: 0x5D02,
34
+ MESSAGE_FLAGS: 0x0E07,
35
+ HASATTACH: 0x0E1B,
36
+ STORE_SUPPORT_MASK: 0x340D,
37
+ INTERNET_CPID: 0x3FDE,
38
+ MESSAGE_CODEPAGE: 0x3FFD,
39
+ PRIORITY: 0x0026,
40
+ IMPORTANCE: 0x0017,
41
+ OBJECT_TYPE: 0x0FFE,
42
+ DISPLAY_TYPE: 0x3900,
43
+ ROWID: 0x3000,
44
+ RECIPIENT_TYPE: 0x0C15,
45
+ DISPLAY_NAME: 0x3001,
46
+ EMAIL_ADDRESS: 0x3003,
47
+ ADDRTYPE: 0x3002,
48
+ SMTP_ADDRESS: 0x39FE,
49
+ ATTACH_METHOD: 0x3705,
50
+ ATTACH_DATA_BIN: 0x3701,
51
+ ATTACH_LONG_FILENAME: 0x3707,
52
+ ATTACH_FILENAME: 0x3704,
53
+ ATTACH_EXTENSION: 0x3703,
54
+ ATTACH_MIME_TAG: 0x370E,
55
+ ATTACH_CONTENT_ID: 0x3712,
56
+ ATTACH_SIZE: 0x0E20,
57
+ ATTACH_NUM: 0x0E21,
58
+ ATTACH_FLAGS: 0x3714,
59
+ ATTACHMENT_HIDDEN: 0x7FFE,
60
+ RENDERING_POSITION: 0x370B
61
+ };
62
+
63
+ /** MAPI property types. @readonly */
64
+ export const PropType = {
65
+ SHORT: 0x0002,
66
+ LONG: 0x0003,
67
+ FLOAT: 0x0004,
68
+ DOUBLE: 0x0005,
69
+ BOOLEAN: 0x000B,
70
+ I8: 0x0014,
71
+ STRING8: 0x001E,
72
+ UNICODE: 0x001F,
73
+ SYSTIME: 0x0040,
74
+ BINARY: 0x0102,
75
+ OBJECT: 0x000D
76
+ };
77
+
78
+ /** Recipient kinds (PR_RECIPIENT_TYPE). @readonly */
79
+ export const RecipientType = { TO: 1, CC: 2, BCC: 3 };
80
+
81
+ /** Attachment storage methods (PR_ATTACH_METHOD). @readonly */
82
+ export const AttachMethod = { NONE: 0, BY_VALUE: 1, BY_REFERENCE: 2, EMBEDDED_MSG: 5 };
83
+
84
+ /** Property stream header sizes, by the object the stream belongs to. @readonly */
85
+ export const HeaderSize = {
86
+ TOP_LEVEL: 32,
87
+ EMBEDDED: 24,
88
+ RECIPIENT: 8,
89
+ ATTACHMENT: 8
90
+ };
91
+
92
+ /** CLSID written to the root entry of a .msg file. */
93
+ export const MSG_CLSID = new Uint8Array([
94
+ 0x0B, 0x0D, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
95
+ 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46
96
+ ]);
97
+
98
+ /** Windows code page number -> WHATWG encoding label. */
99
+ export const CODEPAGES = {
100
+ 20127: 'us-ascii', 28591: 'iso-8859-1', 28592: 'iso-8859-2', 28595: 'iso-8859-5',
101
+ 28597: 'iso-8859-7', 28598: 'iso-8859-8', 28599: 'iso-8859-9', 28605: 'iso-8859-15',
102
+ 65000: 'utf-7', 65001: 'utf-8', 1250: 'windows-1250', 1251: 'windows-1251',
103
+ 1252: 'windows-1252', 1253: 'windows-1253', 1254: 'windows-1254', 1255: 'windows-1255',
104
+ 1256: 'windows-1256', 1257: 'windows-1257', 1258: 'windows-1258',
105
+ 932: 'shift_jis', 936: 'gbk', 949: 'euc-kr', 950: 'big5', 874: 'windows-874'
106
+ };
107
+
108
+ /** @param {number} n @returns {string} */
109
+ export function hex4(n) {
110
+ return ('000' + n.toString(16).toUpperCase()).slice(-4);
111
+ }
112
+
113
+ /** @param {number} n @returns {string} */
114
+ export function hex8(n) {
115
+ return ('0000000' + n.toString(16).toUpperCase()).slice(-8);
116
+ }
117
+
118
+ /**
119
+ * Stream name for a variable-length property, e.g. `__substg1.0_0037001F`.
120
+ * @param {number} id
121
+ * @param {number} type
122
+ * @returns {string}
123
+ */
124
+ export function tagName(id, type) {
125
+ return '__substg1.0_' + hex4(id) + hex4(type);
126
+ }
127
+
128
+ /**
129
+ * FILETIME (100ns ticks since 1601-01-01) -> Date.
130
+ * @param {number} lo @param {number} hi @returns {Date|null}
131
+ */
132
+ export function filetimeToDate(lo, hi) {
133
+ if (!lo && !hi) return null;
134
+ const ft = hi * 4294967296 + lo;
135
+ const ms = ft / 10000 - 11644473600000;
136
+ if (!isFinite(ms) || ms < -2208988800000 || ms > 4102444800000) return null;
137
+ return new Date(ms);
138
+ }
139
+
140
+ /**
141
+ * Date -> FILETIME halves.
142
+ * @param {Date} date @returns {{lo: number, hi: number}}
143
+ */
144
+ export function dateToFiletime(date) {
145
+ const ft = Math.round((date.getTime() + 11644473600000) * 10000);
146
+ return { lo: ft % 4294967296, hi: Math.floor(ft / 4294967296) };
147
+ }
package/src/message.js ADDED
@@ -0,0 +1,199 @@
1
+ /** The format-neutral message model that sits between .msg and .eml. */
2
+ import { buildEml } from './mime/build.js';
3
+ import { parseEml } from './mime/parse.js';
4
+ import {
5
+ Headers, decodeWords, encodeWord, formatAddress, formatDate,
6
+ parseAddressList, parseDate
7
+ } from './mime/headers.js';
8
+ import { readMsg } from './msg/read.js';
9
+ import { writeMsg } from './msg/write.js';
10
+
11
+ /**
12
+ * @typedef {object} Address
13
+ * @property {string} name
14
+ * @property {string} email
15
+ */
16
+
17
+ /**
18
+ * @typedef {object} Attachment
19
+ * @property {string} filename
20
+ * @property {string} mime
21
+ * @property {Uint8Array} data
22
+ * @property {string} [cid] Content-ID, without angle brackets.
23
+ * @property {boolean} [inline] True when referenced from the HTML body by cid.
24
+ */
25
+
26
+ // Content headers are rebuilt from the body structure, so any copies carried
27
+ // over from the source would contradict what we emit.
28
+ const CONTENT_HEADERS = new Set([
29
+ 'mime-version', 'content-type', 'content-transfer-encoding',
30
+ 'content-disposition', 'content-id', 'content-description',
31
+ 'content-language', 'content-location'
32
+ ]);
33
+
34
+ const IMPORTANCE = { low: 0, normal: 1, high: 2 };
35
+
36
+ /**
37
+ * A single email message, independent of its on-disk format.
38
+ *
39
+ * Fields are plain and mutable: read one in, change what you need, write the
40
+ * other out.
41
+ */
42
+ export class Message {
43
+ /** @param {Partial<Message>} [data] */
44
+ constructor(data = {}) {
45
+ /** @type {string} */
46
+ this.subject = data.subject || '';
47
+ /** @type {Address} */
48
+ this.from = data.from || { name: '', email: '' };
49
+ /** @type {Address[]} */
50
+ this.to = data.to || [];
51
+ /** @type {Address[]} */
52
+ this.cc = data.cc || [];
53
+ /** @type {Address[]} */
54
+ this.bcc = data.bcc || [];
55
+ /** @type {Date|null} */
56
+ this.date = data.date || null;
57
+ /** @type {string} */
58
+ this.messageId = data.messageId || '';
59
+ /** @type {string} */
60
+ this.inReplyTo = data.inReplyTo || '';
61
+ /** @type {string} */
62
+ this.references = data.references || '';
63
+ /** @type {number|null} 0 low, 1 normal, 2 high. */
64
+ this.importance = data.importance == null ? null : data.importance;
65
+ /** @type {string} MAPI message class, e.g. `IPM.Note`. */
66
+ this.messageClass = data.messageClass || 'IPM.Note';
67
+ /** @type {string} Plain-text body. */
68
+ this.text = data.text || '';
69
+ /** @type {string} HTML body. */
70
+ this.html = data.html || '';
71
+ /** @type {Attachment[]} */
72
+ this.attachments = data.attachments || [];
73
+ /** @type {Headers} Original internet headers, when the source had them. */
74
+ this.headers = data.headers instanceof Headers
75
+ ? data.headers
76
+ : new Headers(data.headers || []);
77
+ /**
78
+ * Raw MAPI property access — present only for messages read from `.msg`.
79
+ * Use it to reach properties this model does not surface (appointments,
80
+ * contacts, custom properties).
81
+ * @type {import('./mapi/bag.js').PropertyBag|null}
82
+ */
83
+ this.props = data.props || null;
84
+ }
85
+
86
+ /**
87
+ * Parse an Outlook `.msg` file.
88
+ * @param {Uint8Array} bytes
89
+ * @param {{lazy?: boolean}} [opts] `lazy` defers attachment byte extraction
90
+ * until `.data` is read — useful when you only want headers.
91
+ * @returns {Message}
92
+ */
93
+ static fromMsg(bytes, opts = {}) {
94
+ const data = readMsg(bytes, opts);
95
+ // Embedded messages are represented as nested .eml attachments.
96
+ data.attachments = data.attachments.map((a) => {
97
+ if (!a.embedded) return a;
98
+ const { embedded, ...rest } = a;
99
+ return { ...rest, data: new Message(embedded).toEml() };
100
+ });
101
+ return new Message(data);
102
+ }
103
+
104
+ /**
105
+ * Parse an RFC 5322 `.eml` file.
106
+ * @param {Uint8Array} bytes
107
+ * @returns {Message}
108
+ */
109
+ static fromEml(bytes) {
110
+ const parsed = parseEml(bytes);
111
+ const h = parsed.headers;
112
+ return new Message({
113
+ subject: decodeWords(h.get('subject') || ''),
114
+ from: parseAddressList(h.get('from') || '')[0] || { name: '', email: '' },
115
+ to: parseAddressList(h.get('to') || ''),
116
+ cc: parseAddressList(h.get('cc') || ''),
117
+ bcc: parseAddressList(h.get('bcc') || ''),
118
+ date: parseDate(h.get('date')),
119
+ messageId: h.get('message-id') || '',
120
+ inReplyTo: h.get('in-reply-to') || '',
121
+ references: h.get('references') || '',
122
+ importance: IMPORTANCE[(h.get('importance') || '').toLowerCase()] ?? null,
123
+ text: parsed.text,
124
+ html: parsed.html,
125
+ attachments: parsed.attachments,
126
+ headers: h
127
+ });
128
+ }
129
+
130
+ /**
131
+ * Serialise to an RFC 5322 `.eml` document.
132
+ *
133
+ * Headers carried in from the source are preserved verbatim — minus the
134
+ * content headers, which are regenerated to match the body actually written.
135
+ * @returns {Uint8Array}
136
+ */
137
+ toEml() {
138
+ /** @type {Array<[string, string]>} */
139
+ const out = [];
140
+ const seen = new Set();
141
+ for (const [k, v] of this.headers) {
142
+ if (CONTENT_HEADERS.has(k.toLowerCase())) continue;
143
+ out.push([k, v]);
144
+ seen.add(k.toLowerCase());
145
+ }
146
+ const put = (name, value) => {
147
+ if (!value || seen.has(name.toLowerCase())) return;
148
+ out.push([name, value]);
149
+ seen.add(name.toLowerCase());
150
+ };
151
+ const list = (addrs) =>
152
+ addrs.map((a) => formatAddress(a.name, a.email)).filter(Boolean).join(', ');
153
+
154
+ if (this.date) put('Date', formatDate(this.date));
155
+ if (this.from.email || this.from.name) {
156
+ put('From', formatAddress(this.from.name, this.from.email));
157
+ }
158
+ if (this.to.length) put('To', list(this.to));
159
+ if (this.cc.length) put('Cc', list(this.cc));
160
+ if (this.bcc.length) put('Bcc', list(this.bcc));
161
+ if (!seen.has('subject')) put('Subject', encodeWord(this.subject || '') || ' ');
162
+ put('Message-ID', this.messageId);
163
+ put('In-Reply-To', this.inReplyTo);
164
+ put('References', this.references);
165
+
166
+ return buildEml({
167
+ headers: out,
168
+ text: this.text,
169
+ html: this.html,
170
+ attachments: this.attachments
171
+ });
172
+ }
173
+
174
+ /**
175
+ * Serialise to an Outlook `.msg` file.
176
+ * @returns {Uint8Array}
177
+ */
178
+ toMsg() {
179
+ return writeMsg({
180
+ messageClass: this.messageClass,
181
+ subject: this.subject,
182
+ from: this.from,
183
+ to: this.to,
184
+ cc: this.cc,
185
+ bcc: this.bcc,
186
+ date: this.date,
187
+ messageId: this.messageId,
188
+ inReplyTo: this.inReplyTo,
189
+ references: this.references,
190
+ importance: this.importance,
191
+ text: this.text,
192
+ html: this.html,
193
+ attachments: this.attachments,
194
+ // Keeping the original header block lets a later .msg -> .eml
195
+ // conversion restore the full Received: trail.
196
+ headersRaw: this.headers.entries.length ? this.headers.raw : ''
197
+ });
198
+ }
199
+ }
@@ -0,0 +1,100 @@
1
+ /** MIME document generation. */
2
+ import { bytesToBase64, encodeQP, utf8Encode, wrap } from './encodings.js';
3
+ import { encodeParamValue, foldHeader } from './headers.js';
4
+
5
+ function randomBoundary(tag) {
6
+ let s = '';
7
+ for (let i = 0; i < 24; i++) s += '0123456789abcdef'[Math.floor(Math.random() * 16)];
8
+ return '----=_' + tag + '_' + s;
9
+ }
10
+
11
+ function textPart(mime, content) {
12
+ return [
13
+ 'Content-Type: ' + mime + '; charset="utf-8"',
14
+ 'Content-Transfer-Encoding: quoted-printable',
15
+ '',
16
+ encodeQP(utf8Encode(content))
17
+ ].join('\r\n');
18
+ }
19
+
20
+ function attachmentPart(att) {
21
+ const mime = att.mime || 'application/octet-stream';
22
+ const lines = [
23
+ 'Content-Type: ' + mime + '; ' + encodeParamValue('name', att.filename),
24
+ 'Content-Transfer-Encoding: base64'
25
+ ];
26
+ if (att.cid) lines.push('Content-ID: <' + att.cid + '>');
27
+ lines.push('Content-Disposition: ' + (att.inline ? 'inline' : 'attachment') + '; ' +
28
+ encodeParamValue('filename', att.filename));
29
+ lines.push('');
30
+ lines.push(wrap(bytesToBase64(att.data), 76));
31
+ return lines.join('\r\n');
32
+ }
33
+
34
+ function multipart(subtype, parts, extraParams) {
35
+ const b = randomBoundary(subtype.toUpperCase());
36
+ const head = 'Content-Type: multipart/' + subtype + '; boundary="' + b + '"' +
37
+ (extraParams || '');
38
+ let body = '';
39
+ for (const p of parts) body += '--' + b + '\r\n' + p + '\r\n';
40
+ body += '--' + b + '--\r\n';
41
+ return { head, body };
42
+ }
43
+
44
+ /**
45
+ * Build an `.eml` document.
46
+ *
47
+ * Structure is chosen from what is present: `multipart/alternative` for a
48
+ * text+html pair, wrapped in `multipart/related` when there are inline
49
+ * (cid-referenced) parts, wrapped again in `multipart/mixed` for ordinary
50
+ * attachments.
51
+ *
52
+ * @param {object} msg
53
+ * @param {Array<[string,string]>} [msg.headers] Emitted before the MIME headers.
54
+ * @param {string} [msg.text]
55
+ * @param {string} [msg.html]
56
+ * @param {Array<{filename: string, mime: string, data: Uint8Array, cid?: string, inline?: boolean}>} [msg.attachments]
57
+ * @returns {Uint8Array}
58
+ */
59
+ export function buildEml(msg) {
60
+ const inline = [];
61
+ const regular = [];
62
+ for (const a of msg.attachments || []) {
63
+ if (a.inline && a.cid) inline.push(a);
64
+ else regular.push(a);
65
+ }
66
+
67
+ const bodyParts = [];
68
+ if (msg.text) bodyParts.push(textPart('text/plain', msg.text));
69
+ if (msg.html) bodyParts.push(textPart('text/html', msg.html));
70
+ if (!bodyParts.length) bodyParts.push(textPart('text/plain', ''));
71
+
72
+ let current;
73
+ if (bodyParts.length > 1) {
74
+ const alt = multipart('alternative', bodyParts);
75
+ current = alt.head + '\r\n\r\n' + alt.body;
76
+ } else {
77
+ current = bodyParts[0];
78
+ }
79
+
80
+ if (inline.length) {
81
+ const rel = multipart('related', [current].concat(inline.map(attachmentPart)),
82
+ '; type="text/html"');
83
+ current = rel.head + '\r\n\r\n' + rel.body;
84
+ }
85
+ if (regular.length) {
86
+ const mixed = multipart('mixed', [current].concat(regular.map(attachmentPart)));
87
+ current = mixed.head + '\r\n\r\n' + mixed.body;
88
+ }
89
+
90
+ const idx = current.indexOf('\r\n\r\n');
91
+ const partHeaders = current.slice(0, idx);
92
+ const partBody = current.slice(idx + 4);
93
+
94
+ const headerLines = [];
95
+ for (const [k, v] of msg.headers || []) headerLines.push(foldHeader(k, v));
96
+ headerLines.push('MIME-Version: 1.0');
97
+ for (const l of partHeaders.split('\r\n')) if (l) headerLines.push(l);
98
+
99
+ return utf8Encode(headerLines.join('\r\n') + '\r\n\r\n' + partBody);
100
+ }
@@ -0,0 +1,188 @@
1
+ /** Base64, quoted-printable and charset helpers. */
2
+
3
+ const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
4
+
5
+ /**
6
+ * @param {Uint8Array} bytes
7
+ * @returns {string}
8
+ */
9
+ export function bytesToBase64(bytes) {
10
+ let out = '';
11
+ let i;
12
+ const len = bytes.length;
13
+ for (i = 0; i + 2 < len; i += 3) {
14
+ const n = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
15
+ out += B64[(n >> 18) & 63] + B64[(n >> 12) & 63] + B64[(n >> 6) & 63] + B64[n & 63];
16
+ }
17
+ const rem = len - i;
18
+ if (rem === 1) {
19
+ const a = bytes[i] << 16;
20
+ out += B64[(a >> 18) & 63] + B64[(a >> 12) & 63] + '==';
21
+ } else if (rem === 2) {
22
+ const b = (bytes[i] << 16) | (bytes[i + 1] << 8);
23
+ out += B64[(b >> 18) & 63] + B64[(b >> 12) & 63] + B64[(b >> 6) & 63] + '=';
24
+ }
25
+ return out;
26
+ }
27
+
28
+ /**
29
+ * Tolerant base64 decoder: ignores whitespace and any non-alphabet bytes.
30
+ * @param {string} str
31
+ * @returns {Uint8Array}
32
+ */
33
+ export function base64ToBytes(str) {
34
+ let clean = String(str).replace(/[^A-Za-z0-9+/=]/g, '');
35
+ const pad = clean.indexOf('=');
36
+ if (pad >= 0) clean = clean.slice(0, pad);
37
+ const len = clean.length;
38
+ const out = new Uint8Array(Math.floor((len * 3) / 4));
39
+ let p = 0;
40
+ let buf = 0;
41
+ let bits = 0;
42
+ for (let i = 0; i < len; i++) {
43
+ const v = B64.indexOf(clean[i]);
44
+ if (v < 0) continue;
45
+ buf = (buf << 6) | v;
46
+ bits += 6;
47
+ if (bits >= 8) {
48
+ bits -= 8;
49
+ out[p++] = (buf >> bits) & 0xFF;
50
+ }
51
+ }
52
+ return out.subarray(0, p);
53
+ }
54
+
55
+ /**
56
+ * @param {string} str
57
+ * @param {number} width
58
+ * @returns {string}
59
+ */
60
+ export function wrap(str, width) {
61
+ const out = [];
62
+ for (let i = 0; i < str.length; i += width) out.push(str.slice(i, i + width));
63
+ return out.join('\r\n');
64
+ }
65
+
66
+ const encoder = new TextEncoder();
67
+
68
+ /**
69
+ * @param {string} s
70
+ * @returns {Uint8Array}
71
+ */
72
+ export function utf8Encode(s) {
73
+ return encoder.encode(s);
74
+ }
75
+
76
+ /**
77
+ * Decode bytes using a named charset, falling back to windows-1252 for
78
+ * labels the runtime does not know. Real-world mail mislabels constantly,
79
+ * so this never throws.
80
+ * @param {Uint8Array} bytes
81
+ * @param {string} [charset]
82
+ * @returns {string}
83
+ */
84
+ export function decodeBytes(bytes, charset) {
85
+ let cs = (charset || 'utf-8').toLowerCase().replace(/^["']|["']$/g, '');
86
+ if (cs === 'us-ascii' || cs === 'ascii' || cs === 'ansi_x3.4-1968') cs = 'utf-8';
87
+ if (cs === 'cp1252' || cs === 'win-1252') cs = 'windows-1252';
88
+ if (cs === 'utf8') cs = 'utf-8';
89
+ try {
90
+ return new TextDecoder(cs, { fatal: false }).decode(bytes);
91
+ } catch {
92
+ try {
93
+ return new TextDecoder('windows-1252').decode(bytes);
94
+ } catch {
95
+ let s = '';
96
+ for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
97
+ return s;
98
+ }
99
+ }
100
+ }
101
+
102
+ /**
103
+ * @param {Uint8Array} bytes
104
+ * @returns {string}
105
+ */
106
+ export function utf16Decode(bytes) {
107
+ try {
108
+ return new TextDecoder('utf-16le').decode(bytes);
109
+ } catch {
110
+ let s = '';
111
+ for (let i = 0; i + 1 < bytes.length; i += 2) {
112
+ s += String.fromCharCode(bytes[i] | (bytes[i + 1] << 8));
113
+ }
114
+ return s;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * @param {string} str
120
+ * @returns {Uint8Array}
121
+ */
122
+ export function utf16Encode(str) {
123
+ const out = new Uint8Array(str.length * 2);
124
+ for (let i = 0; i < str.length; i++) {
125
+ const c = str.charCodeAt(i);
126
+ out[i * 2] = c & 0xFF;
127
+ out[i * 2 + 1] = c >> 8;
128
+ }
129
+ return out;
130
+ }
131
+
132
+ /**
133
+ * @param {string} str
134
+ * @param {boolean} [isHeaderWord] Treat `_` as space (RFC 2047 Q encoding).
135
+ * @returns {Uint8Array}
136
+ */
137
+ export function decodeQP(str, isHeaderWord) {
138
+ const out = [];
139
+ let i = 0;
140
+ while (i < str.length) {
141
+ const ch = str[i];
142
+ if (ch === '=') {
143
+ if (str[i + 1] === '\r' && str[i + 2] === '\n') { i += 3; continue; }
144
+ if (str[i + 1] === '\n') { i += 2; continue; }
145
+ const hex = str.substr(i + 1, 2);
146
+ if (/^[0-9A-Fa-f]{2}$/.test(hex)) { out.push(parseInt(hex, 16)); i += 3; continue; }
147
+ out.push(61);
148
+ i++;
149
+ } else if (isHeaderWord && ch === '_') {
150
+ out.push(32);
151
+ i++;
152
+ } else {
153
+ out.push(str.charCodeAt(i) & 0xFF);
154
+ i++;
155
+ }
156
+ }
157
+ return new Uint8Array(out);
158
+ }
159
+
160
+ /**
161
+ * @param {Uint8Array} bytes
162
+ * @returns {string}
163
+ */
164
+ export function encodeQP(bytes) {
165
+ let out = '';
166
+ let lineLen = 0;
167
+ function push(tok) {
168
+ if (lineLen + tok.length > 75) { out += '=\r\n'; lineLen = 0; }
169
+ out += tok;
170
+ lineLen += tok.length;
171
+ }
172
+ const hex = (b) => '=' + ('0' + b.toString(16).toUpperCase()).slice(-2);
173
+ for (let i = 0; i < bytes.length; i++) {
174
+ const b = bytes[i];
175
+ if (b === 13 && bytes[i + 1] === 10) { out += '\r\n'; lineLen = 0; i++; continue; }
176
+ if (b === 10) { out += '\r\n'; lineLen = 0; continue; }
177
+ if (b === 32 || b === 9) {
178
+ const next = bytes[i + 1];
179
+ // Whitespace at end of line must be encoded or it will be stripped.
180
+ if (next === 13 || next === 10 || next === undefined) push(hex(b));
181
+ else push(String.fromCharCode(b));
182
+ continue;
183
+ }
184
+ if (b === 61 || b < 32 || b > 126) push(hex(b));
185
+ else push(String.fromCharCode(b));
186
+ }
187
+ return out;
188
+ }