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 +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/dist/parser.d.ts +30 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +178 -0
- package/package.json +14 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export
|
|
1
|
+
export * from './parser.js';
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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
|
-
|
|
2
|
-
export {};
|
|
1
|
+
export * from './parser.js';
|
package/dist/parser.d.ts
ADDED
|
@@ -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.
|
|
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
|
}
|