arn-rawmime 0.0.6 → 0.0.7
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/package.json +1 -1
- package/src/rawmimeBuilder.js +71 -26
package/package.json
CHANGED
package/src/rawmimeBuilder.js
CHANGED
|
@@ -78,7 +78,12 @@ class MimeMessage {
|
|
|
78
78
|
if (isSafe) {
|
|
79
79
|
tempStr += String.fromCharCode(byte);
|
|
80
80
|
} else if (isSpaceOrTab) {
|
|
81
|
-
|
|
81
|
+
// RFC 2045 §6.7 Rule 3: trailing whitespace MUST be encoded
|
|
82
|
+
if (i === buffer.length - 1) {
|
|
83
|
+
tempStr += "=" + byte.toString(16).toUpperCase().padStart(2, "0");
|
|
84
|
+
} else {
|
|
85
|
+
tempStr += String.fromCharCode(byte);
|
|
86
|
+
}
|
|
82
87
|
} else {
|
|
83
88
|
tempStr += "=" + byte.toString(16).toUpperCase().padStart(2, "0");
|
|
84
89
|
}
|
|
@@ -123,6 +128,39 @@ class MimeMessage {
|
|
|
123
128
|
.join("\r\n");
|
|
124
129
|
}
|
|
125
130
|
|
|
131
|
+
// ─── HELPER: RFC 2047 Encoded-Word Chunking ──────────────────────
|
|
132
|
+
_encodeRFC2047(str) {
|
|
133
|
+
const MAX_ENCODED_LENGTH = 75;
|
|
134
|
+
const prefix = "=?UTF-8?B?";
|
|
135
|
+
const suffix = "?=";
|
|
136
|
+
const maxB64Len = MAX_ENCODED_LENGTH - prefix.length - suffix.length; // 63
|
|
137
|
+
const maxBytes = Math.floor(maxB64Len * 3 / 4); // 47 bytes
|
|
138
|
+
|
|
139
|
+
let result = [];
|
|
140
|
+
let currentChunk = "";
|
|
141
|
+
let currentChunkBytes = 0;
|
|
142
|
+
|
|
143
|
+
for (const char of str) {
|
|
144
|
+
const charBytes = Buffer.byteLength(char, "utf8");
|
|
145
|
+
if (currentChunkBytes + charBytes > maxBytes) {
|
|
146
|
+
const b64 = Buffer.from(currentChunk, "utf8").toString("base64");
|
|
147
|
+
result.push(`${prefix}${b64}${suffix}`);
|
|
148
|
+
currentChunk = char;
|
|
149
|
+
currentChunkBytes = charBytes;
|
|
150
|
+
} else {
|
|
151
|
+
currentChunk += char;
|
|
152
|
+
currentChunkBytes += charBytes;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (currentChunk.length > 0) {
|
|
156
|
+
const b64 = Buffer.from(currentChunk, "utf8").toString("base64");
|
|
157
|
+
result.push(`${prefix}${b64}${suffix}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Separate encoded words by space. Standard ASCII folding will wrap them if needed.
|
|
161
|
+
return result.join(" ");
|
|
162
|
+
}
|
|
163
|
+
|
|
126
164
|
// ─── HELPER: Header Folding (RFC 5322) ──────────────────────────
|
|
127
165
|
_foldHeader(name, value) {
|
|
128
166
|
const hasNonAscii = /[^\x00-\x7F]/.test(value);
|
|
@@ -130,45 +168,47 @@ class MimeMessage {
|
|
|
130
168
|
// 1. Unstructured headers (Subject, etc) -> Full Encode
|
|
131
169
|
const unstructured = ["subject", "x-report-abuse", "thread-topic"];
|
|
132
170
|
if (hasNonAscii && unstructured.includes(name.toLowerCase())) {
|
|
133
|
-
|
|
134
|
-
return `${name}: =?UTF-8?B?${encodedValue}?=`;
|
|
171
|
+
value = this._encodeRFC2047(value);
|
|
135
172
|
}
|
|
136
173
|
|
|
137
174
|
// 2. Structured headers (From, To) -> Smart Replace
|
|
138
|
-
//
|
|
139
|
-
if (hasNonAscii) {
|
|
140
|
-
|
|
141
|
-
if (/[^\x00-\x7F]/.test(
|
|
142
|
-
|
|
143
|
-
|
|
175
|
+
// Encodes quoted non-ASCII strings AND unquoted non-ASCII words
|
|
176
|
+
if (hasNonAscii && !unstructured.includes(name.toLowerCase())) {
|
|
177
|
+
value = value.replace(/"([^"]*)"|([^\s<>]*[^\x00-\x7F][^\s<>]*)/g, (match, quoted, unquoted) => {
|
|
178
|
+
if (quoted !== undefined && /[^\x00-\x7F]/.test(quoted)) {
|
|
179
|
+
return this._encodeRFC2047(quoted);
|
|
180
|
+
}
|
|
181
|
+
if (unquoted !== undefined && /[^\x00-\x7F]/.test(unquoted)) {
|
|
182
|
+
return this._encodeRFC2047(unquoted);
|
|
144
183
|
}
|
|
145
184
|
return match;
|
|
146
185
|
});
|
|
147
|
-
// If we changed anything, return it. If not (e.g. unquoted special chars), fallback to old folding
|
|
148
|
-
if (encodedStruct !== value) {
|
|
149
|
-
return `${name}: ${encodedStruct}`;
|
|
150
|
-
}
|
|
151
186
|
}
|
|
152
187
|
|
|
153
188
|
// 3. Standard folding for ASCII-only (or unhandled) headers
|
|
154
189
|
const line = `${name}: ${value}`;
|
|
155
190
|
if (line.length <= 76) return line;
|
|
156
191
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
let
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (remaining.length < available) return result + remaining;
|
|
192
|
+
let result = `${name}: `;
|
|
193
|
+
const tokens = value.split(/(?=[ \t])/);
|
|
194
|
+
let currentLineLength = result.length;
|
|
195
|
+
const prefixLength = result.length;
|
|
163
196
|
|
|
164
|
-
|
|
165
|
-
|
|
197
|
+
tokens.forEach((token) => {
|
|
198
|
+
if (currentLineLength + token.length > 76 && currentLineLength > prefixLength) {
|
|
199
|
+
if (/^[ \t]/.test(token)) {
|
|
200
|
+
result += "\r\n" + token;
|
|
201
|
+
currentLineLength = token.length;
|
|
202
|
+
} else {
|
|
203
|
+
result += token;
|
|
204
|
+
currentLineLength += token.length;
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
result += token;
|
|
208
|
+
currentLineLength += token.length;
|
|
209
|
+
}
|
|
210
|
+
});
|
|
166
211
|
|
|
167
|
-
while (remaining.length > 75) {
|
|
168
|
-
result += remaining.substring(0, 75) + "\r\n ";
|
|
169
|
-
remaining = remaining.substring(75);
|
|
170
|
-
}
|
|
171
|
-
result += remaining;
|
|
172
212
|
return result;
|
|
173
213
|
}
|
|
174
214
|
|
|
@@ -607,6 +647,7 @@ class MimeMessage {
|
|
|
607
647
|
// UPDATE THIS ARRAY BELOW
|
|
608
648
|
// Add "list-unsubscribe" and "list-unsubscribe-post" to this list
|
|
609
649
|
const doNotFold = [
|
|
650
|
+
"subject",
|
|
610
651
|
"message-id",
|
|
611
652
|
"references",
|
|
612
653
|
"in-reply-to",
|
|
@@ -616,6 +657,10 @@ class MimeMessage {
|
|
|
616
657
|
];
|
|
617
658
|
|
|
618
659
|
if (doNotFold.includes(lowerName)) {
|
|
660
|
+
// Still encode non-ASCII, just skip line folding
|
|
661
|
+
if (/[^\x00-\x7F]/.test(h.value)) {
|
|
662
|
+
return `${h.name}: ${this._encodeRFC2047(h.value)}`;
|
|
663
|
+
}
|
|
619
664
|
return `${h.name}: ${h.value}`;
|
|
620
665
|
}
|
|
621
666
|
return this._foldHeader(h.name, h.value);
|