lite-email-parser 1.0.0 → 1.0.1

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.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export {};
1
+ export * from './parser.js';
2
2
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA"}
package/dist/index.js CHANGED
@@ -1,2 +1 @@
1
- console.log("hi");
2
- export {};
1
+ export * from './parser.js';
@@ -0,0 +1,30 @@
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>;
30
+ //# sourceMappingURL=parser.d.ts.map
@@ -0,0 +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"}
package/dist/parser.js ADDED
@@ -0,0 +1,178 @@
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
20
+ const base64Images = $('img[src^="data:"]').toArray();
21
+ for (const el of base64Images) {
22
+ const src = $(el).attr('src');
23
+ if (!src)
24
+ continue;
25
+ const matches = src.match(/^data:(image\/(\w+));base64,(.*)$/);
26
+ if (matches) {
27
+ const contentType = matches[1];
28
+ const base64Data = matches[3];
29
+ const filename = `inline-${uuidv4()}`;
30
+ 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
+ }
42
+ }
43
+ }
44
+ return $.html();
45
+ }
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);
72
+ }
73
+ // Also map by filename so we can match email-provider proxy URLs
74
+ if (att.filename) {
75
+ inlineMap.set(att.filename, publicUrl);
76
+ }
77
+ }
78
+ catch (error) {
79
+ console.error(`Failed to upload attachment ${filename}: ${error}`);
80
+ }
81
+ }
82
+ }
83
+ return { attachments, inlineMap };
84
+ }
85
+ export function extractFirstMessage(html) {
86
+ const $ = cheerio.load(html);
87
+ // Gmail signature
88
+ $('span.gmail_signature_prefix').remove();
89
+ $('div.gmail_signature').remove();
90
+ // Gmail quoted replies
91
+ $('.gmail_chip').remove();
92
+ $('.gmail_quote').nextAll().remove();
93
+ $('.gmail_quote').remove();
94
+ // Outlook signature / forwarded messages
95
+ $('#divRplyFwdMsg').nextAll().remove();
96
+ $('#divRplyFwdMsg').remove();
97
+ // Outlook signature marker: <div id="Signature">
98
+ $('div#Signature').remove();
99
+ // Apple Mail signature
100
+ // Apple Mail wraps signatures in <div dir="ltr" class="AppleMailSignature">
101
+ $('div.AppleMailSignature').remove();
102
+ // Apple Mail quoted replies
103
+ // Apple Mail wraps quoted content in <blockquote type="cite">
104
+ $('blockquote[type="cite"]').nextAll().remove();
105
+ $('blockquote[type="cite"]').remove();
106
+ // Fallback for other email providers
107
+ // Matches any div whose id or class contains "signature" (case-insensitive)
108
+ $('div')
109
+ .filter((_, el) => {
110
+ const id = ($(el).attr('id') || '').toLowerCase();
111
+ const cls = ($(el).attr('class') || '').toLowerCase();
112
+ return id.includes('signature') || cls.includes('signature');
113
+ })
114
+ .remove();
115
+ // Fallback for quoted replies from other email providers
116
+ // Matches any div whose id or class contains "quote" or "reply" (case-insensitive)
117
+ $('div')
118
+ .filter((_, el) => {
119
+ const id = ($(el).attr('id') || '').toLowerCase();
120
+ const cls = ($(el).attr('class') || '').toLowerCase();
121
+ return (id.includes('quote') ||
122
+ id.includes('reply') ||
123
+ cls.includes('quote') ||
124
+ cls.includes('reply'));
125
+ })
126
+ .each((_, el) => {
127
+ $(el).nextAll().remove();
128
+ $(el).remove();
129
+ });
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
+ return $('body').html() || '';
161
+ }
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lite-email-parser",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Parse email to get signature, replies, attachments and inline images",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -9,8 +9,19 @@
9
9
  ],
10
10
  "scripts": {
11
11
  "build": "npx tsc",
12
- "start": "node ./dist/src/index.js"
12
+ "start": "node ./dist/src/index.js",
13
+ "test": "npx -y tsx sample/sample.ts"
13
14
  },
14
15
  "author": "philipedc",
15
- "type": "module"
16
+ "type": "module",
17
+ "dependencies": {
18
+ "cheerio": "^1.2.0",
19
+ "mailparser": "^3.9.14",
20
+ "uuid": "^14.0.1"
21
+ },
22
+ "devDependencies": {
23
+ "@types/mailparser": "^3.4.6",
24
+ "@types/uuid": "^10.0.0",
25
+ "typescript": "^7.0.2"
26
+ }
16
27
  }