@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.
- package/.gitlab-ci.yml +16 -0
- package/.vscode/launch.json +21 -0
- package/.vscode/settings.json +55 -0
- package/README.md +3 -0
- package/dist/QueryIterator.js +28 -0
- package/dist/QueryIterator.js.map +1 -0
- package/dist/mime-parser/AttachmentFile.js +21 -0
- package/dist/mime-parser/AttachmentFile.js.map +1 -0
- package/dist/mime-parser/HeaderContentDisposition.js +32 -0
- package/dist/mime-parser/HeaderContentDisposition.js.map +1 -0
- package/dist/mime-parser/HeaderContentType.js +33 -0
- package/dist/mime-parser/HeaderContentType.js.map +1 -0
- package/dist/mime-parser/MimeMessage.js +99 -0
- package/dist/mime-parser/MimeMessage.js.map +1 -0
- package/dist/mime-parser/MimeNode.js +406 -0
- package/dist/mime-parser/MimeNode.js.map +1 -0
- package/dist/mime-parser/encoder/RawBuffer.js +57 -0
- package/dist/mime-parser/encoder/RawBuffer.js.map +1 -0
- package/dist/mime-parser/encoder/base64-to-blob.js +30 -0
- package/dist/mime-parser/encoder/base64-to-blob.js.map +1 -0
- package/dist/mime-parser/encoder/quoted-printable.js +71 -0
- package/dist/mime-parser/encoder/quoted-printable.js.map +1 -0
- package/dist/mime-parser/encoder/word-encoding.js +45 -0
- package/dist/mime-parser/encoder/word-encoding.js.map +1 -0
- package/dist/mime-parser/parsePairs.js +40 -0
- package/dist/mime-parser/parsePairs.js.map +1 -0
- package/dist/mime-parser/stream/LineStream.js +99 -0
- package/dist/mime-parser/stream/LineStream.js.map +1 -0
- package/dist/mime-parser/stream/TextWriter.js +36 -0
- package/dist/mime-parser/stream/TextWriter.js.map +1 -0
- package/dist/mime-parser/tokenizer.js +46 -0
- package/dist/mime-parser/tokenizer.js.map +1 -0
- package/dist/tests/mime/lines-test.js +19 -0
- package/dist/tests/mime/lines-test.js.map +1 -0
- package/dist/tests/mime/message1.js +58 -0
- package/dist/tests/mime/message1.js.map +1 -0
- package/dist/tests/mime/message2.js +77 -0
- package/dist/tests/mime/message2.js.map +1 -0
- package/dist/tests/mime/pairs.js +33 -0
- package/dist/tests/mime/pairs.js.map +1 -0
- package/dist/tests/mime/parse-headers.js +104 -0
- package/dist/tests/mime/parse-headers.js.map +1 -0
- package/dist/tests/mime/word-encoding-test.js +21 -0
- package/dist/tests/mime/word-encoding-test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/index.js +4 -0
- package/package.json +17 -0
- package/src/QueryIterator.js +33 -0
- package/src/mime-parser/AttachmentFile.js +29 -0
- package/src/mime-parser/HeaderContentDisposition.js +32 -0
- package/src/mime-parser/HeaderContentType.js +34 -0
- package/src/mime-parser/MimeMessage.js +100 -0
- package/src/mime-parser/MimeNode.js +435 -0
- package/src/mime-parser/encoder/RawBuffer.js +60 -0
- package/src/mime-parser/encoder/base64-to-blob.js +26 -0
- package/src/mime-parser/encoder/quoted-printable.js +118 -0
- package/src/mime-parser/encoder/word-encoding.js +43 -0
- package/src/mime-parser/parsePairs.js +34 -0
- package/src/mime-parser/stream/LineStream.js +85 -0
- package/src/mime-parser/stream/TextWriter.js +27 -0
- package/src/mime-parser/tokenizer.js +37 -0
- package/src/tests/mime/lines-test.js +17 -0
- package/src/tests/mime/message1.js +46 -0
- package/src/tests/mime/message2.js +73 -0
- package/src/tests/mime/pairs.js +38 -0
- package/src/tests/mime/parse-headers.js +90 -0
- package/src/tests/mime/word-encoding-test.js +13 -0
- package/test.js +70 -0
- 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
|
+
}
|