@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,60 @@
|
|
|
1
|
+
const toBuffer = (
|
|
2
|
+
/** @type {string} */ text
|
|
3
|
+
) => {
|
|
4
|
+
const bytes = new Uint8Array(text.length);
|
|
5
|
+
for (let index = 0; index < text.length; index++) {
|
|
6
|
+
bytes[index] = text.charCodeAt(index);
|
|
7
|
+
}
|
|
8
|
+
return bytes.buffer;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const RawBuffer = {
|
|
12
|
+
decode: (
|
|
13
|
+
/** @type {string} */ bytesAsString,
|
|
14
|
+
/** @type {string} */ encoding = "utf-8"
|
|
15
|
+
) => {
|
|
16
|
+
if (!encoding) {
|
|
17
|
+
return bytesAsString;
|
|
18
|
+
}
|
|
19
|
+
const te = new TextDecoder(encoding);
|
|
20
|
+
return te.decode(toBuffer(bytesAsString));
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
encode: (
|
|
24
|
+
/** @type {string} */ text,
|
|
25
|
+
/** @type {string} */ encoding = "utf-8"
|
|
26
|
+
) => {
|
|
27
|
+
if (!(/utf\-?8/i.test(encoding))) {
|
|
28
|
+
throw new Error(`Encoding ${encoding} not supported`);
|
|
29
|
+
}
|
|
30
|
+
const te = new TextEncoder();
|
|
31
|
+
const array = te.encode(text);
|
|
32
|
+
const a = [];
|
|
33
|
+
for (const iterator of array) {
|
|
34
|
+
a.push(String.fromCharCode(iterator));
|
|
35
|
+
}
|
|
36
|
+
return a.join("");
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
toBase64Async(
|
|
40
|
+
/** @type {Blob} */ blob,
|
|
41
|
+
asDataUrl = false
|
|
42
|
+
) {
|
|
43
|
+
return new Promise<string>((resolve, reject) => {
|
|
44
|
+
const reader = new FileReader();
|
|
45
|
+
reader.onerror = () => {
|
|
46
|
+
reject(reader.error);
|
|
47
|
+
};
|
|
48
|
+
reader.onload = () => {
|
|
49
|
+
const /** @type {string} */ text = reader.result;
|
|
50
|
+
if (asDataUrl) {
|
|
51
|
+
resolve(text);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const index = text.indexOf(",");
|
|
55
|
+
resolve(text.substring(index + 1));
|
|
56
|
+
};
|
|
57
|
+
reader.readAsDataURL(blob);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* @param {string} base64Data
|
|
4
|
+
* @param {string} contentType
|
|
5
|
+
* @returns
|
|
6
|
+
*/
|
|
7
|
+
export function base64toBlob(base64Data, contentType) {
|
|
8
|
+
contentType = contentType || '';
|
|
9
|
+
const sliceSize = 1024;
|
|
10
|
+
const byteCharacters = atob(base64Data);
|
|
11
|
+
const bytesLength = byteCharacters.length;
|
|
12
|
+
const slicesCount = Math.ceil(bytesLength / sliceSize);
|
|
13
|
+
const byteArrays = new Array(slicesCount);
|
|
14
|
+
|
|
15
|
+
for (let sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) {
|
|
16
|
+
const begin = sliceIndex * sliceSize;
|
|
17
|
+
const end = Math.min(begin + sliceSize, bytesLength);
|
|
18
|
+
|
|
19
|
+
const bytes = new Array(end - begin);
|
|
20
|
+
for (let offset = begin, i = 0; offset < end; ++i, ++offset) {
|
|
21
|
+
bytes[i] = byteCharacters[offset].charCodeAt(0);
|
|
22
|
+
}
|
|
23
|
+
byteArrays[sliceIndex] = new Uint8Array(bytes);
|
|
24
|
+
}
|
|
25
|
+
return new Blob(byteArrays, { type: contentType });
|
|
26
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Original Source Code: https://github.com/mathiasbynens/quoted-printable/blob/master/quoted-printable.js
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
const stringFromCharCode = String.fromCharCode;
|
|
5
|
+
const decode = function(input) {
|
|
6
|
+
return input
|
|
7
|
+
// https://tools.ietf.org/html/rfc2045#section-6.7, rule 3:
|
|
8
|
+
// “Therefore, when decoding a `Quoted-Printable` body, any trailing white
|
|
9
|
+
// space on a line must be deleted, as it will necessarily have been added
|
|
10
|
+
// by intermediate transport agents.”
|
|
11
|
+
.replace(/[\t\x20]$/gm, '')
|
|
12
|
+
// Remove hard line breaks preceded by `=`. Proper `Quoted-Printable`-
|
|
13
|
+
// encoded data only contains CRLF line endings, but for compatibility
|
|
14
|
+
// reasons we support separate CR and LF too.
|
|
15
|
+
.replace(/=(?:\r\n?|\n|$)/g, '')
|
|
16
|
+
// Decode escape sequences of the form `=XX` where `XX` is any
|
|
17
|
+
// combination of two hexidecimal digits. For optimal compatibility,
|
|
18
|
+
// lowercase hexadecimal digits are supported as well. See
|
|
19
|
+
// https://tools.ietf.org/html/rfc2045#section-6.7, note 1.
|
|
20
|
+
.replace(/=([a-fA-F0-9]{2})/g, function($0, $1) {
|
|
21
|
+
const codePoint = parseInt($1, 16);
|
|
22
|
+
return stringFromCharCode(codePoint);
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const handleTrailingCharacters =
|
|
27
|
+
function(
|
|
28
|
+
/** @type {string} */
|
|
29
|
+
text) {
|
|
30
|
+
return text
|
|
31
|
+
.replace(/\x20$/, '=20') // Handle trailing space.
|
|
32
|
+
.replace(/\t$/, '=09'); // Handle trailing tab.
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const regexUnsafeSymbols = /[\0-\x08\n-\x1F=\x7F-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
|
|
36
|
+
const encode = function(text) {
|
|
37
|
+
|
|
38
|
+
// Encode symbols that are definitely unsafe (i.e. unsafe in any context).
|
|
39
|
+
const encoded = text.replace(regexUnsafeSymbols, function(symbol) {
|
|
40
|
+
if (symbol > '\xFF') {
|
|
41
|
+
throw RangeError(
|
|
42
|
+
'`quotedPrintable.encode()` expects extended ASCII input only. ' +
|
|
43
|
+
'Don\u2019t forget to encode the input first using a character ' +
|
|
44
|
+
'encoding like UTF-8.'
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
const codePoint = symbol.charCodeAt(0);
|
|
48
|
+
const hexadecimal = codePoint.toString(16).toUpperCase();
|
|
49
|
+
return '=' + ('0' + hexadecimal).slice(-2);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const LINE_LENGTH = 75;
|
|
53
|
+
|
|
54
|
+
// Limit lines to 76 characters (not counting the CRLF line endings).
|
|
55
|
+
const lines = encoded.split(/\r\n?|\n/g);
|
|
56
|
+
let lineIndex = -1;
|
|
57
|
+
const lineCount = lines.length;
|
|
58
|
+
const result = [];
|
|
59
|
+
let buffer;
|
|
60
|
+
while (++lineIndex < lineCount) {
|
|
61
|
+
const line = lines[lineIndex];
|
|
62
|
+
// Leave room for the trailing `=` for soft line breaks.
|
|
63
|
+
let index = 0;
|
|
64
|
+
const length = line.length;
|
|
65
|
+
while (index < length) {
|
|
66
|
+
buffer = encoded.slice(index, index + LINE_LENGTH);
|
|
67
|
+
// If this line ends with `=`, optionally followed by a single uppercase
|
|
68
|
+
// hexadecimal digit, we broke an escape sequence in half. Fix it by
|
|
69
|
+
// moving these characters to the next line.
|
|
70
|
+
if (/=$/.test(buffer)) {
|
|
71
|
+
buffer = buffer.slice(0, LINE_LENGTH - 1);
|
|
72
|
+
index += LINE_LENGTH - 1;
|
|
73
|
+
} else if (/=[A-F0-9]$/.test(buffer)) {
|
|
74
|
+
buffer = buffer.slice(0, LINE_LENGTH - 2);
|
|
75
|
+
index += LINE_LENGTH - 2;
|
|
76
|
+
} else {
|
|
77
|
+
index += LINE_LENGTH;
|
|
78
|
+
}
|
|
79
|
+
result.push(buffer);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Encode space and tab characters at the end of encoded lines. Note that
|
|
84
|
+
// with the current implementation, this can only occur at the very end of
|
|
85
|
+
// the encoded string — every other line ends with `=` anyway.
|
|
86
|
+
const lastLineLength = buffer.length;
|
|
87
|
+
if (/[\t\x20]$/.test(buffer)) {
|
|
88
|
+
// There’s a space or a tab at the end of the last encoded line. Remove
|
|
89
|
+
// this line from the `result` array, as it needs to change.
|
|
90
|
+
result.pop();
|
|
91
|
+
if (lastLineLength + 2 <= LINE_LENGTH + 1) {
|
|
92
|
+
// It’s possible to encode the character without exceeding the line
|
|
93
|
+
// length limit.
|
|
94
|
+
result.push(
|
|
95
|
+
handleTrailingCharacters(buffer)
|
|
96
|
+
);
|
|
97
|
+
} else {
|
|
98
|
+
// It’s not possible to encode the character without exceeding the line
|
|
99
|
+
// length limit. Remvoe the character from the line, and insert a new
|
|
100
|
+
// line that contains only the encoded character.
|
|
101
|
+
result.push(
|
|
102
|
+
buffer.slice(0, lastLineLength - 1),
|
|
103
|
+
handleTrailingCharacters(
|
|
104
|
+
buffer.slice(lastLineLength - 1, lastLineLength)
|
|
105
|
+
)
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// `Quoted-Printable` uses CRLF.
|
|
111
|
+
return result.join('=\r\n');
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const quotedPrintable = {
|
|
115
|
+
encode,
|
|
116
|
+
decode,
|
|
117
|
+
'version': '1.0.1'
|
|
118
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { RawBuffer } from "./RawBuffer.js";
|
|
2
|
+
|
|
3
|
+
const isEncodedRegEx = /\=\?([^\?\s]+)\?([^\?\s]{1})\?([^\s\?]+)\?\=\s*/gm;
|
|
4
|
+
|
|
5
|
+
const hexToUnicode = /(\=([0-9a-f]{2}))|(\_)/gmi;
|
|
6
|
+
|
|
7
|
+
const decode = (
|
|
8
|
+
/** @type {string} */ text
|
|
9
|
+
) => {
|
|
10
|
+
return text.replace(isEncodedRegEx, (matched, encoding, format, /** @type {string} */ buffer) => {
|
|
11
|
+
if (/b/i.test(format)) {
|
|
12
|
+
return RawBuffer.decode(atob(buffer), encoding);
|
|
13
|
+
}
|
|
14
|
+
if (/q/i.test(format)) {
|
|
15
|
+
const replaced = buffer.replace(hexToUnicode, (m, group, /** @type {string} */ code, ) => {
|
|
16
|
+
if (m === "_") {
|
|
17
|
+
return " ";
|
|
18
|
+
}
|
|
19
|
+
return String.fromCharCode(parseInt(code, 16));
|
|
20
|
+
});
|
|
21
|
+
return RawBuffer.decode( replaced, encoding);
|
|
22
|
+
}
|
|
23
|
+
return matched;
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const isSimpleText = /[0-9a-zA-Z\x20]/;
|
|
28
|
+
|
|
29
|
+
export const wordEncoding = {
|
|
30
|
+
decode,
|
|
31
|
+
|
|
32
|
+
encode: (/** @type {string} */ word, ifNeeded = false) => {
|
|
33
|
+
|
|
34
|
+
if(ifNeeded) {
|
|
35
|
+
if(isSimpleText.test(word)) {
|
|
36
|
+
return word;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
word = RawBuffer.encode(word);
|
|
41
|
+
return `=?UTF-8?B?${btoa(word)}?=`;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { tokenize, tokenizeMax } from "./tokenizer.js";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export const parsePairs = (
|
|
5
|
+
/** @type {string} */ text,
|
|
6
|
+
/** @type {string} */ emptyName) => {
|
|
7
|
+
const pairs = {
|
|
8
|
+
toString() {
|
|
9
|
+
return text;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
for (const iterator of tokenize(text, ";")) {
|
|
13
|
+
const trimmed = iterator.trim();
|
|
14
|
+
if (!trimmed) {
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
const items = tokenizeMax(trimmed, "=", 2).map((x) => x.trim());
|
|
18
|
+
let [key, value] = items;
|
|
19
|
+
if (items.length === 1) {
|
|
20
|
+
value = key;
|
|
21
|
+
key = emptyName ?? key;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
if (value.startsWith('"')) {
|
|
25
|
+
value = JSON.parse(value);
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
// ignore
|
|
29
|
+
}
|
|
30
|
+
// pairs[key] = wordEncoding.decode(value);
|
|
31
|
+
pairs[key] = value;
|
|
32
|
+
}
|
|
33
|
+
return pairs;
|
|
34
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { tokenize } from "../tokenizer.js";
|
|
2
|
+
|
|
3
|
+
export default class LineStream {
|
|
4
|
+
|
|
5
|
+
/** @type {AsyncGenerator<string, any, any>} */ g;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @returns {AsyncGenerator<string, any, any>}
|
|
9
|
+
*/
|
|
10
|
+
async *read() {
|
|
11
|
+
const g = this.g ??= this.lines();
|
|
12
|
+
for(;;) {
|
|
13
|
+
const { value , done } = await g.next();
|
|
14
|
+
if (done) {
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
yield value;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** @returns {AsyncGenerator<string, any,any>} */
|
|
22
|
+
lines() {
|
|
23
|
+
throw new Error("Not Implemented");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class StringLineStream extends LineStream {
|
|
28
|
+
|
|
29
|
+
/** @type {string} */ text;
|
|
30
|
+
|
|
31
|
+
constructor( /** @type {string} */ text) {
|
|
32
|
+
super();
|
|
33
|
+
this.text = text;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @returns {AsyncGenerator<string, any, any>} */
|
|
37
|
+
async *lines() {
|
|
38
|
+
for (const iterator of tokenize(this.text, "\n")) {
|
|
39
|
+
yield iterator;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class ReadableLineStream extends LineStream {
|
|
45
|
+
|
|
46
|
+
/** @type {ReadableStream} */
|
|
47
|
+
readable;
|
|
48
|
+
|
|
49
|
+
constructor( /** @type {ReadableStream} */ readable) {
|
|
50
|
+
super();
|
|
51
|
+
this.readable = readable;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** @returns {AsyncGenerator<string, any, any>} */
|
|
55
|
+
async *lines() {
|
|
56
|
+
const utf8Decoder = new TextDecoder("utf-8");
|
|
57
|
+
const reader = this.readable.getReader();
|
|
58
|
+
let { value: chunk, done: readerDone } = await reader.read();
|
|
59
|
+
chunk = chunk ? utf8Decoder.decode(chunk, { stream: true }) : "";
|
|
60
|
+
|
|
61
|
+
const re = /\r\n|\n|\r/gm;
|
|
62
|
+
let startIndex = 0;
|
|
63
|
+
|
|
64
|
+
for (;;) {
|
|
65
|
+
const result = re.exec(chunk);
|
|
66
|
+
if (!result) {
|
|
67
|
+
if (readerDone) {
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
const remainder = chunk.substr(startIndex);
|
|
71
|
+
({ value: chunk, done: readerDone } = await reader.read());
|
|
72
|
+
chunk =
|
|
73
|
+
remainder + (chunk ? utf8Decoder.decode(chunk, { stream: true }) : "");
|
|
74
|
+
startIndex = re.lastIndex = 0;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
yield chunk.substring(startIndex, result.index);
|
|
78
|
+
startIndex = re.lastIndex;
|
|
79
|
+
}
|
|
80
|
+
if (startIndex < chunk.length) {
|
|
81
|
+
// last line didn't end in a newline char
|
|
82
|
+
yield chunk.substr(startIndex);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export default class TextWriter {
|
|
2
|
+
|
|
3
|
+
writeLine(/** @type {string} */ line) {
|
|
4
|
+
throw new Error("Not Implemented");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class BlobWriter extends TextWriter {
|
|
10
|
+
|
|
11
|
+
/** @type {string[]} */
|
|
12
|
+
lines = [];
|
|
13
|
+
|
|
14
|
+
writeLine(/** @type {string} */line) {
|
|
15
|
+
this.lines.push(line);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
toBlob() {
|
|
19
|
+
const all = this.lines.join("\n");
|
|
20
|
+
return new Blob([all], {});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
toString() {
|
|
24
|
+
return this.lines.join("\n");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export function *tokenize (
|
|
2
|
+
/** @type {string} */ text,
|
|
3
|
+
/** @type {string} */ sep = ",") {
|
|
4
|
+
let start = 0;
|
|
5
|
+
for(;;) {
|
|
6
|
+
const index = text.indexOf(sep, start);
|
|
7
|
+
if (index === -1) {
|
|
8
|
+
break;
|
|
9
|
+
}
|
|
10
|
+
yield text.substring(start, index);
|
|
11
|
+
start = index + 1;
|
|
12
|
+
}
|
|
13
|
+
yield text.substring(start);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
export function tokenizeMax(
|
|
18
|
+
/** @type {string} */ text,
|
|
19
|
+
/** @type {string} */ sep,
|
|
20
|
+
/** @type {number} */ max = Number.MAX_SAFE_INTEGER) {
|
|
21
|
+
let start = 0;
|
|
22
|
+
const items = [];
|
|
23
|
+
max--;
|
|
24
|
+
for(;;) {
|
|
25
|
+
const index = text.indexOf(sep, start);
|
|
26
|
+
if (index === -1) {
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
items.push(text.substring(start, index));
|
|
30
|
+
start = index + 1;
|
|
31
|
+
if (--max === 0) {
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
items.push(text.substring(start));
|
|
36
|
+
return items;
|
|
37
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import assert from "assert";
|
|
2
|
+
import { tokenize } from "../../mime-parser/tokenizer.js";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export default function () {
|
|
6
|
+
|
|
7
|
+
assert.equal(
|
|
8
|
+
"a,b",
|
|
9
|
+
Array.from(tokenize("a\nb", "\n")));
|
|
10
|
+
|
|
11
|
+
assert.equal(
|
|
12
|
+
"a b",
|
|
13
|
+
Array.from(tokenize("a b", "\n")));
|
|
14
|
+
assert.equal(
|
|
15
|
+
"a,,b",
|
|
16
|
+
Array.from(tokenize("a\n\nb", "\n")));
|
|
17
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import assert from "assert";
|
|
2
|
+
import { MimeNode } from "../../mime-parser/MimeNode.js";
|
|
3
|
+
import { StringLineStream } from "../../mime-parser/stream/LineStream.js";
|
|
4
|
+
|
|
5
|
+
const msg1 = `From: Some One <someone@example.com>
|
|
6
|
+
MIME-Version: 1.0
|
|
7
|
+
Content-Type: multipart/mixed;
|
|
8
|
+
boundary="XXXXboundary text"
|
|
9
|
+
|
|
10
|
+
This is a multipart message in MIME format.
|
|
11
|
+
|
|
12
|
+
--XXXXboundary text
|
|
13
|
+
Content-Type: text/plain
|
|
14
|
+
|
|
15
|
+
this is the body text
|
|
16
|
+
|
|
17
|
+
--XXXXboundary text
|
|
18
|
+
Content-Type: text/plain;
|
|
19
|
+
Content-Disposition: attachment;
|
|
20
|
+
filename="test.txt"
|
|
21
|
+
|
|
22
|
+
this is the attachment text
|
|
23
|
+
|
|
24
|
+
--XXXXboundary text--
|
|
25
|
+
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
export default async function () {
|
|
29
|
+
const root = new MimeNode();
|
|
30
|
+
await root.parse(new StringLineStream(msg1));
|
|
31
|
+
|
|
32
|
+
assert.strictEqual("multipart/mixed", root.contentType.type);
|
|
33
|
+
assert.strictEqual("XXXXboundary text", root.contentType.boundary);
|
|
34
|
+
assert.strictEqual("Some One <someone@example.com>", root.header("from"));
|
|
35
|
+
// Assert.strictEqual("This is a multipart message in MIME format.", root.encoded.trim());
|
|
36
|
+
assert.strictEqual(2, root.children.length);
|
|
37
|
+
|
|
38
|
+
const body = root.children[0];
|
|
39
|
+
assert.strictEqual("text/plain", body.contentType.type);
|
|
40
|
+
assert.strictEqual("this is the body text", body.encoded.trim());
|
|
41
|
+
|
|
42
|
+
const attachment = root.children[1];
|
|
43
|
+
assert.strictEqual("text/plain", attachment.contentType.type);
|
|
44
|
+
assert.strictEqual("this is the attachment text", attachment.encoded.trim());
|
|
45
|
+
assert.strictEqual("test.txt", attachment.contentDisposition.filename);
|
|
46
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import assert from "assert";
|
|
2
|
+
import { MimeNode } from "../../mime-parser/MimeNode.js";
|
|
3
|
+
import { StringLineStream } from "../../mime-parser/stream/LineStream.js";
|
|
4
|
+
import { BlobWriter } from "../../mime-parser/stream/TextWriter.js";
|
|
5
|
+
|
|
6
|
+
const msg1 = `From: Some One <someone@example.com>
|
|
7
|
+
MIME-Version: 1.0
|
|
8
|
+
Content-Type: multipart/mixed;
|
|
9
|
+
boundary="XXXXboundary text"
|
|
10
|
+
|
|
11
|
+
This is a multipart message in MIME format.
|
|
12
|
+
|
|
13
|
+
--XXXXboundary text
|
|
14
|
+
Content-Type: text/plain
|
|
15
|
+
|
|
16
|
+
this is the body text
|
|
17
|
+
|
|
18
|
+
--XXXXboundary text
|
|
19
|
+
Content-Type: text/html; charset="UTF-8";
|
|
20
|
+
Content-Transfer-Encoding: quoted-printable
|
|
21
|
+
|
|
22
|
+
<div dir=3D"ltr"><font face=3D"trebuchet ms, sans-serif">Seeking Actors and=
|
|
23
|
+
Actresses for a micro budget feature. The story revolves around a couple a=
|
|
24
|
+
nd their guests who find themselves in a tense and gripping situation. They=
|
|
25
|
+
are taken hostage in their own home by a radical gunman who lurks outside.=
|
|
26
|
+
We are looking for individuals who can bring depth and authenticity to the=
|
|
27
|
+
ir characters. The ability to convey a range of emotions and create believa=
|
|
28
|
+
ble performances is crucial for this project.<br><br>Submit ASAP through:=
|
|
29
|
+
=C2=A0<a href=3D"https://socialmail.in">https://socialmail.in</a><br><br>=
|
|
30
|
+
#newmexicoactors #newmexicocasting=C2=A0</font><br></div>
|
|
31
|
+
|
|
32
|
+
--XXXXboundary text--
|
|
33
|
+
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
export default async function () {
|
|
37
|
+
const root = new MimeNode();
|
|
38
|
+
await root.parse(new StringLineStream(msg1));
|
|
39
|
+
|
|
40
|
+
assertAll(root);
|
|
41
|
+
|
|
42
|
+
// write and read it again...
|
|
43
|
+
|
|
44
|
+
const blob = new BlobWriter();
|
|
45
|
+
await root.save(blob);
|
|
46
|
+
|
|
47
|
+
const encoded = blob.toString();
|
|
48
|
+
|
|
49
|
+
// reparse...
|
|
50
|
+
const root2 = new MimeNode();
|
|
51
|
+
await root2.parse(new StringLineStream(encoded));
|
|
52
|
+
|
|
53
|
+
assertAll(root2);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function assertAll( /** @type {MimeNode} */ root) {
|
|
57
|
+
assert.strictEqual("multipart/mixed", root.contentType.type);
|
|
58
|
+
assert.strictEqual("XXXXboundary text", root.contentType.boundary);
|
|
59
|
+
assert.strictEqual("Some One <someone@example.com>", root.header("from"));
|
|
60
|
+
// assert.strictEqual("This is a multipart message in MIME format.", root.encoded.trim());
|
|
61
|
+
assert.strictEqual(2, root.children.length);
|
|
62
|
+
|
|
63
|
+
const body = root.children[0];
|
|
64
|
+
assert.strictEqual("text/plain", body.contentType.type);
|
|
65
|
+
assert.strictEqual("this is the body text", body.encoded.trim());
|
|
66
|
+
|
|
67
|
+
const html = root.children[1];
|
|
68
|
+
assert.strictEqual("text/html", html.contentType.type);
|
|
69
|
+
assert.strictEqual("quoted-printable", html.contentTransferEncoding);
|
|
70
|
+
|
|
71
|
+
const data = html.text;
|
|
72
|
+
assert.strictEqual(`<div dir="ltr"><font face="trebuchet ms, sans-serif">Seeking Actors and Actresses for a micro budget feature. The story revolves around a couple and their guests who find themselves in a tense and gripping situation. They are taken hostage in their own home by a radical gunman who lurks outside. We are looking for individuals who can bring depth and authenticity to their characters. The ability to convey a range of emotions and create believable performances is crucial for this project.<br><br>Submit ASAP through: <a href="https://socialmail.in">https://socialmail.in</a><br><br>#newmexicoactors #newmexicocasting </font><br></div>`, data);
|
|
73
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { parsePairs } from "../../mime-parser/parsePairs.js";
|
|
2
|
+
|
|
3
|
+
const equalJson = (e, t, m) => {
|
|
4
|
+
e = JSON.stringify(e);
|
|
5
|
+
t = JSON.stringify(t);
|
|
6
|
+
// eslint-disable-next-line eqeqeq
|
|
7
|
+
if (e !== t) {
|
|
8
|
+
throw new Error(m ?? `Assertion failed ${e} !== ${t}`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function() {
|
|
13
|
+
|
|
14
|
+
equalJson(
|
|
15
|
+
parsePairs("multipart/mixed; boundary=--abcd"),
|
|
16
|
+
{
|
|
17
|
+
"multipart/mixed": "multipart/mixed",
|
|
18
|
+
boundary: "--abcd"
|
|
19
|
+
}
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
equalJson(
|
|
24
|
+
parsePairs("multipart/mixed; boundary=--abcd", "type"),
|
|
25
|
+
{
|
|
26
|
+
type: "multipart/mixed",
|
|
27
|
+
boundary: "--abcd"
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
equalJson(
|
|
32
|
+
parsePairs("multipart/mixed;", "type"),
|
|
33
|
+
{
|
|
34
|
+
type: "multipart/mixed"
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
}
|