lite-email-parser 2.0.0 → 2.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 (52) hide show
  1. package/binding.gyp +20 -6
  2. package/core/include/html_extractor.h +100 -0
  3. package/core/include/html_parser.h +13 -0
  4. package/core/src/html_extractor.cpp +662 -0
  5. package/core/src/html_parser.cpp +221 -0
  6. package/deps/gumbo-parser/src/attribute.c +44 -0
  7. package/deps/gumbo-parser/src/attribute.h +37 -0
  8. package/deps/gumbo-parser/src/char_ref.c +301 -0
  9. package/deps/gumbo-parser/src/char_ref.h +70 -0
  10. package/deps/gumbo-parser/src/char_ref_gperf.c +11126 -0
  11. package/deps/gumbo-parser/src/error.c +287 -0
  12. package/deps/gumbo-parser/src/error.h +225 -0
  13. package/deps/gumbo-parser/src/gumbo.h +683 -0
  14. package/deps/gumbo-parser/src/insertion_mode.h +55 -0
  15. package/deps/gumbo-parser/src/parser.c +4433 -0
  16. package/deps/gumbo-parser/src/parser.h +57 -0
  17. package/deps/gumbo-parser/src/string_buffer.c +110 -0
  18. package/deps/gumbo-parser/src/string_buffer.h +84 -0
  19. package/deps/gumbo-parser/src/string_piece.c +48 -0
  20. package/deps/gumbo-parser/src/string_piece.h +38 -0
  21. package/deps/gumbo-parser/src/tag.c +125 -0
  22. package/deps/gumbo-parser/src/tag_enum.h +155 -0
  23. package/deps/gumbo-parser/src/tag_gperf.h +350 -0
  24. package/deps/gumbo-parser/src/tag_sizes.h +2 -0
  25. package/deps/gumbo-parser/src/tag_strings.h +155 -0
  26. package/deps/gumbo-parser/src/token_type.h +42 -0
  27. package/deps/gumbo-parser/src/tokenizer.c +3003 -0
  28. package/deps/gumbo-parser/src/tokenizer.h +123 -0
  29. package/deps/gumbo-parser/src/tokenizer_states.h +104 -0
  30. package/deps/gumbo-parser/src/utf8.c +270 -0
  31. package/deps/gumbo-parser/src/utf8.h +132 -0
  32. package/deps/gumbo-parser/src/util.c +58 -0
  33. package/deps/gumbo-parser/src/util.h +60 -0
  34. package/deps/gumbo-parser/src/vector.c +128 -0
  35. package/deps/gumbo-parser/src/vector.h +70 -0
  36. package/dist/cjs/index.cjs +63 -0
  37. package/dist/cjs/index.d.cts +9 -0
  38. package/dist/cjs/index.d.cts.map +1 -0
  39. package/dist/cjs/index.d.ts.map +1 -1
  40. package/dist/cjs/index.js +32 -3
  41. package/dist/cjs/parser.d.ts +4 -0
  42. package/dist/cjs/parser.d.ts.map +1 -0
  43. package/dist/cjs/parser.js +118 -0
  44. package/dist/index.cjs +63 -0
  45. package/dist/index.d.cts +9 -0
  46. package/dist/index.d.cts.map +1 -0
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +32 -3
  49. package/dist/parser.d.ts +4 -0
  50. package/dist/parser.d.ts.map +1 -0
  51. package/dist/parser.js +81 -0
  52. package/package.json +11 -3
@@ -0,0 +1,662 @@
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