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.
- package/LICENSE +21 -0
- package/README.md +203 -0
- package/dist/mailfile.cjs +2035 -0
- package/dist/mailfile.js +2037 -0
- package/dist/mailfile.min.js +30 -0
- package/package.json +84 -0
- package/src/cfb/index.js +4 -0
- package/src/cfb/read.js +221 -0
- package/src/cfb/write.js +262 -0
- package/src/errors.js +49 -0
- package/src/index.js +86 -0
- package/src/mapi/bag.js +195 -0
- package/src/mapi/index.js +6 -0
- package/src/mapi/tags.js +147 -0
- package/src/message.js +199 -0
- package/src/mime/build.js +100 -0
- package/src/mime/encodings.js +188 -0
- package/src/mime/headers.js +344 -0
- package/src/mime/index.js +12 -0
- package/src/mime/parse.js +153 -0
- package/src/msg/read.js +178 -0
- package/src/msg/write.js +124 -0
- package/src/rtf/deencapsulate.js +155 -0
- package/src/rtf/index.js +3 -0
- package/src/rtf/lzfu.js +67 -0
- package/src/util.js +59 -0
- package/types/cfb/index.d.ts +2 -0
- package/types/cfb/read.d.ts +70 -0
- package/types/cfb/write.d.ts +67 -0
- package/types/errors.d.ts +36 -0
- package/types/index.d.ts +73 -0
- package/types/mapi/bag.d.ts +77 -0
- package/types/mapi/index.d.ts +2 -0
- package/types/mapi/tags.d.ts +137 -0
- package/types/message.d.ts +94 -0
- package/types/mime/build.d.ts +27 -0
- package/types/mime/encodings.d.ts +52 -0
- package/types/mime/headers.d.ts +111 -0
- package/types/mime/index.d.ts +4 -0
- package/types/mime/parse.d.ts +43 -0
- package/types/msg/read.d.ts +26 -0
- package/types/msg/write.d.ts +6 -0
- package/types/rtf/deencapsulate.d.ts +23 -0
- package/types/rtf/index.d.ts +2 -0
- package/types/rtf/lzfu.d.ts +8 -0
- package/types/util.d.ts +25 -0
package/src/mapi/tags.js
ADDED
|
@@ -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
|
+
}
|