lite-email-parser 1.0.1 → 1.0.2

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/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # Lite email parser
2
+
3
+ Simple library to remove signature and replies and extract attachments and inline attachments from an email.
4
+
5
+ ## Import
6
+
7
+ `const { parseEmail } = require('lite-email-parser');`
8
+
9
+ `import { parseEmail } from 'lite-email-parser';`
10
+
11
+
12
+ ## Usage
13
+
14
+ ### No file upload
15
+
16
+ ```
17
+ // Example: Simple usage without uploading attachments
18
+ const { parseEmail } = require('lite-email-parser');
19
+ const fs = require('fs');
20
+
21
+ // Load your email (string or Buffer accepted here)
22
+ const email = fs.readFileSync('email.eml');
23
+
24
+ const result = await parseEmail(email);
25
+
26
+ // lastMessage: Pure HTML content with replies/signatures stripped
27
+ console.log(result.lastMessage);
28
+
29
+ // attachments: Array of IFile objects
30
+ console.log(result.attachments);
31
+
32
+ // inlineAttachments: Array of IFile objects embedded in the email
33
+ console.log(result.inlineAttachments);
34
+ ```
35
+
36
+ ### File upload
37
+
38
+ ```
39
+ const { parseEmail } = require('lite-email-parser');
40
+ const fs = require('fs');
41
+
42
+ // Load your email (string or Buffer accepted here)
43
+ const email = fs.readFileSync('email.eml');
44
+
45
+ const result = await parseEmail(email);
46
+
47
+ // lastMessage: Pure HTML content with replies/signatures stripped
48
+ console.log(result.lastMessage);
49
+
50
+ // attachments: Array of IFile objects
51
+ console.log(result.attachments);
52
+
53
+ // inlineAttachments: Array of IFile objects embedded in the email
54
+ console.log(result.inlineAttachments);
55
+ ```
@@ -0,0 +1,3 @@
1
+ export { parseEmail, replaceSrc } from './parser.js';
2
+ export type { IFile, ParseEmailResult } from './types.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAGpD,YAAY,EAAE,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA"}
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.replaceSrc = exports.parseEmail = void 0;
4
+ // Public API
5
+ var parser_js_1 = require("./parser.js");
6
+ Object.defineProperty(exports, "parseEmail", { enumerable: true, get: function () { return parser_js_1.parseEmail; } });
7
+ Object.defineProperty(exports, "replaceSrc", { enumerable: true, get: function () { return parser_js_1.replaceSrc; } });
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
@@ -0,0 +1,4 @@
1
+ import { IFile, ParseEmailResult } from './types.js';
2
+ export declare function parseEmail(buffer: Buffer | string): Promise<ParseEmailResult>;
3
+ export declare function replaceSrc(html: string, files: IFile[]): string;
4
+ //# sourceMappingURL=parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/parser.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAErD,wBAAsB,UAAU,CAC9B,MAAM,EAAE,MAAM,GAAG,MAAM,GACtB,OAAO,CAAC,gBAAgB,CAAC,CAkB3B;AAuDD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,MAAM,CAW/D"}
@@ -0,0 +1,204 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.parseEmail = parseEmail;
37
+ exports.replaceSrc = replaceSrc;
38
+ const mailparser_1 = require("mailparser");
39
+ const cheerio = __importStar(require("cheerio"));
40
+ async function parseEmail(buffer) {
41
+ const parsedEmail = await (0, mailparser_1.simpleParser)(buffer);
42
+ let content = parsedEmail.html;
43
+ if (!content) {
44
+ content = parsedEmail.text || '';
45
+ }
46
+ const cleanedContent = extractFirstMessage(content);
47
+ const attachments = await attachmentsToFiles(parsedEmail.attachments || []);
48
+ const inlineAttachments = inlineAttachmentsToFiles(cleanedContent);
49
+ return {
50
+ lastMessage: cleanedContent,
51
+ attachments,
52
+ inlineAttachments,
53
+ };
54
+ }
55
+ async function attachmentsToFiles(attach) {
56
+ const files = [];
57
+ for (const att of attach) {
58
+ const filename = att.filename || new Date().toDateString();
59
+ const contentType = att.contentType || 'application/octet-stream';
60
+ const attachment = {
61
+ name: filename,
62
+ type: contentType,
63
+ size: att.size,
64
+ buffer: att.content,
65
+ };
66
+ files.push(attachment);
67
+ }
68
+ return files;
69
+ }
70
+ function inlineAttachmentsToFiles(content) {
71
+ const files = [];
72
+ const $ = cheerio.load(content);
73
+ // Base64 embedded images — extract them as IFile entries
74
+ const base64Images = $('img[src^="data:"]').toArray();
75
+ for (const el of base64Images) {
76
+ const src = $(el).attr('src');
77
+ if (!src)
78
+ continue;
79
+ const matches = src.match(/^data:(image\/(\w+));base64,(.*)$/);
80
+ if (matches) {
81
+ const contentType = matches[1];
82
+ const extension = matches[2];
83
+ const base64Data = matches[3];
84
+ const buffer = Buffer.from(base64Data, 'base64');
85
+ files.push({
86
+ name: `inline-${new Date().toISOString()}.${extension}`,
87
+ src: src, // if we upload to cloud, replace src
88
+ type: contentType,
89
+ size: buffer.byteLength,
90
+ buffer,
91
+ originalSrc: src,
92
+ });
93
+ }
94
+ }
95
+ return files;
96
+ }
97
+ // Replace src attributes with the uploaded file URLs
98
+ function replaceSrc(html, files) {
99
+ const $ = cheerio.load(html);
100
+ for (const file of files) {
101
+ if (file.originalSrc && file.src) {
102
+ $(`img[src="${file.originalSrc}"]`).attr('src', file.src);
103
+ }
104
+ }
105
+ return $.html();
106
+ }
107
+ function extractFirstMessage(html) {
108
+ const $ = cheerio.load(html);
109
+ // Remove text dividers (e.g. ----- Original Message -----) and everything after them
110
+ let dividerNode = null;
111
+ let isTextNode = false;
112
+ function findDividerNode(parent) {
113
+ const contents = $(parent).contents();
114
+ for (let i = 0; i < contents.length; i++) {
115
+ const node = contents[i];
116
+ if (node.type === 'text') {
117
+ const text = $(node).text();
118
+ const match = text.match(/-{5,}.*?-{5,}/);
119
+ if (match) {
120
+ const index = text.indexOf(match[0]);
121
+ node.data = text.substring(0, index);
122
+ dividerNode = node;
123
+ isTextNode = true;
124
+ return true;
125
+ }
126
+ }
127
+ else if (node.type === 'tag') {
128
+ if (node.name !== 'style' && node.name !== 'script') {
129
+ const clone = $(node).clone();
130
+ clone.find('style, script').remove();
131
+ if (/-{5,}.*?-{5,}/.test(clone.text())) {
132
+ const foundInChild = findDividerNode(node);
133
+ if (!foundInChild) {
134
+ dividerNode = node;
135
+ isTextNode = false;
136
+ }
137
+ return true;
138
+ }
139
+ }
140
+ }
141
+ }
142
+ return false;
143
+ }
144
+ findDividerNode($('body'));
145
+ if (dividerNode) {
146
+ let current = $(dividerNode);
147
+ let isFirst = true;
148
+ while (current.length > 0 && !current.is('body')) {
149
+ current.nextAll().remove();
150
+ const parent = current.parent();
151
+ if (isFirst) {
152
+ if (!isTextNode) {
153
+ current.remove();
154
+ }
155
+ isFirst = false;
156
+ }
157
+ current = parent;
158
+ }
159
+ }
160
+ // Gmail signature
161
+ $('span.gmail_signature_prefix').remove();
162
+ $('div.gmail_signature').remove();
163
+ // Gmail quoted replies
164
+ $('.gmail_chip').remove();
165
+ $('.gmail_quote').nextAll().remove();
166
+ $('.gmail_quote').remove();
167
+ // Outlook signature / forwarded messages
168
+ $('#divRplyFwdMsg').nextAll().remove();
169
+ $('#divRplyFwdMsg').remove();
170
+ // Outlook signature marker: <div id="Signature">
171
+ $('div#Signature').remove();
172
+ // Apple Mail signature
173
+ // Apple Mail wraps signatures in <div dir="ltr" class="AppleMailSignature">
174
+ $('div.AppleMailSignature').remove();
175
+ // Apple Mail quoted replies
176
+ // Apple Mail wraps quoted content in <blockquote type="cite">
177
+ $('blockquote[type="cite"]').nextAll().remove();
178
+ $('blockquote[type="cite"]').remove();
179
+ // Fallback for other email providers
180
+ // Matches any div whose id or class contains "signature" (case-insensitive)
181
+ $('div')
182
+ .filter((_, el) => {
183
+ const id = ($(el).attr('id') || '').toLowerCase();
184
+ const cls = ($(el).attr('class') || '').toLowerCase();
185
+ return id.includes('signature') || cls.includes('signature');
186
+ })
187
+ .remove();
188
+ // Fallback for quoted replies from other email providers
189
+ // Matches any div whose id or class contains "quote" or "reply" (case-insensitive)
190
+ $('div')
191
+ .filter((_, el) => {
192
+ const id = ($(el).attr('id') || '').toLowerCase();
193
+ const cls = ($(el).attr('class') || '').toLowerCase();
194
+ return (id.includes('quote') ||
195
+ id.includes('reply') ||
196
+ cls.includes('quote') ||
197
+ cls.includes('reply'));
198
+ })
199
+ .each((_, el) => {
200
+ $(el).nextAll().remove();
201
+ $(el).remove();
202
+ });
203
+ return $('body').html() || '';
204
+ }
@@ -0,0 +1,14 @@
1
+ export interface IFile {
2
+ name: string;
3
+ src?: string;
4
+ type: string;
5
+ size: number;
6
+ buffer: Buffer;
7
+ originalSrc?: string;
8
+ }
9
+ export interface ParseEmailResult {
10
+ lastMessage: string;
11
+ attachments: IFile[];
12
+ inlineAttachments: IFile[];
13
+ }
14
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,KAAK,EAAE,CAAC;IACrB,iBAAiB,EAAE,KAAK,EAAE,CAAC;CAC5B"}
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/dist/index.cjs ADDED
@@ -0,0 +1,190 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+
29
+ // src/index.ts
30
+ var index_exports = {};
31
+ __export(index_exports, {
32
+ parseEmail: () => parseEmail
33
+ });
34
+ module.exports = __toCommonJS(index_exports);
35
+
36
+ // src/parser.ts
37
+ var import_mailparser = require("mailparser");
38
+ var cheerio = __toESM(require("cheerio"), 1);
39
+ async function parseEmail(buffer, uploader, uploadInlineFilesWithSrc) {
40
+ const parsedEmail = await (0, import_mailparser.simpleParser)(buffer);
41
+ let content = parsedEmail.html;
42
+ if (!content) {
43
+ content = parsedEmail.text || "";
44
+ }
45
+ let cleanedContent = extractFirstMessage(content);
46
+ let attachments = [];
47
+ let inlineAttachments = [];
48
+ const pendingAttachments = await attachmentsToFiles(parsedEmail.attachments);
49
+ const pendingInline = inlineAttachmentsToFiles(cleanedContent);
50
+ if (uploader) {
51
+ attachments = await uploadFiles(pendingAttachments, uploader);
52
+ if (uploadInlineFilesWithSrc) {
53
+ inlineAttachments = await uploadFiles(pendingInline, uploader);
54
+ cleanedContent = replaceSrc(inlineAttachments, cleanedContent);
55
+ } else {
56
+ inlineAttachments = pendingInline;
57
+ }
58
+ }
59
+ return {
60
+ attachments,
61
+ inlineAttachments,
62
+ lastMessage: cleanedContent
63
+ };
64
+ }
65
+ async function attachmentsToFiles(attach) {
66
+ const files = [];
67
+ for (const att of attach) {
68
+ const filename = att.filename || (/* @__PURE__ */ new Date()).toDateString();
69
+ const contentType = att.contentType || "application/octet-stream";
70
+ const attachment = {
71
+ name: filename,
72
+ type: contentType,
73
+ size: att.size,
74
+ buffer: att.content
75
+ };
76
+ files.push(attachment);
77
+ }
78
+ return files;
79
+ }
80
+ function inlineAttachmentsToFiles(content) {
81
+ const files = [];
82
+ const $ = cheerio.load(content);
83
+ const base64Images = $('img[src^="data:"]').toArray();
84
+ for (const el of base64Images) {
85
+ const src = $(el).attr("src");
86
+ if (!src) continue;
87
+ const matches = src.match(/^data:(image\/(\w+));base64,(.*)$/);
88
+ if (matches) {
89
+ const contentType = matches[1];
90
+ const extension = matches[2];
91
+ const base64Data = matches[3];
92
+ const buffer = Buffer.from(base64Data, "base64");
93
+ files.push({
94
+ name: `inline-${(/* @__PURE__ */ new Date()).toISOString()}.${extension}`,
95
+ src,
96
+ // keep the data URI as the src
97
+ type: contentType,
98
+ size: buffer.byteLength,
99
+ buffer,
100
+ originalSrc: src
101
+ });
102
+ }
103
+ }
104
+ return files;
105
+ }
106
+ async function uploadFiles(files, uploader) {
107
+ const failedFiles = [];
108
+ const uploadedFiles = [];
109
+ for (const file of files) {
110
+ try {
111
+ const uploadedFile = await uploader({
112
+ buffer: file.buffer || file.src || "",
113
+ contentType: file.type,
114
+ filename: file.name || "unnamed"
115
+ });
116
+ const { buffer, ...uploadedFileObj } = file;
117
+ uploadedFiles.push({
118
+ ...uploadedFileObj,
119
+ src: uploadedFile
120
+ });
121
+ } catch (err) {
122
+ console.error(`Failed to upload file: ${file.name}`, err);
123
+ failedFiles.push(file);
124
+ }
125
+ }
126
+ if (failedFiles.length > 0) {
127
+ throw new Error(`Failed to upload: ${failedFiles.map((f) => f.name).join(", ")}`);
128
+ }
129
+ return uploadedFiles;
130
+ }
131
+ function replaceSrc(inlineAttachments, html) {
132
+ const $ = cheerio.load(html);
133
+ for (const file of inlineAttachments) {
134
+ if (file.originalSrc) {
135
+ $(`img[src="${file.originalSrc}"]`).attr("src", file.src);
136
+ }
137
+ }
138
+ return $.html();
139
+ }
140
+ function extractFirstMessage(html) {
141
+ const $ = cheerio.load(html);
142
+ $("span.gmail_signature_prefix").remove();
143
+ $("div.gmail_signature").remove();
144
+ $(".gmail_chip").remove();
145
+ $(".gmail_quote").nextAll().remove();
146
+ $(".gmail_quote").remove();
147
+ $("#divRplyFwdMsg").nextAll().remove();
148
+ $("#divRplyFwdMsg").remove();
149
+ $("div#Signature").remove();
150
+ $("div.AppleMailSignature").remove();
151
+ $('blockquote[type="cite"]').nextAll().remove();
152
+ $('blockquote[type="cite"]').remove();
153
+ $("div").filter((_, el) => {
154
+ const id = ($(el).attr("id") || "").toLowerCase();
155
+ const cls = ($(el).attr("class") || "").toLowerCase();
156
+ return id.includes("signature") || cls.includes("signature");
157
+ }).remove();
158
+ $("div").filter((_, el) => {
159
+ const id = ($(el).attr("id") || "").toLowerCase();
160
+ const cls = ($(el).attr("class") || "").toLowerCase();
161
+ return id.includes("quote") || id.includes("reply") || cls.includes("quote") || cls.includes("reply");
162
+ }).each((_, el) => {
163
+ $(el).nextAll().remove();
164
+ $(el).remove();
165
+ });
166
+ $("body").find("div, p, font, span, td").filter(function() {
167
+ if ($(this).children().not("br").length > 0) {
168
+ return false;
169
+ }
170
+ const elementText = $(this).text().toLowerCase();
171
+ return [
172
+ "this e-mail and any attachments may contain confidential",
173
+ "sent from"
174
+ ].some((text) => elementText.includes(text));
175
+ }).remove();
176
+ $("div").filter((_, el) => {
177
+ if ($(el).closest("li").length > 0) {
178
+ return false;
179
+ }
180
+ if ($(el).find("img").length > 0) {
181
+ return false;
182
+ }
183
+ return $(el).text().trim().length === 0 && $(el).children().not("br").length === 0;
184
+ }).remove();
185
+ return $("body").html() || "";
186
+ }
187
+ // Annotate the CommonJS export names for ESM import in node:
188
+ 0 && (module.exports = {
189
+ parseEmail
190
+ });
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- export * from './parser.js';
1
+ export { parseEmail, replaceSrc } from './parser.js';
2
+ export type { IFile, ParseEmailResult } from './types.js';
2
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAGpD,YAAY,EAAE,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA"}
package/dist/index.js CHANGED
@@ -1 +1,2 @@
1
- export * from './parser.js';
1
+ // Public API
2
+ export { parseEmail, replaceSrc } from './parser.js';
package/dist/parser.d.ts CHANGED
@@ -1,30 +1,4 @@
1
- import { ParsedMail } from "mailparser";
2
- export interface IFile {
3
- name?: string;
4
- url: string;
5
- type?: string;
6
- size?: number;
7
- tags?: Record<string, string>;
8
- }
9
- export interface UploadFileOptions {
10
- buffer: Buffer | string;
11
- contentType: string;
12
- filename: string;
13
- }
14
- export type UploadFunction = (file: UploadFileOptions) => Promise<string>;
15
- export declare function processInlineImages(html: string, cidMap: Map<string, string>, uploader: UploadFunction): Promise<string>;
16
- export declare function processEmailAttachments(parsedEmail: ParsedMail, uploader: UploadFunction): Promise<{
17
- attachments: IFile[];
18
- inlineMap: Map<string, string>;
19
- }>;
20
- export declare function extractFirstMessage(html: string): string;
21
- export interface ParseEmailOptions {
22
- buffer: Buffer | string;
23
- uploader: UploadFunction;
24
- }
25
- export interface ParseEmailResult {
26
- attachments: IFile[];
27
- response: string;
28
- }
29
- export declare function parseEmail(options: ParseEmailOptions): Promise<ParseEmailResult>;
1
+ import { IFile, ParseEmailResult } from './types.js';
2
+ export declare function parseEmail(buffer: Buffer | string): Promise<ParseEmailResult>;
3
+ export declare function replaceSrc(html: string, files: IFile[]): string;
30
4
  //# sourceMappingURL=parser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,UAAU,EAAE,MAAM,YAAY,CAAC;AAItD,MAAM,WAAW,KAAK;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,iBAAiB;IAC9B,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,cAAc,GAAG,CAAC,IAAI,EAAE,iBAAiB,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAE1E,wBAAsB,mBAAmB,CACrC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAC3B,QAAQ,EAAE,cAAc,GACzB,OAAO,CAAC,MAAM,CAAC,CA8CjB;AAED,wBAAsB,uBAAuB,CACzC,WAAW,EAAE,UAAU,EACvB,QAAQ,EAAE,cAAc,GACzB,OAAO,CAAC;IAAE,WAAW,EAAE,KAAK,EAAE,CAAC;IAAC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAAC,CA6CnE;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA0FxD;AAED,MAAM,WAAW,iBAAiB;IAC9B,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,QAAQ,EAAE,cAAc,CAAC;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAC7B,WAAW,EAAE,KAAK,EAAE,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAmBtF"}
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAErD,wBAAsB,UAAU,CAC9B,MAAM,EAAE,MAAM,GAAG,MAAM,GACtB,OAAO,CAAC,gBAAgB,CAAC,CAkB3B;AAuDD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,MAAM,CAW/D"}
package/dist/parser.js CHANGED
@@ -1,22 +1,39 @@
1
- import { simpleParser } from "mailparser";
2
- import * as cheerio from "cheerio";
3
- import { v4 as uuidv4 } from "uuid";
4
- export async function processInlineImages(html, cidMap, uploader) {
5
- const $ = cheerio.load(html);
6
- // remove width and height attributes from <img> tags
7
- $('img').each((_, el) => {
8
- $(el).removeAttr('width');
9
- $(el).removeAttr('height');
10
- });
11
- // replace inline attachments
12
- $('img[src^="cid:"]').each((_, el) => {
13
- const src = $(el).attr('src');
14
- const cid = src?.replace('cid:', '').trim();
15
- if (cid && cidMap.has(cid)) {
16
- $(el).attr('src', cidMap.get(cid));
17
- }
18
- });
19
- // handle base64 images
1
+ import { simpleParser } from 'mailparser';
2
+ import * as cheerio from 'cheerio';
3
+ export async function parseEmail(buffer) {
4
+ const parsedEmail = await simpleParser(buffer);
5
+ let content = parsedEmail.html;
6
+ if (!content) {
7
+ content = parsedEmail.text || '';
8
+ }
9
+ const cleanedContent = extractFirstMessage(content);
10
+ const attachments = await attachmentsToFiles(parsedEmail.attachments || []);
11
+ const inlineAttachments = inlineAttachmentsToFiles(cleanedContent);
12
+ return {
13
+ lastMessage: cleanedContent,
14
+ attachments,
15
+ inlineAttachments,
16
+ };
17
+ }
18
+ async function attachmentsToFiles(attach) {
19
+ const files = [];
20
+ for (const att of attach) {
21
+ const filename = att.filename || new Date().toDateString();
22
+ const contentType = att.contentType || 'application/octet-stream';
23
+ const attachment = {
24
+ name: filename,
25
+ type: contentType,
26
+ size: att.size,
27
+ buffer: att.content,
28
+ };
29
+ files.push(attachment);
30
+ }
31
+ return files;
32
+ }
33
+ function inlineAttachmentsToFiles(content) {
34
+ const files = [];
35
+ const $ = cheerio.load(content);
36
+ // Base64 embedded images — extract them as IFile entries
20
37
  const base64Images = $('img[src^="data:"]').toArray();
21
38
  for (const el of base64Images) {
22
39
  const src = $(el).attr('src');
@@ -25,65 +42,84 @@ export async function processInlineImages(html, cidMap, uploader) {
25
42
  const matches = src.match(/^data:(image\/(\w+));base64,(.*)$/);
26
43
  if (matches) {
27
44
  const contentType = matches[1];
45
+ const extension = matches[2];
28
46
  const base64Data = matches[3];
29
- const filename = `inline-${uuidv4()}`;
30
47
  const buffer = Buffer.from(base64Data, 'base64');
31
- try {
32
- const url = await uploader({
33
- buffer,
34
- contentType,
35
- filename
36
- });
37
- $(el).attr('src', url);
38
- }
39
- catch (err) {
40
- console.error(`Failed to upload base64 inline image: ${err}`);
41
- }
48
+ files.push({
49
+ name: `inline-${new Date().toISOString()}.${extension}`,
50
+ src: src, // if we upload to cloud, replace src
51
+ type: contentType,
52
+ size: buffer.byteLength,
53
+ buffer,
54
+ originalSrc: src,
55
+ });
56
+ }
57
+ }
58
+ return files;
59
+ }
60
+ // Replace src attributes with the uploaded file URLs
61
+ export function replaceSrc(html, files) {
62
+ const $ = cheerio.load(html);
63
+ for (const file of files) {
64
+ if (file.originalSrc && file.src) {
65
+ $(`img[src="${file.originalSrc}"]`).attr('src', file.src);
42
66
  }
43
67
  }
44
68
  return $.html();
45
69
  }
46
- export async function processEmailAttachments(parsedEmail, uploader) {
47
- const attachments = [];
48
- const inlineMap = new Map();
49
- if (parsedEmail.attachments && parsedEmail.attachments.length > 0) {
50
- for (const att of parsedEmail.attachments) {
51
- const filename = att.filename || uuidv4();
52
- const contentType = att.contentType || 'application/octet-stream';
53
- try {
54
- const publicUrl = await uploader({
55
- buffer: att.content,
56
- contentType,
57
- filename
58
- });
59
- const attachment = {
60
- name: att.filename,
61
- url: publicUrl,
62
- type: att.contentType,
63
- size: att.size,
64
- };
65
- attachment.tags = att.related
66
- ? { inlineAttachment: 'true' }
67
- : { attachment: 'true' };
68
- attachments.push(attachment);
69
- // Map Content-ID to URL (mailparser wraps CID in < >, strip them)
70
- if (att.contentId) {
71
- inlineMap.set(att.contentId.replace(/[<>]/g, ''), publicUrl);
70
+ function extractFirstMessage(html) {
71
+ const $ = cheerio.load(html);
72
+ // Remove text dividers (e.g. ----- Original Message -----) and everything after them
73
+ let dividerNode = null;
74
+ let isTextNode = false;
75
+ function findDividerNode(parent) {
76
+ const contents = $(parent).contents();
77
+ for (let i = 0; i < contents.length; i++) {
78
+ const node = contents[i];
79
+ if (node.type === 'text') {
80
+ const text = $(node).text();
81
+ const match = text.match(/-{5,}.*?-{5,}/);
82
+ if (match) {
83
+ const index = text.indexOf(match[0]);
84
+ node.data = text.substring(0, index);
85
+ dividerNode = node;
86
+ isTextNode = true;
87
+ return true;
72
88
  }
73
- // Also map by filename so we can match email-provider proxy URLs
74
- if (att.filename) {
75
- inlineMap.set(att.filename, publicUrl);
89
+ }
90
+ else if (node.type === 'tag') {
91
+ if (node.name !== 'style' && node.name !== 'script') {
92
+ const clone = $(node).clone();
93
+ clone.find('style, script').remove();
94
+ if (/-{5,}.*?-{5,}/.test(clone.text())) {
95
+ const foundInChild = findDividerNode(node);
96
+ if (!foundInChild) {
97
+ dividerNode = node;
98
+ isTextNode = false;
99
+ }
100
+ return true;
101
+ }
76
102
  }
77
103
  }
78
- catch (error) {
79
- console.error(`Failed to upload attachment ${filename}: ${error}`);
104
+ }
105
+ return false;
106
+ }
107
+ findDividerNode($('body'));
108
+ if (dividerNode) {
109
+ let current = $(dividerNode);
110
+ let isFirst = true;
111
+ while (current.length > 0 && !current.is('body')) {
112
+ current.nextAll().remove();
113
+ const parent = current.parent();
114
+ if (isFirst) {
115
+ if (!isTextNode) {
116
+ current.remove();
117
+ }
118
+ isFirst = false;
80
119
  }
120
+ current = parent;
81
121
  }
82
122
  }
83
- return { attachments, inlineMap };
84
- }
85
- export function extractFirstMessage(html) {
86
- const $ = cheerio.load(html);
87
123
  // Gmail signature
88
124
  $('span.gmail_signature_prefix').remove();
89
125
  $('div.gmail_signature').remove();
@@ -127,52 +163,5 @@ export function extractFirstMessage(html) {
127
163
  $(el).nextAll().remove();
128
164
  $(el).remove();
129
165
  });
130
- // Remove confidentiality disclaimers
131
- $('body')
132
- .find('div, p, font, span, td')
133
- .filter(function () {
134
- // Skip if this element has child elements that aren't just <br>
135
- if ($(this).children().not('br').length > 0) {
136
- return false;
137
- }
138
- const elementText = $(this).text().toLowerCase();
139
- return [
140
- 'this e-mail and any attachments may contain confidential',
141
- 'sent from'
142
- ].some((text) => elementText.includes(text));
143
- })
144
- .remove();
145
- // Clean empty divs
146
- $('div')
147
- .filter((_, el) => {
148
- // Avoid removing divs that are direct children of list items, as this can break bullet points
149
- if ($(el).closest('li').length > 0) {
150
- return false;
151
- }
152
- // Avoid removing divs that contain images (inline images, etc.)
153
- if ($(el).find('img').length > 0) {
154
- return false;
155
- }
156
- return ($(el).text().trim().length === 0 &&
157
- $(el).children().not('br').length === 0);
158
- })
159
- .remove();
160
166
  return $('body').html() || '';
161
167
  }
162
- export async function parseEmail(options) {
163
- const { buffer, uploader } = options;
164
- const parsedEmail = await simpleParser(buffer);
165
- const { attachments, inlineMap } = await processEmailAttachments(parsedEmail, uploader);
166
- let response = parsedEmail.html;
167
- if (response) {
168
- response = await processInlineImages(response, inlineMap, uploader);
169
- response = extractFirstMessage(response);
170
- }
171
- else {
172
- response = parsedEmail.text || '';
173
- }
174
- return {
175
- attachments,
176
- response,
177
- };
178
- }
@@ -0,0 +1,14 @@
1
+ export interface IFile {
2
+ name: string;
3
+ src?: string;
4
+ type: string;
5
+ size: number;
6
+ buffer: Buffer;
7
+ originalSrc?: string;
8
+ }
9
+ export interface ParseEmailResult {
10
+ lastMessage: string;
11
+ attachments: IFile[];
12
+ inlineAttachments: IFile[];
13
+ }
14
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,KAAK,EAAE,CAAC;IACrB,iBAAiB,EAAE,KAAK,EAAE,CAAC;CAC5B"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,27 +1,38 @@
1
1
  {
2
2
  "name": "lite-email-parser",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Parse email to get signature, replies, attachments and inline images",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
5
+ "main": "./dist/cjs/index.js",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "require": {
15
+ "types": "./dist/cjs/index.d.ts",
16
+ "default": "./dist/cjs/index.js"
17
+ }
18
+ }
19
+ },
7
20
  "files": [
8
21
  "dist/"
9
22
  ],
10
23
  "scripts": {
11
- "build": "npx tsc",
12
- "start": "node ./dist/src/index.js",
24
+ "build": "npx tsc && npx tsc --module commonjs --outDir dist/cjs --declaration && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json",
25
+ "start": "node ./dist/index.js",
13
26
  "test": "npx -y tsx sample/sample.ts"
14
27
  },
15
28
  "author": "philipedc",
16
29
  "type": "module",
17
30
  "dependencies": {
18
31
  "cheerio": "^1.2.0",
19
- "mailparser": "^3.9.14",
20
- "uuid": "^14.0.1"
32
+ "mailparser": "^3.9.14"
21
33
  },
22
34
  "devDependencies": {
23
35
  "@types/mailparser": "^3.4.6",
24
- "@types/uuid": "^10.0.0",
25
36
  "typescript": "^7.0.2"
26
37
  }
27
- }
38
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":""}
package/dist/src/index.js DELETED
@@ -1,2 +0,0 @@
1
- console.log("hi");
2
- export {};