@social-mail/shared 1.0.3

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 (69) hide show
  1. package/.gitlab-ci.yml +16 -0
  2. package/.vscode/launch.json +21 -0
  3. package/.vscode/settings.json +55 -0
  4. package/README.md +3 -0
  5. package/dist/QueryIterator.js +28 -0
  6. package/dist/QueryIterator.js.map +1 -0
  7. package/dist/mime-parser/AttachmentFile.js +21 -0
  8. package/dist/mime-parser/AttachmentFile.js.map +1 -0
  9. package/dist/mime-parser/HeaderContentDisposition.js +32 -0
  10. package/dist/mime-parser/HeaderContentDisposition.js.map +1 -0
  11. package/dist/mime-parser/HeaderContentType.js +33 -0
  12. package/dist/mime-parser/HeaderContentType.js.map +1 -0
  13. package/dist/mime-parser/MimeMessage.js +99 -0
  14. package/dist/mime-parser/MimeMessage.js.map +1 -0
  15. package/dist/mime-parser/MimeNode.js +406 -0
  16. package/dist/mime-parser/MimeNode.js.map +1 -0
  17. package/dist/mime-parser/encoder/RawBuffer.js +57 -0
  18. package/dist/mime-parser/encoder/RawBuffer.js.map +1 -0
  19. package/dist/mime-parser/encoder/base64-to-blob.js +30 -0
  20. package/dist/mime-parser/encoder/base64-to-blob.js.map +1 -0
  21. package/dist/mime-parser/encoder/quoted-printable.js +71 -0
  22. package/dist/mime-parser/encoder/quoted-printable.js.map +1 -0
  23. package/dist/mime-parser/encoder/word-encoding.js +45 -0
  24. package/dist/mime-parser/encoder/word-encoding.js.map +1 -0
  25. package/dist/mime-parser/parsePairs.js +40 -0
  26. package/dist/mime-parser/parsePairs.js.map +1 -0
  27. package/dist/mime-parser/stream/LineStream.js +99 -0
  28. package/dist/mime-parser/stream/LineStream.js.map +1 -0
  29. package/dist/mime-parser/stream/TextWriter.js +36 -0
  30. package/dist/mime-parser/stream/TextWriter.js.map +1 -0
  31. package/dist/mime-parser/tokenizer.js +46 -0
  32. package/dist/mime-parser/tokenizer.js.map +1 -0
  33. package/dist/tests/mime/lines-test.js +19 -0
  34. package/dist/tests/mime/lines-test.js.map +1 -0
  35. package/dist/tests/mime/message1.js +58 -0
  36. package/dist/tests/mime/message1.js.map +1 -0
  37. package/dist/tests/mime/message2.js +77 -0
  38. package/dist/tests/mime/message2.js.map +1 -0
  39. package/dist/tests/mime/pairs.js +33 -0
  40. package/dist/tests/mime/pairs.js.map +1 -0
  41. package/dist/tests/mime/parse-headers.js +104 -0
  42. package/dist/tests/mime/parse-headers.js.map +1 -0
  43. package/dist/tests/mime/word-encoding-test.js +21 -0
  44. package/dist/tests/mime/word-encoding-test.js.map +1 -0
  45. package/dist/tsconfig.tsbuildinfo +1 -0
  46. package/index.js +4 -0
  47. package/package.json +17 -0
  48. package/src/QueryIterator.js +33 -0
  49. package/src/mime-parser/AttachmentFile.js +29 -0
  50. package/src/mime-parser/HeaderContentDisposition.js +32 -0
  51. package/src/mime-parser/HeaderContentType.js +34 -0
  52. package/src/mime-parser/MimeMessage.js +100 -0
  53. package/src/mime-parser/MimeNode.js +435 -0
  54. package/src/mime-parser/encoder/RawBuffer.js +60 -0
  55. package/src/mime-parser/encoder/base64-to-blob.js +26 -0
  56. package/src/mime-parser/encoder/quoted-printable.js +118 -0
  57. package/src/mime-parser/encoder/word-encoding.js +43 -0
  58. package/src/mime-parser/parsePairs.js +34 -0
  59. package/src/mime-parser/stream/LineStream.js +85 -0
  60. package/src/mime-parser/stream/TextWriter.js +27 -0
  61. package/src/mime-parser/tokenizer.js +37 -0
  62. package/src/tests/mime/lines-test.js +17 -0
  63. package/src/tests/mime/message1.js +46 -0
  64. package/src/tests/mime/message2.js +73 -0
  65. package/src/tests/mime/pairs.js +38 -0
  66. package/src/tests/mime/parse-headers.js +90 -0
  67. package/src/tests/mime/word-encoding-test.js +13 -0
  68. package/test.js +70 -0
  69. package/tsconfig.json +16 -0
