@postalsys/email-text-tools 1.4.1 → 2.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/index.js +1 -5
- package/lib/html-to-text.js +3 -1
- package/package.json +5 -9
- package/examples/risk.js +0 -85
- package/examples/summary.js +0 -31
- package/lib/generate-summary.js +0 -214
- package/lib/risk-analysis.js +0 -240
package/index.js
CHANGED
|
@@ -5,15 +5,11 @@ const htmlToText = require('./lib/html-to-text');
|
|
|
5
5
|
const inlineHtml = require('./lib/inline-html');
|
|
6
6
|
const inlineText = require('./lib/inline-text');
|
|
7
7
|
const mimeHtml = require('./lib/mime-html');
|
|
8
|
-
const generateSummary = require('./lib/generate-summary');
|
|
9
|
-
const riskAnalysis = require('./lib/risk-analysis');
|
|
10
8
|
|
|
11
9
|
module.exports = {
|
|
12
10
|
textToHtml,
|
|
13
11
|
htmlToText,
|
|
14
12
|
inlineHtml,
|
|
15
13
|
inlineText,
|
|
16
|
-
mimeHtml
|
|
17
|
-
generateSummary,
|
|
18
|
-
riskAnalysis
|
|
14
|
+
mimeHtml
|
|
19
15
|
};
|
package/lib/html-to-text.js
CHANGED
|
@@ -6,7 +6,9 @@ const htmlToText = compile({
|
|
|
6
6
|
selectors: [
|
|
7
7
|
{ selector: 'p', options: { leadingLineBreaks: 1, trailingLineBreaks: 1 } },
|
|
8
8
|
{ selector: 'pre', options: { leadingLineBreaks: 1, trailingLineBreaks: 1 } },
|
|
9
|
-
{ selector: 'table.class#id', format: 'dataTable' }
|
|
9
|
+
{ selector: 'table.class#id', format: 'dataTable' },
|
|
10
|
+
// ignore images
|
|
11
|
+
{ selector: 'img', format: 'skip' }
|
|
10
12
|
],
|
|
11
13
|
hideLinkHrefIfSameAsText: true
|
|
12
14
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@postalsys/email-text-tools",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Email text tools",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -19,22 +19,18 @@
|
|
|
19
19
|
"homepage": "https://github.com/postalsys/email-text-tools#readme",
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"eslint-config-nodemailer": "1.2.0",
|
|
22
|
-
"eslint-config-prettier": "8.8.0"
|
|
23
|
-
"mailparser": "3.6.4"
|
|
22
|
+
"eslint-config-prettier": "8.8.0"
|
|
24
23
|
},
|
|
25
24
|
"dependencies": {
|
|
26
|
-
"dompurify": "3.0.
|
|
27
|
-
"gpt3-tokenizer": "1.1.5",
|
|
28
|
-
"handlebars": "4.7.7",
|
|
25
|
+
"dompurify": "3.0.3",
|
|
29
26
|
"he": "1.2.0",
|
|
30
27
|
"html-to-text": "9.0.5",
|
|
31
28
|
"js-beautify": "1.14.7",
|
|
32
|
-
"jsdom": "
|
|
29
|
+
"jsdom": "22.0.0",
|
|
33
30
|
"juice": "9.0.0",
|
|
34
31
|
"linkify-it": "4.0.1",
|
|
35
32
|
"moment-timezone": "0.5.43",
|
|
36
33
|
"node-html-parser": "6.1.5",
|
|
37
|
-
"tlds": "1.238.0"
|
|
38
|
-
"undici": "5.22.0"
|
|
34
|
+
"tlds": "1.238.0"
|
|
39
35
|
}
|
|
40
36
|
}
|
package/examples/risk.js
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs').promises;
|
|
4
|
-
const { riskAnalysis } = require('..');
|
|
5
|
-
const simpleParser = require('mailparser').simpleParser;
|
|
6
|
-
const libmime = require('libmime');
|
|
7
|
-
const Path = require('path');
|
|
8
|
-
|
|
9
|
-
const BASE_PATH = process.argv[2] || '.';
|
|
10
|
-
const MAX_ENTRIES = Math.abs(Number(process.argv[3]) || 0);
|
|
11
|
-
|
|
12
|
-
async function analyzeEmail(path) {
|
|
13
|
-
const eml = await fs.readFile(path);
|
|
14
|
-
|
|
15
|
-
const parsed = await simpleParser(eml);
|
|
16
|
-
|
|
17
|
-
const result = await riskAnalysis(
|
|
18
|
-
{
|
|
19
|
-
headers: parsed.headerLines.map(header => libmime.decodeHeader(header.line)),
|
|
20
|
-
attachments: parsed.attachments,
|
|
21
|
-
html: parsed.html,
|
|
22
|
-
text: parsed.text
|
|
23
|
-
},
|
|
24
|
-
process.env.OPENAI_API_KEY,
|
|
25
|
-
{
|
|
26
|
-
//gptModel: 'gpt-3.5-turbo'
|
|
27
|
-
gptModel: 'gpt-4',
|
|
28
|
-
maxTokens: 6000
|
|
29
|
-
}
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
result.file = Path.basename(path);
|
|
33
|
-
|
|
34
|
-
console.log(JSON.stringify(result));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function shuffleArray(array) {
|
|
38
|
-
for (let i = array.length - 1; i > 0; i--) {
|
|
39
|
-
let j = Math.floor(Math.random() * (i + 1));
|
|
40
|
-
let temp = array[i];
|
|
41
|
-
array[i] = array[j];
|
|
42
|
-
array[j] = temp;
|
|
43
|
-
}
|
|
44
|
-
return array;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function main() {
|
|
48
|
-
const stat = await fs.stat(BASE_PATH);
|
|
49
|
-
if (stat.isDirectory()) {
|
|
50
|
-
// process as directory
|
|
51
|
-
|
|
52
|
-
let processed = 0;
|
|
53
|
-
let list = await fs.readdir(BASE_PATH);
|
|
54
|
-
|
|
55
|
-
for (let file of shuffleArray(list)) {
|
|
56
|
-
if (Path.extname(file).toLowerCase() === '.eml') {
|
|
57
|
-
if (processed) {
|
|
58
|
-
process.stdout.write(',');
|
|
59
|
-
}
|
|
60
|
-
try {
|
|
61
|
-
await analyzeEmail(Path.join(BASE_PATH, file));
|
|
62
|
-
console.error(`Processed ${file} [${processed + 1}]`);
|
|
63
|
-
} catch (err) {
|
|
64
|
-
console.log(JSON.stringify({ file, error: err.message }));
|
|
65
|
-
console.error(`Failed processing ${file} [${processed + 1}]`);
|
|
66
|
-
console.error(err);
|
|
67
|
-
}
|
|
68
|
-
processed++;
|
|
69
|
-
|
|
70
|
-
if (MAX_ENTRIES && processed >= MAX_ENTRIES) {
|
|
71
|
-
break;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
console.error(`Processed ${processed} files from ${BASE_PATH}`);
|
|
76
|
-
} else {
|
|
77
|
-
// process as file
|
|
78
|
-
await analyzeEmail(BASE_PATH);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
console.log('[');
|
|
83
|
-
main().then(() => {
|
|
84
|
-
console.log(']');
|
|
85
|
-
});
|
package/examples/summary.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs').promises;
|
|
4
|
-
const generateSummary = require('../lib/generate-summary');
|
|
5
|
-
const simpleParser = require('mailparser').simpleParser;
|
|
6
|
-
const libmime = require('libmime');
|
|
7
|
-
|
|
8
|
-
async function main() {
|
|
9
|
-
const eml = await fs.readFile(process.argv[2]);
|
|
10
|
-
|
|
11
|
-
const parsed = await simpleParser(eml);
|
|
12
|
-
|
|
13
|
-
const summary = await generateSummary(
|
|
14
|
-
{
|
|
15
|
-
headers: parsed.headerLines.map(header => libmime.decodeHeader(header.line)),
|
|
16
|
-
attachments: parsed.attachments,
|
|
17
|
-
html: parsed.html,
|
|
18
|
-
text: parsed.text
|
|
19
|
-
},
|
|
20
|
-
process.env.OPENAI_API_KEY,
|
|
21
|
-
{
|
|
22
|
-
//gptModel: 'gpt-3.5-turbo'
|
|
23
|
-
gptModel: 'gpt-4',
|
|
24
|
-
maxTokens: 6000
|
|
25
|
-
}
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
console.log(summary);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
main();
|
package/lib/generate-summary.js
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const packageData = require('../package.json');
|
|
4
|
-
const htmlToText = require('../lib/html-to-text');
|
|
5
|
-
const GPT3Tokenizer = require('gpt3-tokenizer').default;
|
|
6
|
-
|
|
7
|
-
const { fetch: fetchCmd, Agent } = require('undici');
|
|
8
|
-
const fetchAgent = new Agent({ connect: { timeout: 90 * 1000 } });
|
|
9
|
-
|
|
10
|
-
const tokenizer = new GPT3Tokenizer({ type: 'gpt3' });
|
|
11
|
-
|
|
12
|
-
const MAX_ALLOWED_TEXT_LENGTH = 32 * 1024;
|
|
13
|
-
const MAX_ALLOWED_TOKENS = 4000;
|
|
14
|
-
const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions';
|
|
15
|
-
|
|
16
|
-
const SYSTEM_PROMPT = `
|
|
17
|
-
I want you to act as an email client that processes emails and highlights important parts.
|
|
18
|
-
You do not comment or explain anything.
|
|
19
|
-
`
|
|
20
|
-
.replace(/\s+/g, ' ')
|
|
21
|
-
.trim();
|
|
22
|
-
|
|
23
|
-
const USER_PROMPT = `
|
|
24
|
-
Instructions:
|
|
25
|
-
- You are an executive assistant scanning incoming emails to inform your superiors about what is important and what is not.
|
|
26
|
-
- Your task is to monitor and analyze incoming emails, which consist of the message headers, a list of attachments, and text content
|
|
27
|
-
- Describe the sentiment of the email using one word. Use either "positive", "neutral" or "negative". Include this value in the response as a "sentiment" property.
|
|
28
|
-
- Generate a one-sentence summary of the email. Include this value in the response as a "summary" property.
|
|
29
|
-
- Does it seem like the sender of the email would expect a reply to this email? Include this information in the response as a "shouldReply" property with the value "true" if they expect it and "false" if not.
|
|
30
|
-
- If this email is a reply to a previous email, then extract the text content that only the email's sender wrote, and include this as a "replyText" property in the response.
|
|
31
|
-
- Do not include message signatures in the extracted reply text
|
|
32
|
-
- Respond with a JSON formatted structure. Do not write any other explanations.
|
|
33
|
-
|
|
34
|
-
Facts:
|
|
35
|
-
- An email consists of message headers, an attachments list, and text content
|
|
36
|
-
- The email to analyze is formatted in a JSON format
|
|
37
|
-
- An email is a reply to a previous email only if it includes a "in-reply-to" header
|
|
38
|
-
- The email structure includes a property "headers" that contains an array of header values.
|
|
39
|
-
- Each header contains two properties, "key" as the header field key name and "value" as the header value without the key prefix
|
|
40
|
-
- The email structure includes a property "attachments" that contains an array of attachments.
|
|
41
|
-
- Each attachment includes a "filename" property that describes the file name, a "contentType" property that describes the Content-Type value of the attachment
|
|
42
|
-
- The email includes a "text" property for the text content
|
|
43
|
-
|
|
44
|
-
Analyze the following email:
|
|
45
|
-
`.trim();
|
|
46
|
-
|
|
47
|
-
async function generateSummary(message, apiToken, opts) {
|
|
48
|
-
opts = opts || {};
|
|
49
|
-
|
|
50
|
-
let maxAllowedTokens = opts.maxTokens || MAX_ALLOWED_TOKENS;
|
|
51
|
-
let gptModel = opts.gptModel || 'gpt-3.5-turbo';
|
|
52
|
-
|
|
53
|
-
let text = message.text || '';
|
|
54
|
-
if (message.html && (!text || message.html.length > text.length * 2)) {
|
|
55
|
-
text = htmlToText(message.html);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
let prompt;
|
|
59
|
-
|
|
60
|
-
const content = {
|
|
61
|
-
headers: [].concat(message.headers || []).filter(
|
|
62
|
-
// use a whitelist
|
|
63
|
-
header => ['from', 'to', 'cc', 'bcc', 'subject', 'mime-version', 'date', 'content-type', 'list-id'].includes(header.key)
|
|
64
|
-
),
|
|
65
|
-
attachments: [].concat(message.attachments || []).map(attachment => ({ filename: attachment.filename, contentType: attachment.contentType }))
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
let charactersRemoved = 0;
|
|
69
|
-
let promptText = text;
|
|
70
|
-
|
|
71
|
-
if (promptText.length > MAX_ALLOWED_TEXT_LENGTH) {
|
|
72
|
-
charactersRemoved += promptText.length - MAX_ALLOWED_TEXT_LENGTH;
|
|
73
|
-
promptText = promptText.substr(0, MAX_ALLOWED_TEXT_LENGTH);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
while (promptText.length) {
|
|
77
|
-
content.text = promptText;
|
|
78
|
-
prompt = `${USER_PROMPT}
|
|
79
|
-
|
|
80
|
-
${JSON.stringify(content)}`;
|
|
81
|
-
|
|
82
|
-
let tokens = tokenizer.encode(prompt);
|
|
83
|
-
if (tokens.text.length <= maxAllowedTokens) {
|
|
84
|
-
break;
|
|
85
|
-
}
|
|
86
|
-
if (promptText.length > 2 * 1024 * 1024) {
|
|
87
|
-
promptText = promptText.substring(0, promptText.length - 1024 * 1024);
|
|
88
|
-
charactersRemoved += 1024 * 1024;
|
|
89
|
-
} else if (promptText.length > 2 * 1024) {
|
|
90
|
-
promptText = promptText.substring(0, promptText.length - 1024);
|
|
91
|
-
charactersRemoved += 1024;
|
|
92
|
-
} else if (promptText.length > 2 * 256) {
|
|
93
|
-
promptText = promptText.substring(0, promptText.length - 256);
|
|
94
|
-
charactersRemoved += 255;
|
|
95
|
-
} else if (promptText.length > 2 * 100) {
|
|
96
|
-
promptText = promptText.substring(0, promptText.length - 100);
|
|
97
|
-
charactersRemoved += 100;
|
|
98
|
-
} else if (promptText.length > 2 * 10) {
|
|
99
|
-
promptText = promptText.substring(0, promptText.length - 10);
|
|
100
|
-
charactersRemoved += 10;
|
|
101
|
-
} else if (promptText.length > 1) {
|
|
102
|
-
promptText = promptText.substring(0, promptText.length - 1);
|
|
103
|
-
charactersRemoved += 1;
|
|
104
|
-
} else {
|
|
105
|
-
throw new Error(`Prompt too long. Removed ${charactersRemoved} characters.`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
let headers = {
|
|
110
|
-
'User-Agent': `${packageData.name}/${packageData.version}`,
|
|
111
|
-
Authorization: `Bearer ${apiToken}`,
|
|
112
|
-
'Content-Type': 'application/json'
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
let payload = {
|
|
116
|
-
model: gptModel,
|
|
117
|
-
messages: [
|
|
118
|
-
{
|
|
119
|
-
role: 'system',
|
|
120
|
-
content: `${SYSTEM_PROMPT}`
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
role: 'user',
|
|
124
|
-
content: prompt
|
|
125
|
-
}
|
|
126
|
-
]
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
let res;
|
|
130
|
-
let data;
|
|
131
|
-
let retries = 0;
|
|
132
|
-
|
|
133
|
-
let run = async () => {
|
|
134
|
-
res = await fetchCmd(OPENAI_API_URL, {
|
|
135
|
-
method: 'post',
|
|
136
|
-
headers,
|
|
137
|
-
body: JSON.stringify(payload),
|
|
138
|
-
dispatcher: fetchAgent
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
data = await res.json();
|
|
142
|
-
|
|
143
|
-
if (!res.ok) {
|
|
144
|
-
if (res.status === 429 && ++retries < 5) {
|
|
145
|
-
// try again
|
|
146
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
147
|
-
return await run();
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (data && data.error) {
|
|
151
|
-
let error = new Error(data.error.message || data.error);
|
|
152
|
-
if (data.error.code) {
|
|
153
|
-
error.code = data.error.code;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
error.statusCode = res.status;
|
|
157
|
-
throw error;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
let error = new Error('Failed to run API request');
|
|
161
|
-
error.statusCode = res.status;
|
|
162
|
-
throw error;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (!data) {
|
|
166
|
-
throw new Error(`Failed to POST API request`);
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
const reqStartTime = Date.now();
|
|
171
|
-
await run();
|
|
172
|
-
const reqEndTime = Date.now();
|
|
173
|
-
|
|
174
|
-
let values;
|
|
175
|
-
const output =
|
|
176
|
-
data &&
|
|
177
|
-
data.choices &&
|
|
178
|
-
data.choices
|
|
179
|
-
.filter(msg => msg && msg.message && msg.message.role === 'assistant' && msg.message.content)
|
|
180
|
-
.sort((a, b) => ((a && a.index) || 0) - ((b && b.index) || 0))
|
|
181
|
-
.map(msg => msg.message.content)
|
|
182
|
-
.join('\n')
|
|
183
|
-
.trim()
|
|
184
|
-
.replace(/^\s*"|"\s*$/g, '')
|
|
185
|
-
.trim();
|
|
186
|
-
|
|
187
|
-
try {
|
|
188
|
-
values = JSON.parse(output);
|
|
189
|
-
if (typeof values.shouldReply === 'string') {
|
|
190
|
-
values.shouldReply = values.shouldReply === 'true';
|
|
191
|
-
}
|
|
192
|
-
if (typeof values.shouldReply !== 'undefined' && typeof values.shouldReply !== 'boolean') {
|
|
193
|
-
delete values.shouldReply;
|
|
194
|
-
}
|
|
195
|
-
} catch (err) {
|
|
196
|
-
let error = new Error('Failed to parse output from OpenAI API');
|
|
197
|
-
error.textContent = output;
|
|
198
|
-
throw error;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const response = Object.assign({ id: null, tokens: null, model: null }, values, {
|
|
202
|
-
id: data && data.id,
|
|
203
|
-
tokens: data && data.usage && data.usage.total_tokens,
|
|
204
|
-
model: gptModel,
|
|
205
|
-
|
|
206
|
-
_text: content.text,
|
|
207
|
-
_time: reqEndTime - reqStartTime,
|
|
208
|
-
_cr: charactersRemoved
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
return response;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
module.exports = generateSummary;
|
package/lib/risk-analysis.js
DELETED
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const packageData = require('../package.json');
|
|
4
|
-
const htmlToText = require('../lib/html-to-text');
|
|
5
|
-
const GPT3Tokenizer = require('gpt3-tokenizer').default;
|
|
6
|
-
|
|
7
|
-
const { fetch: fetchCmd, Agent } = require('undici');
|
|
8
|
-
const fetchAgent = new Agent({ connect: { timeout: 90 * 1000 } });
|
|
9
|
-
|
|
10
|
-
const tokenizer = new GPT3Tokenizer({ type: 'gpt3' });
|
|
11
|
-
|
|
12
|
-
const MAX_ALLOWED_TEXT_LENGTH = 32 * 1024;
|
|
13
|
-
const MAX_ALLOWED_TOKENS = 4000;
|
|
14
|
-
const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions';
|
|
15
|
-
|
|
16
|
-
const SYSTEM_PROMPT = `
|
|
17
|
-
I want you to act as are an IT security expert that monitors emails for suspicious and risky activity.
|
|
18
|
-
`
|
|
19
|
-
.replace(/\s+/g, ' ')
|
|
20
|
-
.trim();
|
|
21
|
-
|
|
22
|
-
const USER_PROMPT = `
|
|
23
|
-
Instructions:
|
|
24
|
-
- You are an IT security expert scanning incoming emails to detect and block fraud attempts.
|
|
25
|
-
- Your task is to monitor and analyze incoming emails, which consist of the message headers, a list of attachments, and text content
|
|
26
|
-
- Your analysis should contain (but is not limited to) the following risk factors:
|
|
27
|
-
- Does the email include links with domain names that contain typos or homoglyphs that might mislead the user about the actual target of the link
|
|
28
|
-
- Does the sender address of the email not seem to match the persona or organization the sender email claims to be
|
|
29
|
-
- Does the email promise the user an extremely good financial outcome
|
|
30
|
-
- Does the email promise unclaimed money or goods
|
|
31
|
-
- Does the email offer a vague business opportunity with no specific details
|
|
32
|
-
- Does the email suggest there is a severe penalty if the user does not act as requested
|
|
33
|
-
- Does the email claim that there are technical issues with the user's email account
|
|
34
|
-
- Does the email offer services or activities that are not suitable for underage persons
|
|
35
|
-
- Does the sender's email look like a throwaway address
|
|
36
|
-
- Does the sender's email look like it might be spoofed
|
|
37
|
-
- Does the email contain attachments where the name of the attachment might be misleading and suggest a different file format than is actually used
|
|
38
|
-
- Does the email contain attachments that might be executable files
|
|
39
|
-
- Provide a risk score for the email using the following scale: 1 - 5 (where 1 is low risk, and 5 is high risk), taking into account what may happen if a user acts by the instructions given in the email.
|
|
40
|
-
- Provide a short single-sentence assessment text that includes details about issues that increase the risk score, do not disclose details that decrease the risk score or do not affect it.
|
|
41
|
-
- Respond with a JSON formatted structure with a numeric risk score as "risk" property and the assessment as the "assessment" property. Do not write any other explanations.
|
|
42
|
-
|
|
43
|
-
Facts:
|
|
44
|
-
- An email consists of message headers, an attachments list, and text content
|
|
45
|
-
- An email might contain an "authentication-results" result header that indicates if the message passed SPF, DKIM, ARC, and DMARC checks
|
|
46
|
-
- Having a valid SPF, DKIM, and DMARC increases the chances that the sender domain name is not spoofed
|
|
47
|
-
- For valid SPF, the "authentication-results" header must include "spf=pass"
|
|
48
|
-
- For valid DKIM, the "authentication-results" header must include "dkim=pass"
|
|
49
|
-
- For valid DMARC, the "authentication-results" header must include "dmarc=pass"
|
|
50
|
-
- For valid ARC, the "authentication-results" header must include "arc=pass"
|
|
51
|
-
- Throwaway email addresses might contain a word or a name and more than one number in the username of the email address
|
|
52
|
-
- Throwaway email addresses might use a randomly generated or hex text string as the username of the email address
|
|
53
|
-
- Throwaway email addresses might use free email services like gmail.com, outlook.com, hotmail.com, yahoo.com, aol.com, etc
|
|
54
|
-
- The email to analyze is formatted in a JSON format
|
|
55
|
-
- The email structure includes a property "headers" that contains an array of header values.
|
|
56
|
-
- Each header contains two properties, "key" as the header field key name and "value" as the header value without the key prefix
|
|
57
|
-
- The email structure includes a property "attachments" that contains an array of attachments.
|
|
58
|
-
- Each attachment includes a "filename" property that describes the file name, "contentType" property that describes the Content-Type value of the attachment
|
|
59
|
-
- The email includes a "text" property for the text content
|
|
60
|
-
|
|
61
|
-
Analyze the following email:
|
|
62
|
-
`.trim();
|
|
63
|
-
|
|
64
|
-
async function riskAnalysis(message, apiToken, opts) {
|
|
65
|
-
opts = opts || {};
|
|
66
|
-
|
|
67
|
-
let maxAllowedTokens = opts.maxTokens || MAX_ALLOWED_TOKENS;
|
|
68
|
-
let gptModel = opts.gptModel || 'gpt-3.5-turbo';
|
|
69
|
-
|
|
70
|
-
let text = message.text || '';
|
|
71
|
-
if (message.html && (!text || message.html.length > text.length * 2)) {
|
|
72
|
-
text = htmlToText(message.html);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
let prompt;
|
|
76
|
-
|
|
77
|
-
const headerSeen = new Set();
|
|
78
|
-
const content = {
|
|
79
|
-
headers: []
|
|
80
|
-
.concat(message.headers || [])
|
|
81
|
-
.filter(
|
|
82
|
-
// use a whitelist
|
|
83
|
-
header =>
|
|
84
|
-
['from', 'to', 'cc', 'bcc', 'subject', 'mime-version', 'authentication-results', 'date', 'content-type', 'list-id'].includes(header.key)
|
|
85
|
-
)
|
|
86
|
-
.filter(header => {
|
|
87
|
-
// only keept the latest
|
|
88
|
-
if (['authentication-results'].includes(header.key)) {
|
|
89
|
-
if (headerSeen.has(header.key)) {
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
headerSeen.add(header.key);
|
|
93
|
-
}
|
|
94
|
-
return true;
|
|
95
|
-
}),
|
|
96
|
-
attachments: [].concat(message.attachments || []).map(attachment => ({ filename: attachment.filename, contentType: attachment.contentType }))
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
let charactersRemoved = 0;
|
|
100
|
-
let promptText = text;
|
|
101
|
-
|
|
102
|
-
if (promptText.length > MAX_ALLOWED_TEXT_LENGTH) {
|
|
103
|
-
charactersRemoved += promptText.length - MAX_ALLOWED_TEXT_LENGTH;
|
|
104
|
-
promptText = promptText.substr(0, MAX_ALLOWED_TEXT_LENGTH);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
while (promptText.length) {
|
|
108
|
-
content.text = promptText;
|
|
109
|
-
prompt = `${USER_PROMPT}
|
|
110
|
-
|
|
111
|
-
${JSON.stringify(content)}`;
|
|
112
|
-
|
|
113
|
-
let tokens = tokenizer.encode(prompt);
|
|
114
|
-
if (tokens.text.length <= maxAllowedTokens) {
|
|
115
|
-
break;
|
|
116
|
-
}
|
|
117
|
-
if (promptText.length > 2 * 1024 * 1024) {
|
|
118
|
-
promptText = promptText.substring(0, promptText.length - 1024 * 1024);
|
|
119
|
-
charactersRemoved += 1024 * 1024;
|
|
120
|
-
} else if (promptText.length > 2 * 1024) {
|
|
121
|
-
promptText = promptText.substring(0, promptText.length - 1024);
|
|
122
|
-
charactersRemoved += 1024;
|
|
123
|
-
} else if (promptText.length > 2 * 256) {
|
|
124
|
-
promptText = promptText.substring(0, promptText.length - 256);
|
|
125
|
-
charactersRemoved += 255;
|
|
126
|
-
} else if (promptText.length > 2 * 100) {
|
|
127
|
-
promptText = promptText.substring(0, promptText.length - 100);
|
|
128
|
-
charactersRemoved += 100;
|
|
129
|
-
} else if (promptText.length > 2 * 10) {
|
|
130
|
-
promptText = promptText.substring(0, promptText.length - 10);
|
|
131
|
-
charactersRemoved += 10;
|
|
132
|
-
} else if (promptText.length > 1) {
|
|
133
|
-
promptText = promptText.substring(0, promptText.length - 1);
|
|
134
|
-
charactersRemoved += 1;
|
|
135
|
-
} else {
|
|
136
|
-
throw new Error(`Prompt too long. Removed ${charactersRemoved} characters.`);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
let headers = {
|
|
141
|
-
'User-Agent': `${packageData.name}/${packageData.version}`,
|
|
142
|
-
Authorization: `Bearer ${apiToken}`,
|
|
143
|
-
'Content-Type': 'application/json'
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
let payload = {
|
|
147
|
-
model: gptModel,
|
|
148
|
-
messages: [
|
|
149
|
-
{
|
|
150
|
-
role: 'system',
|
|
151
|
-
content: `${SYSTEM_PROMPT}`
|
|
152
|
-
},
|
|
153
|
-
{
|
|
154
|
-
role: 'user',
|
|
155
|
-
content: prompt
|
|
156
|
-
}
|
|
157
|
-
]
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
let res;
|
|
161
|
-
let data;
|
|
162
|
-
let retries = 0;
|
|
163
|
-
|
|
164
|
-
let run = async () => {
|
|
165
|
-
res = await fetchCmd(OPENAI_API_URL, {
|
|
166
|
-
method: 'post',
|
|
167
|
-
headers,
|
|
168
|
-
body: JSON.stringify(payload),
|
|
169
|
-
dispatcher: fetchAgent
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
data = await res.json();
|
|
173
|
-
|
|
174
|
-
if (!res.ok) {
|
|
175
|
-
if (res.status === 429 && ++retries < 5) {
|
|
176
|
-
// try again
|
|
177
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
178
|
-
return await run();
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (data && data.error) {
|
|
182
|
-
let error = new Error(data.error.message || data.error);
|
|
183
|
-
if (data.error.code) {
|
|
184
|
-
error.code = data.error.code;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
error.statusCode = res.status;
|
|
188
|
-
throw error;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
let error = new Error('Failed to run API request');
|
|
192
|
-
error.statusCode = res.status;
|
|
193
|
-
throw error;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (!data) {
|
|
197
|
-
throw new Error(`Failed to POST API request`);
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
const reqStartTime = Date.now();
|
|
202
|
-
await run();
|
|
203
|
-
const reqEndTime = Date.now();
|
|
204
|
-
|
|
205
|
-
let values;
|
|
206
|
-
const output =
|
|
207
|
-
data &&
|
|
208
|
-
data.choices &&
|
|
209
|
-
data.choices
|
|
210
|
-
.filter(msg => msg && msg.message && msg.message.role === 'assistant' && msg.message.content)
|
|
211
|
-
.sort((a, b) => ((a && a.index) || 0) - ((b && b.index) || 0))
|
|
212
|
-
.map(msg => msg.message.content)
|
|
213
|
-
.join('\n')
|
|
214
|
-
.trim()
|
|
215
|
-
.replace(/^\s*"|"\s*$/g, '')
|
|
216
|
-
.trim();
|
|
217
|
-
|
|
218
|
-
try {
|
|
219
|
-
values = JSON.parse(output);
|
|
220
|
-
values.risk = Number(values.risk) || -1;
|
|
221
|
-
} catch (err) {
|
|
222
|
-
let error = new Error('Failed to parse output from OpenAI API');
|
|
223
|
-
error.textContent = output;
|
|
224
|
-
throw error;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const response = Object.assign({ id: null, tokens: null, model: null }, values, {
|
|
228
|
-
id: data && data.id,
|
|
229
|
-
tokens: data && data.usage && data.usage.total_tokens,
|
|
230
|
-
model: gptModel,
|
|
231
|
-
|
|
232
|
-
_text: content.text,
|
|
233
|
-
_time: reqEndTime - reqStartTime,
|
|
234
|
-
_cr: charactersRemoved
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
return response;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
module.exports = riskAnalysis;
|