lite-email-parser 2.0.3 → 2.1.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/dist/index.cjs +29 -4
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +27 -29
- package/package.json +14 -29
- package/binding.gyp +0 -47
- package/core/include/html_extractor.h +0 -100
- package/core/include/html_parser.h +0 -13
- package/core/src/html_extractor.cpp +0 -662
- package/core/src/html_parser.cpp +0 -221
- package/deps/gumbo-parser/src/attribute.c +0 -44
- package/deps/gumbo-parser/src/attribute.h +0 -37
- package/deps/gumbo-parser/src/char_ref.c +0 -301
- package/deps/gumbo-parser/src/char_ref.h +0 -70
- package/deps/gumbo-parser/src/char_ref_gperf.c +0 -11126
- package/deps/gumbo-parser/src/error.c +0 -287
- package/deps/gumbo-parser/src/error.h +0 -225
- package/deps/gumbo-parser/src/gumbo.h +0 -683
- package/deps/gumbo-parser/src/insertion_mode.h +0 -55
- package/deps/gumbo-parser/src/parser.c +0 -4433
- package/deps/gumbo-parser/src/parser.h +0 -57
- package/deps/gumbo-parser/src/string_buffer.c +0 -110
- package/deps/gumbo-parser/src/string_buffer.h +0 -84
- package/deps/gumbo-parser/src/string_piece.c +0 -48
- package/deps/gumbo-parser/src/string_piece.h +0 -38
- package/deps/gumbo-parser/src/tag.c +0 -125
- package/deps/gumbo-parser/src/tag_enum.h +0 -155
- package/deps/gumbo-parser/src/tag_gperf.h +0 -350
- package/deps/gumbo-parser/src/tag_sizes.h +0 -2
- package/deps/gumbo-parser/src/tag_strings.h +0 -155
- package/deps/gumbo-parser/src/token_type.h +0 -42
- package/deps/gumbo-parser/src/tokenizer.c +0 -3003
- package/deps/gumbo-parser/src/tokenizer.h +0 -123
- package/deps/gumbo-parser/src/tokenizer_states.h +0 -104
- package/deps/gumbo-parser/src/utf8.c +0 -270
- package/deps/gumbo-parser/src/utf8.h +0 -132
- package/deps/gumbo-parser/src/util.c +0 -58
- package/deps/gumbo-parser/src/util.h +0 -60
- package/deps/gumbo-parser/src/vector.c +0 -128
- package/deps/gumbo-parser/src/vector.h +0 -70
- package/dist/cjs/index.cjs +0 -63
- package/dist/cjs/index.d.cts +0 -9
- package/dist/cjs/index.d.cts.map +0 -1
- package/dist/cjs/index.d.ts +0 -9
- package/dist/cjs/index.d.ts.map +0 -1
- package/dist/cjs/index.js +0 -92
- package/dist/cjs/package.json +0 -1
- package/dist/cjs/parser.d.ts +0 -4
- package/dist/cjs/parser.d.ts.map +0 -1
- package/dist/cjs/parser.js +0 -118
- package/dist/cjs/types.d.ts +0 -14
- package/dist/cjs/types.d.ts.map +0 -1
- package/dist/cjs/types.js +0 -2
- package/dist/parser.d.ts +0 -4
- package/dist/parser.d.ts.map +0 -1
- package/dist/parser.js +0 -81
- package/src/addon.cpp +0 -141
|
@@ -1,662 +0,0 @@
|
|
|
1
|
-
#include "html_extractor.h"
|
|
2
|
-
|
|
3
|
-
#include <algorithm>
|
|
4
|
-
#include <array>
|
|
5
|
-
#include <cctype>
|
|
6
|
-
#include <cstdint>
|
|
7
|
-
#include <fstream>
|
|
8
|
-
#include <iostream>
|
|
9
|
-
#include <sstream>
|
|
10
|
-
#include <string>
|
|
11
|
-
#include <vector>
|
|
12
|
-
|
|
13
|
-
namespace liteEmailParser {
|
|
14
|
-
|
|
15
|
-
ParseEmailResult parseEmail(const std::string& rawEmail) {
|
|
16
|
-
ParseEmailResult result;
|
|
17
|
-
|
|
18
|
-
if (rawEmail.empty()) {
|
|
19
|
-
return result;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Split headers from body (separated by first blank line)
|
|
23
|
-
std::string::size_type headerEnd = rawEmail.find("\r\n\r\n");
|
|
24
|
-
std::string lineEnding = "\r\n";
|
|
25
|
-
if (headerEnd == std::string::npos) {
|
|
26
|
-
headerEnd = rawEmail.find("\n\n");
|
|
27
|
-
lineEnding = "\n";
|
|
28
|
-
}
|
|
29
|
-
if (headerEnd == std::string::npos) {
|
|
30
|
-
return result;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
std::string headers = rawEmail.substr(0, headerEnd);
|
|
34
|
-
std::string body = rawEmail.substr(headerEnd + lineEnding.size() * 2);
|
|
35
|
-
|
|
36
|
-
// Extract top-level headers
|
|
37
|
-
result.subject = detail::getHeaderValue(headers, "Subject");
|
|
38
|
-
result.from = detail::getHeaderValue(headers, "From");
|
|
39
|
-
result.to = detail::getHeaderValue(headers, "To");
|
|
40
|
-
|
|
41
|
-
// Parse MIME parts once
|
|
42
|
-
std::string contentType = detail::getHeaderValue(headers, "Content-Type");
|
|
43
|
-
std::string boundary = detail::extractBoundary(contentType);
|
|
44
|
-
|
|
45
|
-
std::vector<detail::MimePart> parts;
|
|
46
|
-
if (!boundary.empty()) {
|
|
47
|
-
parts = detail::parseMimeParts(body, boundary);
|
|
48
|
-
} else {
|
|
49
|
-
// Not multipart — single body
|
|
50
|
-
std::string encoding = detail::getHeaderValue(headers, "Content-Transfer-Encoding");
|
|
51
|
-
detail::MimePart single;
|
|
52
|
-
single.contentType = contentType;
|
|
53
|
-
single.transferEncoding = encoding;
|
|
54
|
-
single.body = body;
|
|
55
|
-
parts.push_back(single);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Find HTML body (or fall back to text/plain) from the parsed parts
|
|
59
|
-
for (const auto& part : parts) {
|
|
60
|
-
std::string ct = part.contentType;
|
|
61
|
-
std::transform(ct.begin(), ct.end(), ct.begin(),
|
|
62
|
-
[](unsigned char c) { return std::tolower(c); });
|
|
63
|
-
if (ct.find("text/html") != std::string::npos) {
|
|
64
|
-
std::string enc = part.transferEncoding;
|
|
65
|
-
std::transform(enc.begin(), enc.end(), enc.begin(),
|
|
66
|
-
[](unsigned char c) { return std::tolower(c); });
|
|
67
|
-
if (enc.find("quoted-printable") != std::string::npos) {
|
|
68
|
-
result.text = detail::decodeQuotedPrintable(part.body);
|
|
69
|
-
} else if (enc.find("base64") != std::string::npos) {
|
|
70
|
-
result.text = detail::decodeBase64(part.body);
|
|
71
|
-
} else {
|
|
72
|
-
result.text = part.body;
|
|
73
|
-
}
|
|
74
|
-
break;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Fallback to text/plain if no HTML found
|
|
79
|
-
if (result.text.empty()) {
|
|
80
|
-
for (const auto& part : parts) {
|
|
81
|
-
std::string ct = part.contentType;
|
|
82
|
-
std::transform(ct.begin(), ct.end(), ct.begin(),
|
|
83
|
-
[](unsigned char c) { return std::tolower(c); });
|
|
84
|
-
if (ct.find("text/plain") != std::string::npos) {
|
|
85
|
-
std::string enc = part.transferEncoding;
|
|
86
|
-
std::transform(enc.begin(), enc.end(), enc.begin(),
|
|
87
|
-
[](unsigned char c) { return std::tolower(c); });
|
|
88
|
-
if (enc.find("quoted-printable") != std::string::npos) {
|
|
89
|
-
result.text = detail::decodeQuotedPrintable(part.body);
|
|
90
|
-
} else if (enc.find("base64") != std::string::npos) {
|
|
91
|
-
result.text = detail::decodeBase64(part.body);
|
|
92
|
-
} else {
|
|
93
|
-
result.text = part.body;
|
|
94
|
-
}
|
|
95
|
-
break;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Replace cid: references with base64 data URIs (using same parts, no re-parse)
|
|
101
|
-
if (!result.text.empty() && !parts.empty()) {
|
|
102
|
-
result.text = detail::replaceCidWithBase64(result.text, parts);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Extract attachments from parsed MIME parts
|
|
106
|
-
for (const auto& part : parts) {
|
|
107
|
-
std::string ct = part.contentType;
|
|
108
|
-
std::transform(ct.begin(), ct.end(), ct.begin(),
|
|
109
|
-
[](unsigned char c) { return std::tolower(c); });
|
|
110
|
-
|
|
111
|
-
// Skip text parts (already used for body)
|
|
112
|
-
if (ct.find("text/html") != std::string::npos ||
|
|
113
|
-
ct.find("text/plain") != std::string::npos) {
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Skip parts with no content type (shouldn't happen, but guard)
|
|
118
|
-
if (ct.empty()) continue;
|
|
119
|
-
|
|
120
|
-
// Decode the attachment body
|
|
121
|
-
std::string rawData;
|
|
122
|
-
std::string enc = part.transferEncoding;
|
|
123
|
-
std::transform(enc.begin(), enc.end(), enc.begin(),
|
|
124
|
-
[](unsigned char c) { return std::tolower(c); });
|
|
125
|
-
if (enc.find("base64") != std::string::npos) {
|
|
126
|
-
rawData = detail::decodeBase64(part.body);
|
|
127
|
-
} else if (enc.find("quoted-printable") != std::string::npos) {
|
|
128
|
-
rawData = detail::decodeQuotedPrintable(part.body);
|
|
129
|
-
} else {
|
|
130
|
-
rawData = part.body;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
Attachment att;
|
|
134
|
-
att.name = part.filename.empty() ? "attachment" : part.filename;
|
|
135
|
-
att.type = detail::extractMimeType(part.contentType);
|
|
136
|
-
att.size = rawData.size();
|
|
137
|
-
att.buffer = std::move(rawData);
|
|
138
|
-
|
|
139
|
-
// If it has a Content-ID, it's an inline attachment
|
|
140
|
-
if (!part.contentId.empty()) {
|
|
141
|
-
att.originalSrc = "data:" + att.type + ";base64," + detail::encodeBase64(att.buffer);
|
|
142
|
-
result.inlineAttachments.push_back(std::move(att));
|
|
143
|
-
} else {
|
|
144
|
-
result.attachments.push_back(std::move(att));
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return result;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
std::string readEmailFile(const std::string& filePath) {
|
|
152
|
-
std::ifstream file(filePath, std::ios::in | std::ios::binary);
|
|
153
|
-
if (!file.is_open()) {
|
|
154
|
-
std::cerr << "Error: could not open file: " << filePath << std::endl;
|
|
155
|
-
return "";
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
std::ostringstream ss;
|
|
159
|
-
ss << file.rdbuf();
|
|
160
|
-
return ss.str();
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
std::string replaceSrc(const std::string& html, const std::vector<Attachment>& files) {
|
|
164
|
-
std::string result = html;
|
|
165
|
-
|
|
166
|
-
for (const auto& file : files) {
|
|
167
|
-
if (file.originalSrc.empty() || file.src.empty()) {
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Find and replace src="<originalSrc>" with src="<newSrc>"
|
|
172
|
-
std::string searchAttr = "src=\"" + file.originalSrc + "\"";
|
|
173
|
-
std::string replaceAttr = "src=\"" + file.src + "\"";
|
|
174
|
-
|
|
175
|
-
std::string::size_type pos = 0;
|
|
176
|
-
while ((pos = result.find(searchAttr, pos)) != std::string::npos) {
|
|
177
|
-
result.replace(pos, searchAttr.size(), replaceAttr);
|
|
178
|
-
pos += replaceAttr.size();
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return result;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
ExtractionResult extractHtmlBody(const std::string& rawEmail) {
|
|
186
|
-
if (rawEmail.empty()) {
|
|
187
|
-
return {"", false};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Split headers from body (separated by first blank line)
|
|
191
|
-
std::string::size_type headerEnd = rawEmail.find("\r\n\r\n");
|
|
192
|
-
std::string lineEnding = "\r\n";
|
|
193
|
-
if (headerEnd == std::string::npos) {
|
|
194
|
-
headerEnd = rawEmail.find("\n\n");
|
|
195
|
-
lineEnding = "\n";
|
|
196
|
-
}
|
|
197
|
-
if (headerEnd == std::string::npos) {
|
|
198
|
-
// No body found — treat entire content as body? Return empty.
|
|
199
|
-
return {"", false};
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
std::string headers = rawEmail.substr(0, headerEnd);
|
|
203
|
-
std::string body = rawEmail.substr(headerEnd + lineEnding.size() * 2);
|
|
204
|
-
|
|
205
|
-
// Get the top-level Content-Type to find the boundary
|
|
206
|
-
std::string contentType = detail::getHeaderValue(headers, "Content-Type");
|
|
207
|
-
std::string boundary = detail::extractBoundary(contentType);
|
|
208
|
-
|
|
209
|
-
std::vector<detail::MimePart> parts;
|
|
210
|
-
if (!boundary.empty()) {
|
|
211
|
-
parts = detail::parseMimeParts(body, boundary);
|
|
212
|
-
} else {
|
|
213
|
-
// Not multipart — single body
|
|
214
|
-
std::string encoding = detail::getHeaderValue(headers, "Content-Transfer-Encoding");
|
|
215
|
-
detail::MimePart single;
|
|
216
|
-
single.contentType = contentType;
|
|
217
|
-
single.transferEncoding = encoding;
|
|
218
|
-
single.body = body;
|
|
219
|
-
parts.push_back(single);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// First pass: look for text/html
|
|
223
|
-
for (const auto& part : parts) {
|
|
224
|
-
std::string ct = part.contentType;
|
|
225
|
-
std::transform(ct.begin(), ct.end(), ct.begin(),
|
|
226
|
-
[](unsigned char c) { return std::tolower(c); });
|
|
227
|
-
if (ct.find("text/html") != std::string::npos) {
|
|
228
|
-
std::string decoded = part.body;
|
|
229
|
-
std::string enc = part.transferEncoding;
|
|
230
|
-
std::transform(enc.begin(), enc.end(), enc.begin(),
|
|
231
|
-
[](unsigned char c) { return std::tolower(c); });
|
|
232
|
-
if (enc.find("quoted-printable") != std::string::npos) {
|
|
233
|
-
decoded = detail::decodeQuotedPrintable(part.body);
|
|
234
|
-
} else if (enc.find("base64") != std::string::npos) {
|
|
235
|
-
decoded = detail::decodeBase64(part.body);
|
|
236
|
-
}
|
|
237
|
-
return {decoded, true};
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Second pass: fall back to text/plain
|
|
242
|
-
for (const auto& part : parts) {
|
|
243
|
-
std::string ct = part.contentType;
|
|
244
|
-
std::transform(ct.begin(), ct.end(), ct.begin(),
|
|
245
|
-
[](unsigned char c) { return std::tolower(c); });
|
|
246
|
-
if (ct.find("text/plain") != std::string::npos) {
|
|
247
|
-
std::string decoded = part.body;
|
|
248
|
-
std::string enc = part.transferEncoding;
|
|
249
|
-
std::transform(enc.begin(), enc.end(), enc.begin(),
|
|
250
|
-
[](unsigned char c) { return std::tolower(c); });
|
|
251
|
-
if (enc.find("quoted-printable") != std::string::npos) {
|
|
252
|
-
decoded = detail::decodeQuotedPrintable(part.body);
|
|
253
|
-
} else if (enc.find("base64") != std::string::npos) {
|
|
254
|
-
decoded = detail::decodeBase64(part.body);
|
|
255
|
-
}
|
|
256
|
-
return {decoded, false};
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Nothing found
|
|
261
|
-
return {"", false};
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
namespace detail {
|
|
265
|
-
|
|
266
|
-
std::string extractBoundary(const std::string& contentTypeHeader) {
|
|
267
|
-
// Look for boundary= in the Content-Type header
|
|
268
|
-
std::string lower = contentTypeHeader;
|
|
269
|
-
std::transform(lower.begin(), lower.end(), lower.begin(),
|
|
270
|
-
[](unsigned char c) { return std::tolower(c); });
|
|
271
|
-
|
|
272
|
-
std::string::size_type pos = lower.find("boundary=");
|
|
273
|
-
if (pos == std::string::npos) {
|
|
274
|
-
return "";
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
pos += 9; // skip "boundary="
|
|
278
|
-
std::string boundary;
|
|
279
|
-
if (pos < contentTypeHeader.size() && contentTypeHeader[pos] == '"') {
|
|
280
|
-
// Quoted boundary
|
|
281
|
-
pos++;
|
|
282
|
-
std::string::size_type end = contentTypeHeader.find('"', pos);
|
|
283
|
-
if (end != std::string::npos) {
|
|
284
|
-
boundary = contentTypeHeader.substr(pos, end - pos);
|
|
285
|
-
}
|
|
286
|
-
} else {
|
|
287
|
-
// Unquoted boundary — ends at semicolon, whitespace, or end of string
|
|
288
|
-
std::string::size_type end = pos;
|
|
289
|
-
while (end < contentTypeHeader.size() &&
|
|
290
|
-
contentTypeHeader[end] != ';' &&
|
|
291
|
-
contentTypeHeader[end] != ' ' &&
|
|
292
|
-
contentTypeHeader[end] != '\t' &&
|
|
293
|
-
contentTypeHeader[end] != '\r' &&
|
|
294
|
-
contentTypeHeader[end] != '\n') {
|
|
295
|
-
end++;
|
|
296
|
-
}
|
|
297
|
-
boundary = contentTypeHeader.substr(pos, end - pos);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
return boundary;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
std::vector<MimePart> parseMimeParts(const std::string& rawContent,
|
|
304
|
-
const std::string& boundary) {
|
|
305
|
-
std::vector<MimePart> results;
|
|
306
|
-
|
|
307
|
-
std::string delimiter = "--" + boundary;
|
|
308
|
-
std::string endDelimiter = delimiter + "--";
|
|
309
|
-
|
|
310
|
-
// Find all parts between delimiters
|
|
311
|
-
std::string::size_type pos = rawContent.find(delimiter);
|
|
312
|
-
if (pos == std::string::npos) {
|
|
313
|
-
return results;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// Skip past the first delimiter line
|
|
317
|
-
pos += delimiter.size();
|
|
318
|
-
// Skip to end of line
|
|
319
|
-
if (pos < rawContent.size() && rawContent[pos] == '\r') pos++;
|
|
320
|
-
if (pos < rawContent.size() && rawContent[pos] == '\n') pos++;
|
|
321
|
-
|
|
322
|
-
while (pos < rawContent.size()) {
|
|
323
|
-
// Find the next delimiter
|
|
324
|
-
std::string::size_type nextPos = rawContent.find(delimiter, pos);
|
|
325
|
-
if (nextPos == std::string::npos) {
|
|
326
|
-
break;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Extract this part's content (between current pos and next delimiter)
|
|
330
|
-
std::string partContent = rawContent.substr(pos, nextPos - pos);
|
|
331
|
-
|
|
332
|
-
// Remove trailing \r\n before the delimiter
|
|
333
|
-
if (partContent.size() >= 2 &&
|
|
334
|
-
partContent[partContent.size() - 2] == '\r' &&
|
|
335
|
-
partContent[partContent.size() - 1] == '\n') {
|
|
336
|
-
partContent.erase(partContent.size() - 2);
|
|
337
|
-
} else if (partContent.size() >= 1 &&
|
|
338
|
-
partContent[partContent.size() - 1] == '\n') {
|
|
339
|
-
partContent.erase(partContent.size() - 1);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// Split part into headers and body
|
|
343
|
-
std::string::size_type partHeaderEnd = partContent.find("\r\n\r\n");
|
|
344
|
-
std::string partLineEnding = "\r\n";
|
|
345
|
-
if (partHeaderEnd == std::string::npos) {
|
|
346
|
-
partHeaderEnd = partContent.find("\n\n");
|
|
347
|
-
partLineEnding = "\n";
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
if (partHeaderEnd != std::string::npos) {
|
|
351
|
-
std::string partHeaders = partContent.substr(0, partHeaderEnd);
|
|
352
|
-
std::string partBody = partContent.substr(
|
|
353
|
-
partHeaderEnd + partLineEnding.size() * 2);
|
|
354
|
-
|
|
355
|
-
std::string partCt = getHeaderValue(partHeaders, "Content-Type");
|
|
356
|
-
std::string partEnc = getHeaderValue(partHeaders, "Content-Transfer-Encoding");
|
|
357
|
-
std::string partCid = getHeaderValue(partHeaders, "Content-ID");
|
|
358
|
-
std::string partDisp = getHeaderValue(partHeaders, "Content-Disposition");
|
|
359
|
-
|
|
360
|
-
// Check if this part is itself multipart (nested)
|
|
361
|
-
std::string nestedBoundary = extractBoundary(partCt);
|
|
362
|
-
if (!nestedBoundary.empty()) {
|
|
363
|
-
// Recurse into nested multipart
|
|
364
|
-
auto nested = parseMimeParts(partBody, nestedBoundary);
|
|
365
|
-
results.insert(results.end(), nested.begin(), nested.end());
|
|
366
|
-
} else {
|
|
367
|
-
MimePart part;
|
|
368
|
-
part.contentType = partCt;
|
|
369
|
-
part.transferEncoding = partEnc;
|
|
370
|
-
part.contentId = extractContentId(partCid);
|
|
371
|
-
part.contentDisposition = partDisp;
|
|
372
|
-
part.filename = extractFilename(partDisp, partCt);
|
|
373
|
-
part.body = partBody;
|
|
374
|
-
results.push_back(part);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Move past the delimiter
|
|
379
|
-
pos = nextPos + delimiter.size();
|
|
380
|
-
// Check if this is the end delimiter
|
|
381
|
-
if (pos + 2 <= rawContent.size() &&
|
|
382
|
-
rawContent[pos] == '-' && rawContent[pos + 1] == '-') {
|
|
383
|
-
break;
|
|
384
|
-
}
|
|
385
|
-
// Skip to end of line
|
|
386
|
-
if (pos < rawContent.size() && rawContent[pos] == '\r') pos++;
|
|
387
|
-
if (pos < rawContent.size() && rawContent[pos] == '\n') pos++;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
return results;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
std::string getHeaderValue(const std::string& headers, const std::string& key) {
|
|
394
|
-
// Case-insensitive search for "Key: value" in headers
|
|
395
|
-
// Handles folded headers (continuation lines starting with whitespace)
|
|
396
|
-
std::string lowerHeaders = headers;
|
|
397
|
-
std::transform(lowerHeaders.begin(), lowerHeaders.end(), lowerHeaders.begin(),
|
|
398
|
-
[](unsigned char c) { return std::tolower(c); });
|
|
399
|
-
|
|
400
|
-
std::string lowerKey = key;
|
|
401
|
-
std::transform(lowerKey.begin(), lowerKey.end(), lowerKey.begin(),
|
|
402
|
-
[](unsigned char c) { return std::tolower(c); });
|
|
403
|
-
lowerKey += ":";
|
|
404
|
-
|
|
405
|
-
std::string::size_type pos = lowerHeaders.find(lowerKey);
|
|
406
|
-
if (pos == std::string::npos) {
|
|
407
|
-
return "";
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// Make sure it's at the start of a line (or start of headers)
|
|
411
|
-
if (pos > 0 && lowerHeaders[pos - 1] != '\n') {
|
|
412
|
-
// Search again from after this position
|
|
413
|
-
while (pos != std::string::npos) {
|
|
414
|
-
pos = lowerHeaders.find(lowerKey, pos + 1);
|
|
415
|
-
if (pos != std::string::npos && (pos == 0 || lowerHeaders[pos - 1] == '\n')) {
|
|
416
|
-
break;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
if (pos == std::string::npos) {
|
|
420
|
-
return "";
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Skip "Key:"
|
|
425
|
-
pos += lowerKey.size();
|
|
426
|
-
// Skip leading whitespace
|
|
427
|
-
while (pos < headers.size() && (headers[pos] == ' ' || headers[pos] == '\t')) {
|
|
428
|
-
pos++;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// Collect value including folded lines
|
|
432
|
-
std::string value;
|
|
433
|
-
while (pos < headers.size()) {
|
|
434
|
-
if (headers[pos] == '\r') {
|
|
435
|
-
pos++;
|
|
436
|
-
continue;
|
|
437
|
-
}
|
|
438
|
-
if (headers[pos] == '\n') {
|
|
439
|
-
// Check if next line is a continuation (starts with space/tab)
|
|
440
|
-
if (pos + 1 < headers.size() &&
|
|
441
|
-
(headers[pos + 1] == ' ' || headers[pos + 1] == '\t')) {
|
|
442
|
-
value += ' ';
|
|
443
|
-
pos += 2; // skip \n and the whitespace
|
|
444
|
-
// Skip additional whitespace
|
|
445
|
-
while (pos < headers.size() &&
|
|
446
|
-
(headers[pos] == ' ' || headers[pos] == '\t')) {
|
|
447
|
-
pos++;
|
|
448
|
-
}
|
|
449
|
-
continue;
|
|
450
|
-
} else {
|
|
451
|
-
break;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
value += headers[pos];
|
|
455
|
-
pos++;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
return value;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
std::string decodeQuotedPrintable(const std::string& input) {
|
|
462
|
-
std::string output;
|
|
463
|
-
output.reserve(input.size());
|
|
464
|
-
|
|
465
|
-
for (std::string::size_type i = 0; i < input.size(); i++) {
|
|
466
|
-
if (input[i] == '=') {
|
|
467
|
-
if (i + 1 < input.size() && input[i + 1] == '\n') {
|
|
468
|
-
// Soft line break (LF only)
|
|
469
|
-
i += 1;
|
|
470
|
-
} else if (i + 2 < input.size() &&
|
|
471
|
-
input[i + 1] == '\r' && input[i + 2] == '\n') {
|
|
472
|
-
// Soft line break (CRLF)
|
|
473
|
-
i += 2;
|
|
474
|
-
} else if (i + 2 < input.size() &&
|
|
475
|
-
std::isxdigit(static_cast<unsigned char>(input[i + 1])) &&
|
|
476
|
-
std::isxdigit(static_cast<unsigned char>(input[i + 2]))) {
|
|
477
|
-
// Hex-encoded byte
|
|
478
|
-
char hex[3] = {input[i + 1], input[i + 2], '\0'};
|
|
479
|
-
unsigned int byte = 0;
|
|
480
|
-
std::sscanf(hex, "%x", &byte);
|
|
481
|
-
output += static_cast<char>(byte);
|
|
482
|
-
i += 2;
|
|
483
|
-
} else {
|
|
484
|
-
// Malformed — just pass through
|
|
485
|
-
output += input[i];
|
|
486
|
-
}
|
|
487
|
-
} else {
|
|
488
|
-
output += input[i];
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
return output;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// Base64 decode table and decoder based on reference from base64.dev
|
|
496
|
-
static const char kEncodeTable[] =
|
|
497
|
-
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
498
|
-
"abcdefghijklmnopqrstuvwxyz"
|
|
499
|
-
"0123456789+/";
|
|
500
|
-
|
|
501
|
-
static std::array<int, 256> buildDecodeTable() {
|
|
502
|
-
std::array<int, 256> t;
|
|
503
|
-
t.fill(-1);
|
|
504
|
-
for (int i = 0; i < 64; ++i) {
|
|
505
|
-
t[static_cast<unsigned char>(kEncodeTable[i])] = i;
|
|
506
|
-
}
|
|
507
|
-
return t;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
std::string decodeBase64(const std::string& input) {
|
|
511
|
-
static const auto table = buildDecodeTable();
|
|
512
|
-
std::string out;
|
|
513
|
-
out.reserve((input.size() / 4) * 3);
|
|
514
|
-
|
|
515
|
-
std::uint32_t buffer = 0;
|
|
516
|
-
int bits = 0;
|
|
517
|
-
for (unsigned char c : input) {
|
|
518
|
-
if (c == '=') break;
|
|
519
|
-
int v = table[c];
|
|
520
|
-
if (v < 0) continue; // skip whitespace / newlines
|
|
521
|
-
buffer = (buffer << 6) | v;
|
|
522
|
-
bits += 6;
|
|
523
|
-
if (bits >= 8) {
|
|
524
|
-
bits -= 8;
|
|
525
|
-
out.push_back(static_cast<char>((buffer >> bits) & 0xFF));
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
return out;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
std::string encodeBase64(const std::string& input) {
|
|
532
|
-
std::string out;
|
|
533
|
-
const auto* data = reinterpret_cast<const unsigned char*>(input.data());
|
|
534
|
-
std::size_t len = input.size();
|
|
535
|
-
out.reserve(((len + 2) / 3) * 4);
|
|
536
|
-
|
|
537
|
-
std::size_t i = 0;
|
|
538
|
-
while (i + 3 <= len) {
|
|
539
|
-
std::uint32_t n = (data[i] << 16) | (data[i + 1] << 8) | data[i + 2];
|
|
540
|
-
out.push_back(kEncodeTable[(n >> 18) & 0x3F]);
|
|
541
|
-
out.push_back(kEncodeTable[(n >> 12) & 0x3F]);
|
|
542
|
-
out.push_back(kEncodeTable[(n >> 6) & 0x3F]);
|
|
543
|
-
out.push_back(kEncodeTable[n & 0x3F]);
|
|
544
|
-
i += 3;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
if (std::size_t rem = len - i; rem > 0) {
|
|
548
|
-
std::uint32_t n = data[i] << 16;
|
|
549
|
-
if (rem == 2) n |= data[i + 1] << 8;
|
|
550
|
-
out.push_back(kEncodeTable[(n >> 18) & 0x3F]);
|
|
551
|
-
out.push_back(kEncodeTable[(n >> 12) & 0x3F]);
|
|
552
|
-
out.push_back(rem == 2 ? kEncodeTable[(n >> 6) & 0x3F] : '=');
|
|
553
|
-
out.push_back('=');
|
|
554
|
-
}
|
|
555
|
-
return out;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
std::string extractMimeType(const std::string& contentTypeHeader) {
|
|
559
|
-
// Get just the type/subtype (e.g. "image/png") before any semicolons
|
|
560
|
-
std::string::size_type semi = contentTypeHeader.find(';');
|
|
561
|
-
std::string mimeType = (semi != std::string::npos)
|
|
562
|
-
? contentTypeHeader.substr(0, semi)
|
|
563
|
-
: contentTypeHeader;
|
|
564
|
-
// Trim whitespace
|
|
565
|
-
while (!mimeType.empty() && (mimeType.back() == ' ' || mimeType.back() == '\t')) {
|
|
566
|
-
mimeType.pop_back();
|
|
567
|
-
}
|
|
568
|
-
while (!mimeType.empty() && (mimeType.front() == ' ' || mimeType.front() == '\t')) {
|
|
569
|
-
mimeType.erase(mimeType.begin());
|
|
570
|
-
}
|
|
571
|
-
return mimeType;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
std::string extractContentId(const std::string& contentIdHeader) {
|
|
575
|
-
// Strip angle brackets: "<ii_mrnoxu771>" -> "ii_mrnoxu771"
|
|
576
|
-
std::string cid = contentIdHeader;
|
|
577
|
-
if (!cid.empty() && cid.front() == '<') {
|
|
578
|
-
cid.erase(cid.begin());
|
|
579
|
-
}
|
|
580
|
-
if (!cid.empty() && cid.back() == '>') {
|
|
581
|
-
cid.pop_back();
|
|
582
|
-
}
|
|
583
|
-
return cid;
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
std::string extractFilename(const std::string& contentDisposition,
|
|
587
|
-
const std::string& contentType) {
|
|
588
|
-
// Try Content-Disposition filename= first
|
|
589
|
-
auto extractParam = [](const std::string& header, const std::string& param) -> std::string {
|
|
590
|
-
std::string lower = header;
|
|
591
|
-
std::transform(lower.begin(), lower.end(), lower.begin(),
|
|
592
|
-
[](unsigned char c) { return std::tolower(c); });
|
|
593
|
-
std::string::size_type pos = lower.find(param + "=");
|
|
594
|
-
if (pos == std::string::npos) return "";
|
|
595
|
-
pos += param.size() + 1;
|
|
596
|
-
if (pos < header.size() && header[pos] == '"') {
|
|
597
|
-
pos++;
|
|
598
|
-
std::string::size_type end = header.find('"', pos);
|
|
599
|
-
if (end != std::string::npos) return header.substr(pos, end - pos);
|
|
600
|
-
} else {
|
|
601
|
-
std::string::size_type end = pos;
|
|
602
|
-
while (end < header.size() && header[end] != ';' &&
|
|
603
|
-
header[end] != ' ' && header[end] != '\r' && header[end] != '\n') {
|
|
604
|
-
end++;
|
|
605
|
-
}
|
|
606
|
-
return header.substr(pos, end - pos);
|
|
607
|
-
}
|
|
608
|
-
return "";
|
|
609
|
-
};
|
|
610
|
-
|
|
611
|
-
std::string filename = extractParam(contentDisposition, "filename");
|
|
612
|
-
if (filename.empty()) {
|
|
613
|
-
filename = extractParam(contentType, "name");
|
|
614
|
-
}
|
|
615
|
-
return filename;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
std::string replaceCidWithBase64(const std::string& html,
|
|
619
|
-
const std::vector<MimePart>& parts) {
|
|
620
|
-
std::string result = html;
|
|
621
|
-
|
|
622
|
-
for (const auto& part : parts) {
|
|
623
|
-
if (part.contentId.empty()) continue;
|
|
624
|
-
|
|
625
|
-
// Build the cid: reference to search for
|
|
626
|
-
std::string cidRef = "cid:" + part.contentId;
|
|
627
|
-
|
|
628
|
-
// Check if this cid is referenced in the HTML
|
|
629
|
-
if (result.find(cidRef) == std::string::npos) continue;
|
|
630
|
-
|
|
631
|
-
// Decode the attachment body (typically base64-encoded in email)
|
|
632
|
-
std::string rawData;
|
|
633
|
-
std::string enc = part.transferEncoding;
|
|
634
|
-
std::transform(enc.begin(), enc.end(), enc.begin(),
|
|
635
|
-
[](unsigned char c) { return std::tolower(c); });
|
|
636
|
-
if (enc.find("base64") != std::string::npos) {
|
|
637
|
-
rawData = decodeBase64(part.body);
|
|
638
|
-
} else {
|
|
639
|
-
rawData = part.body;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
// Re-encode to base64 (clean, no line breaks) for data URI
|
|
643
|
-
std::string base64Data = encodeBase64(rawData);
|
|
644
|
-
|
|
645
|
-
// Build data URI: data:image/png;base64,iVBOR...
|
|
646
|
-
std::string mimeType = extractMimeType(part.contentType);
|
|
647
|
-
std::string dataUri = "data:" + mimeType + ";base64," + base64Data;
|
|
648
|
-
|
|
649
|
-
// Replace all occurrences of cid:xxx with the data URI
|
|
650
|
-
std::string::size_type pos = 0;
|
|
651
|
-
while ((pos = result.find(cidRef, pos)) != std::string::npos) {
|
|
652
|
-
result.replace(pos, cidRef.size(), dataUri);
|
|
653
|
-
pos += dataUri.size();
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
return result;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
} // namespace detail
|
|
661
|
-
|
|
662
|
-
} // namespace liteEmailParser
|