@@ -0,0 +1,33 @@
1
+ export default class QueryIterator {
2
+
3
+ /**
4
+ * @template T
5
+ * @returns {T}
6
+ */
7
+ static first(
8
+ /** @type {IterableIterator<T>} */ source,
9
+ /** @type {(item: T) => boolean} */ fx
10
+ ) {
11
+ for (const iterator of source) {
12
+ if (fx(iterator)) {
13
+ return iterator;
14
+ }
15
+ }
16
+ }
17
+
18
+ /**
19
+ * @template T
20
+ * @returns {T | null}
21
+ */
22
+ firstOrFail(
23
+ /** @type {IterableIterator<T>} */ source,
24
+ /** @type {(item: T) => boolean} */ fx
25
+ ) {
26
+ for (const iterator of source) {
27
+ if (fx(iterator)) {
28
+ return iterator;
29
+ }
30
+ }
31
+
32
+ }
33
+ }
@@ -0,0 +1,29 @@
1
+ import { MimeNode } from "./MimeNode.js";
2
+
3
+
4
+
5
+ export class AttachmentFile extends File {
6
+
7
+ /** @type {"inline" | "attachment"} */
8
+ disposition;
9
+
10
+ /** @type {string} */
11
+ contentId;
12
+
13
+ /**
14
+ * Associated mime node if any
15
+ * @type {MimeNode}
16
+ */
17
+ node;
18
+
19
+ constructor(
20
+ /** @type {BlobPart[]} */ fileBits,
21
+ /** @type {string} */ fileName,
22
+ /** @type {FilePropertyBag & { disposition?: "inline" | "attachment"; contentId?: string; }} */
23
+ options) {
24
+ super(fileBits, fileName, options);
25
+ this.contentId = options.contentId;
26
+ this.disposition = options.disposition;
27
+ }
28
+
29
+ }
@@ -0,0 +1,32 @@
1
+ import { wordEncoding } from "./encoder/word-encoding.js";
2
+ import { parsePairs } from "./parsePairs.js";
3
+
4
+
5
+ export class HeaderContentDisposition {
6
+
7
+ /** @type {string} */ type;
8
+ /** @type {string} */ filename;
9
+ /** @type {string} */ name;
10
+
11
+ constructor(
12
+ /** @type {string} */ type,
13
+ /** @type {string} */ filename,
14
+ /** @type {string} */ name
15
+ ) {
16
+ this.type = type;
17
+ this.filename = filename;
18
+ this.name = name;
19
+ }
20
+
21
+ parse(/** @type {string} */ text) {
22
+ const items = parsePairs(text, "type");
23
+ this.type = items.type;
24
+ this.filename = items.filename;
25
+ this.name = items.name;
26
+ }
27
+
28
+ toString() {
29
+ return `${this.type};\r\n\t\tfilename="${wordEncoding.encode( this.filename)}";`;
30
+ }
31
+
32
+ }
@@ -0,0 +1,34 @@
1
+ import { parsePairs } from "./parsePairs.js";
2
+
3
+
4
+ export class HeaderContentType {
5
+
6
+ /** @type {string} */ type;
7
+ /** @type {string} */ charset;
8
+ /** @type {string} */ boundary;
9
+
10
+ constructor(
11
+ /** @type {string} */ type,
12
+ /** @type {string} */ charset = "UTF-8",
13
+ /** @type {string} */ boundary
14
+ ) {
15
+ this.type = type;
16
+ this.charset = charset;
17
+ this.boundary = boundary;
18
+ }
19
+
20
+ parse( /** @type {string} */ text) {
21
+ const items = parsePairs(text, "type");
22
+ this.type = items.type;
23
+ this.charset = items.charset;
24
+ this.boundary = items.boundary;
25
+ }
26
+
27
+ toString() {
28
+ if (this.boundary) {
29
+ return `${this.type}; charset="${this.charset}"; boundary="${this.boundary}";`;
30
+ }
31
+ return `${this.type}; charset="${this.charset}";`;
32
+ }
33
+ }
34
+
@@ -0,0 +1,100 @@
1
+ import { MimeNode } from "./MimeNode.js";
2
+ import { AttachmentFile } from "./AttachmentFile.js";
3
+ import { ReadableLineStream, StringLineStream } from "./stream/LineStream.js";
4
+ import { BlobWriter } from "./stream/TextWriter.js";
5
+
6
+ export default class MimeMessage {
7
+
8
+ static async load(
9
+ /** @type {ReadableStream | string} */ reader,
10
+ /** @type {string} */ name,
11
+ /** @type {boolean} */ inline
12
+ ) {
13
+ const node = new MimeNode();
14
+ if (name) {
15
+ node.setHeader("X-Name", name);
16
+ }
17
+ await node.parse( typeof reader === "string"
18
+ ? new StringLineStream(reader)
19
+ : new ReadableLineStream(reader));
20
+ const attachments = await node.attachments();
21
+
22
+ // we should inline the images and remove them from attachments array for simplicity
23
+ // this should be done when templates are imported from somewhere else
24
+
25
+ const msg = new MimeMessage(node, attachments);
26
+ if (inline) {
27
+ await msg.inlineImages();
28
+ }
29
+ return msg;
30
+ }
31
+
32
+ get name() {
33
+ return this.node.header("x-name");
34
+ }
35
+
36
+ get html() {
37
+ const alternative = this.node.getFirstChild("multipart/alternative");
38
+ return alternative?.getFirstChild("text/html")?.text ?? "";
39
+ }
40
+
41
+ set html(/** @type {string} */ v) {
42
+ const alternative = this.node.getFirstChild("multipart/alternative", true);
43
+ const htmlMime = alternative.getFirstChild("text/html", true);
44
+ htmlMime.contentTransferEncoding = "quoted-printable";
45
+ htmlMime.text = v;
46
+ htmlMime.contentType.charset = "UTF-8";
47
+ }
48
+
49
+ get text() {
50
+ const alternative = this.node.getFirstChild("multipart/alternative");
51
+ return alternative?.getFirstChild("text/plain")?.text ?? "";
52
+ }
53
+
54
+ set text(/** @type {string} */ v) {
55
+ const alternative = this.node.getFirstChild("multipart/alternative", true);
56
+ const htmlMime = alternative.getFirstChild("text/plain", true);
57
+ htmlMime.contentTransferEncoding = "quoted-printable";
58
+ htmlMime.text = v;
59
+ htmlMime.contentType.charset = "UTF-8";
60
+ }
61
+
62
+ get subject() {
63
+ return this.node.header("subject") ?? "";
64
+ }
65
+
66
+ set subject(/** @type {string} */ v) {
67
+ this.node.setHeader("subject", v);
68
+ }
69
+
70
+ /** @type {MimeNode} */ node;
71
+ /** @type {AttachmentFile[]} */ attachments;
72
+
73
+ constructor(
74
+ /** @type {MimeNode} */ node = new MimeNode("multipart/mixed"),
75
+ /** @type {AttachmentFile[]} */ attachments = []) {
76
+ this.node = node;
77
+ this.attachments = attachments;
78
+ }
79
+
80
+ async save() {
81
+ const blobWriter = new BlobWriter();
82
+ await this.node.save(blobWriter);
83
+ return blobWriter.toBlob();
84
+ }
85
+
86
+ async inlineImages() {
87
+ let text = this.html;
88
+ if (!text) {
89
+ return;
90
+ }
91
+
92
+ for (const iterator of this.attachments) {
93
+ if (iterator.contentId) {
94
+ const encoded = iterator.node.encoded;
95
+ text = text.split(`cid:${iterator.contentId}`).join(`data://${iterator.type},${encoded}`);
96
+ }
97
+ }
98
+ }
99
+
100
+ }
@@ -0,0 +1,435 @@
1
+ /* eslint-disable @typescript-eslint/no-redundant-type-constituents */
2
+ // import QueryIterator from "../../common/QueryIterator";
3
+ import { AttachmentFile } from "./AttachmentFile.js";
4
+ import { HeaderContentType } from "./HeaderContentType.js";
5
+ import { HeaderContentDisposition } from "./HeaderContentDisposition.js";
6
+ import { RawBuffer } from "./encoder/RawBuffer.js";
7
+ import { base64toBlob } from "./encoder/base64-to-blob.js";
8
+ import { quotedPrintable } from "./encoder/quoted-printable.js";
9
+ import { wordEncoding } from "./encoder/word-encoding.js";
10
+ import LineStream from "./stream/LineStream.js";
11
+ import TextWriter from "./stream/TextWriter.js";
12
+
13
+ // export interface IContentTypeObject {
14
+ // type: string;
15
+ // boundary?: string;
16
+ // charset?: string
17
+ // };
18
+
19
+ // export type IContentType = string | IContentTypeObject;
20
+
21
+ // export interface IContentDispositionObject {
22
+ // type: string;
23
+ // filename?: string;
24
+ // };
25
+
26
+ // export type IContentDisposition = string | IContentDispositionObject;
27
+
28
+
29
+
30
+ export class CIMap extends Map {
31
+
32
+ ci = new Map();
33
+
34
+ set(key, value) {
35
+ if (typeof key === "string") {
36
+ let ck = this.ci.get(key);
37
+ if (ck === void 0) {
38
+ ck = key.toLocaleLowerCase();
39
+ this.ci.set(ck, key);
40
+ } else {
41
+ key = ck;
42
+ }
43
+ }
44
+ return super.set(key, value);
45
+ }
46
+
47
+ get(key) {
48
+ if (typeof key === "string") {
49
+ const ck = key.toLocaleLowerCase();
50
+ key = this.ci.get(ck);
51
+ }
52
+ return super.get(key);
53
+ }
54
+
55
+ }
56
+
57
+
58
+ export class MimeNode {
59
+
60
+ static createMessage({
61
+ html,
62
+ subject,
63
+ text = "",
64
+ name = "",
65
+ attachments = []
66
+ }) {
67
+ const root = new MimeNode("multipart/mixed");
68
+ root.setHeader("Subject", subject);
69
+ if (name) {
70
+ root.setHeader("X-Name", name);
71
+ }
72
+ const body = new MimeNode("multipart/alternative");
73
+ body.children = [];
74
+ if (html) {
75
+ body.children.push(this.create(html, "text/html"));
76
+ }
77
+ if (text) {
78
+ body.children.push(this.create(text, "text/plain"));
79
+ }
80
+ root.children = [body];
81
+ if (attachments) {
82
+ for (const iterator of attachments) {
83
+ root.children.push(this.create(iterator));
84
+ }
85
+ }
86
+ return root;
87
+ }
88
+
89
+ static create(
90
+ /** @type {string | File} */ text,
91
+ /** @type {string} */ type,
92
+ /** @type {string} */id) {
93
+ if (typeof text === "string") {
94
+ const node = new MimeNode(type);
95
+ node.text = text;
96
+ return node;
97
+ }
98
+ const fileNode = new MimeNode(text.type);
99
+ fileNode.blobData = text;
100
+ fileNode.contentDisposition = new HeaderContentDisposition(type || "attachment");
101
+ fileNode.contentDisposition.filename = text.name;
102
+ fileNode.setHeader("Content-Disposition", fileNode.contentDisposition);
103
+ if (id) {
104
+ fileNode.setHeader("Content-Id", id);
105
+ }
106
+ return fileNode;
107
+ }
108
+
109
+ /** @type {string} */ encoded;
110
+
111
+ /** @type {MimeNode[]} */ children;
112
+
113
+ /** @type {HeaderContentType} */ contentType;
114
+
115
+ /** @type {HeaderContentDisposition} */ contentDisposition;
116
+
117
+ /** @returns {string} */
118
+ get text() {
119
+ let { encoded } = this;
120
+ if (!encoded) {
121
+ return;
122
+ }
123
+ // we need to convert byte array to utf8
124
+ const cte = this.contentTransferEncoding?.toLowerCase();
125
+ switch (cte) {
126
+ case "quoted-printable":
127
+ encoded = quotedPrintable.decode(encoded);
128
+ break;
129
+ case "base64":
130
+ encoded = btoa(encoded);
131
+ break;
132
+ }
133
+ return this.textContent ??= RawBuffer.decode(encoded, this.contentType?.charset);
134
+ }
135
+
136
+ set text( /** @type {string} */ v) {
137
+ this.contentTransferEncoding = "quoted-printable";
138
+ this.textContent = v;
139
+ }
140
+
141
+ /** @returns {Blob} */
142
+ get blob() {
143
+ // this will create blob..
144
+ if (this.contentTransferEncoding?.toLocaleLowerCase() !== "base64") {
145
+ throw new Error("Not supported");
146
+ }
147
+ return this.blobData ??= base64toBlob(this.encoded, this.contentType.type);
148
+ }
149
+
150
+ set blob( /** @type {Blob} */ v) {
151
+ this.blobData = v;
152
+ }
153
+
154
+ /** @returns {string} */
155
+ get contentTransferEncoding() {
156
+ return this.header("Content-Transfer-Encoding");
157
+ }
158
+
159
+ set contentTransferEncoding( /** @type {string} */ v) {
160
+ this.setHeader("Content-Transfer-Encoding", v);
161
+ }
162
+
163
+ /** @returns {string} */
164
+ get boundary() {
165
+ return this.contentType.boundary;
166
+ }
167
+
168
+ /** @returns {IterableIterator<MimeNode>} */
169
+ get descendent() {
170
+ return this.enumerate();
171
+ }
172
+
173
+ /** @type {Blob} */ blobData;
174
+ /** @type {string} */ textContent;
175
+
176
+ /** @type {Map<string, string | any>} */ headers;
177
+
178
+ constructor( /** @type {string} */ type) {
179
+ this.headers = new CIMap();
180
+ this.contentType = new HeaderContentType(type);
181
+ this.headers.set("Content-Type", this.contentType);
182
+ }
183
+
184
+ getFirstChild(
185
+ /** @type {string} */ contentType,
186
+ create = false
187
+ ) {
188
+ contentType = contentType.toLocaleLowerCase();
189
+ let child = QueryIterator.first(this.descendent, (x) => x.contentType.type === contentType);
190
+ if (!child && create) {
191
+ child = new MimeNode(contentType);
192
+ (this.children ??= []).push(child);
193
+ }
194
+ return child;
195
+ }
196
+
197
+ header(/** @type {string} */ name) {
198
+ return this.headers.get(name);
199
+ }
200
+
201
+ setHeader(
202
+ /** @type {string} */name,
203
+ /** @type {string} */ value
204
+ ) {
205
+ // name = name.toLocaleLowerCase();
206
+ // for (const [key] of this.headers.keys()) {
207
+ // if (key.toLocaleLowerCase() === name) {
208
+ // this.headers.set(key, value);
209
+ // return;
210
+ // }
211
+ // }
212
+
213
+ // // change name...
214
+ // switch(name.toLocaleLowerCase()) {
215
+ // case "content-type":
216
+ // name = "Content-Type";
217
+ // break;
218
+ // case "content-transfer-encoding":
219
+ // name = "Content-Transfer-Encoding";
220
+ // break;
221
+ // case "content-disposition":
222
+ // name = "Content-Disposition";
223
+ // break;
224
+ // }
225
+
226
+ this.headers.set(name, value);
227
+ }
228
+
229
+ async asFile() {
230
+ const text = this.encoded.split("\n").map((x) => x.trim()).join("");
231
+ const url = `data:${this.contentType.type};base64,${text}`;
232
+ const blob = await fetch(url);
233
+ const af = new AttachmentFile(
234
+ [await blob.blob()],
235
+ this.contentDisposition.filename,
236
+ {
237
+ type: this.contentType.type,
238
+ contentId: this.header("Content-Id"),
239
+ disposition: this.contentDisposition.type
240
+ });
241
+ af.node = this;
242
+ return af;
243
+ }
244
+
245
+ async attachments(
246
+ /** @type {AttachmentFile[]} */ files = []) {
247
+ for (const iterator of this.children) {
248
+ if (iterator.children) {
249
+ await iterator.attachments(files);
250
+ continue;
251
+ }
252
+ if (iterator.contentDisposition?.type) {
253
+ files.push(await iterator.asFile());
254
+ }
255
+ }
256
+ return files;
257
+ }
258
+
259
+ async parse(
260
+ /** @type {LineStream} */ lines,
261
+ last = ""
262
+ ) {
263
+
264
+ await this.parseHeaders(lines);
265
+
266
+ let boundary = this.contentType.boundary;
267
+ if (!boundary) {
268
+ const data = [];
269
+
270
+ // parse data till end of the boundary
271
+ for await (const line of lines.read()) {
272
+ const trimmed = line.trimEnd();
273
+ if (trimmed.length === 0) {
274
+ break;
275
+ }
276
+ if (trimmed === last) {
277
+ break;
278
+ }
279
+ data.push(line);
280
+ }
281
+
282
+ this.encoded = data.join("\n");
283
+ return;
284
+ }
285
+
286
+ boundary = "--" + boundary;
287
+ const end = boundary + "--";
288
+
289
+ for (; ;) {
290
+ let boundaryFound = false;
291
+ // skip till boundary
292
+ for await (const line of lines.read()) {
293
+ if (line === boundary) {
294
+ boundaryFound = true;
295
+ break;
296
+ }
297
+ if (line === end) {
298
+ return;
299
+ }
300
+ }
301
+
302
+ if (!boundaryFound) {
303
+ break;
304
+ }
305
+
306
+ // begin a new child..
307
+ const child = new MimeNode("");
308
+ await child.parse(lines, end);
309
+
310
+ (this.children ??= []).push(child);
311
+ }
312
+ }
313
+
314
+ async parseHeaders(
315
+ /** @type {LineStream} */ lines
316
+ ) {
317
+
318
+ let headerName = "";
319
+ let headerValue = "";
320
+
321
+ for await (const iterator of lines.read()) {
322
+ const value = iterator.trim();
323
+ if (value.length === 0) {
324
+ break;
325
+ }
326
+ if (/^[\x20\t]/.test(iterator)) {
327
+ headerValue += wordEncoding.decode(value);
328
+ continue;
329
+ }
330
+ if (headerValue) {
331
+ this.setHeader(headerName, headerValue);
332
+ }
333
+ const index = value.indexOf(":");
334
+ headerName = value.substring(0, index);
335
+ headerValue = wordEncoding.decode(value.substring(index+1).trim());
336
+ }
337
+
338
+ if (headerValue) {
339
+ this.setHeader(headerName, headerValue);
340
+ }
341
+
342
+ const ct = this.header("Content-Type");
343
+ this.contentType.parse(ct);
344
+ this.setHeader("Content-Type", ct);
345
+
346
+ const cd = this.header("Content-Disposition");
347
+ if (cd) {
348
+ this.contentDisposition ??= new HeaderContentDisposition();
349
+ this.contentDisposition.parse(cd);
350
+ this.setHeader("Content-Disposition", cd);
351
+ }
352
+ }
353
+
354
+ async save(/** @type {TextWriter} */ writer) {
355
+
356
+ if (this.textContent) {
357
+ this.contentType.charset = "UTF-8";
358
+ }
359
+
360
+ if (this.children?.length) {
361
+ this.contentType.boundary ||= `${Date.now()}-${Date.now()}-${Date.now()}`;
362
+ }
363
+
364
+ for (const [key, element] of this.headers.entries()) {
365
+ switch(key) {
366
+ case "content-disposition":
367
+ writer.writeLine(`Content-Disposition: ${this.contentDisposition}`);
368
+ continue;
369
+ case "content-type":
370
+ writer.writeLine(`Content-Type:${this.contentType.toString()}`);
371
+ continue;
372
+ case "subject":
373
+ writer.writeLine(`${key}: ${wordEncoding.encode(element, true)}`);
374
+ continue;
375
+ }
376
+ writer.writeLine(`${key}: ${element}`);
377
+ }
378
+
379
+ writer.writeLine("");
380
+
381
+ // check if we have any children...
382
+ if (this.children?.length) {
383
+
384
+ const boundary = `--${this.contentType.boundary}`;
385
+
386
+ for (const iterator of this.children) {
387
+ writer.writeLine(boundary);
388
+ await iterator.save(writer);
389
+ writer.writeLine("");
390
+ }
391
+
392
+ writer.writeLine(boundary + "--");
393
+
394
+
395
+ return;
396
+ }
397
+
398
+ // check if we have text or binary
399
+ if (this.blobData) {
400
+ // convert to base64
401
+ const base64 = await RawBuffer.toBase64Async(this.blobData);
402
+ let start = 0;
403
+ const max = 80;
404
+ for (;;) {
405
+ if ( start + max > base64.length) {
406
+ writer.writeLine(base64.substring(start));
407
+ break;
408
+ }
409
+ writer.writeLine(base64.substring(start, start + max));
410
+ start += max;
411
+ }
412
+ writer.writeLine("");
413
+ return;
414
+ }
415
+
416
+ let text = this.text;
417
+ if (this.contentTransferEncoding === "quoted-printable") {
418
+ text = quotedPrintable.encode( RawBuffer.encode(text));
419
+ }
420
+ writer.writeLine(RawBuffer.encode(text));
421
+ writer.writeLine("");
422
+ return;
423
+ }
424
+
425
+ /** @returns {IterableIterator<MimeNode>} */
426
+ *enumerate() {
427
+ yield this;
428
+ if (!this.children) {
429
+ return;
430
+ }
431
+ for (const iterator of this.children) {
432
+ yield* iterator.enumerate();
433
+ }
434
+ }
435
+ }