@postalsys/email-text-tools 1.3.0 → 1.3.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/examples/risk.js +58 -4
- package/lib/risk-analysis.js +72 -24
- package/package.json +2 -1
package/examples/risk.js
CHANGED
|
@@ -4,9 +4,13 @@ const fs = require('fs').promises;
|
|
|
4
4
|
const { riskAnalysis } = require('..');
|
|
5
5
|
const simpleParser = require('mailparser').simpleParser;
|
|
6
6
|
const libmime = require('libmime');
|
|
7
|
+
const Path = require('path');
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
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);
|
|
10
14
|
|
|
11
15
|
const parsed = await simpleParser(eml);
|
|
12
16
|
|
|
@@ -20,7 +24,57 @@ async function main() {
|
|
|
20
24
|
process.env.OPENAI_API_KEY
|
|
21
25
|
);
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
result.file = Path.basename(path);
|
|
28
|
+
|
|
29
|
+
console.log(JSON.stringify(result));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function shuffleArray(array) {
|
|
33
|
+
for (let i = array.length - 1; i > 0; i--) {
|
|
34
|
+
let j = Math.floor(Math.random() * (i + 1));
|
|
35
|
+
let temp = array[i];
|
|
36
|
+
array[i] = array[j];
|
|
37
|
+
array[j] = temp;
|
|
38
|
+
}
|
|
39
|
+
return array;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function main() {
|
|
43
|
+
const stat = await fs.stat(BASE_PATH);
|
|
44
|
+
if (stat.isDirectory()) {
|
|
45
|
+
// process as directory
|
|
46
|
+
|
|
47
|
+
let processed = 0;
|
|
48
|
+
let list = await fs.readdir(BASE_PATH);
|
|
49
|
+
|
|
50
|
+
for (let file of shuffleArray(list)) {
|
|
51
|
+
if (Path.extname(file).toLowerCase() === '.eml') {
|
|
52
|
+
if (processed) {
|
|
53
|
+
process.stdout.write(',');
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
await analyzeEmail(Path.join(BASE_PATH, file));
|
|
57
|
+
console.error(`Processed ${file} [${processed + 1}]`);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.log(JSON.stringify({ file, error: err.message }));
|
|
60
|
+
console.error(`Failed processing ${file} [${processed + 1}]`);
|
|
61
|
+
console.error(err);
|
|
62
|
+
}
|
|
63
|
+
processed++;
|
|
64
|
+
|
|
65
|
+
if (MAX_ENTRIES && processed >= MAX_ENTRIES) {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
console.error(`Processed ${processed} files from ${BASE_PATH}`);
|
|
71
|
+
} else {
|
|
72
|
+
// process as file
|
|
73
|
+
await analyzeEmail(BASE_PATH);
|
|
74
|
+
}
|
|
24
75
|
}
|
|
25
76
|
|
|
26
|
-
|
|
77
|
+
console.log('[');
|
|
78
|
+
main().then(() => {
|
|
79
|
+
console.log(']');
|
|
80
|
+
});
|
package/lib/risk-analysis.js
CHANGED
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
const packageData = require('../package.json');
|
|
4
4
|
const htmlToText = require('../lib/html-to-text');
|
|
5
5
|
const nodeFetch = require('node-fetch');
|
|
6
|
+
const GPT3Tokenizer = require('gpt3-tokenizer').default;
|
|
6
7
|
const fetchCmd = global.fetch || nodeFetch;
|
|
7
8
|
|
|
8
|
-
const
|
|
9
|
+
const tokenizer = new GPT3Tokenizer({ type: 'gpt3' });
|
|
10
|
+
|
|
11
|
+
const MAX_ALLOWED_TEXT_LENGTH = 32 * 1024;
|
|
12
|
+
const MAX_ALLOWED_TOKENS = 4000;
|
|
9
13
|
const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions';
|
|
10
14
|
|
|
11
15
|
const SYSTEM_PROMPT = `
|
|
@@ -23,16 +27,26 @@ Instructions:
|
|
|
23
27
|
- Does the sender address of the email match the persona or organization the sender email claims to be
|
|
24
28
|
- Does the email promise the user extremely good financial outcome
|
|
25
29
|
- Does the email promise unclaimed money or goods
|
|
30
|
+
- Does the email offer a vague business opportunity with no specific details
|
|
26
31
|
- Does the email suggest there is a severe penalty if the user does not act as requested
|
|
27
32
|
- Does the email claim that there are technical issues with the user's email account
|
|
28
33
|
- Does the email offer services or activities that are not suitable for underage persons
|
|
29
34
|
- Does the sender email look like a throwaway address
|
|
35
|
+
- Does the sender email look like it might be spoofed
|
|
30
36
|
- Does the email contain attachments where the name of the attachment might be misleading and suggest a different file format than is actually used
|
|
37
|
+
- Does the email contain attachments that might be executable files
|
|
31
38
|
- Provide a risk score for the email using the following scale: 1 - 5 (where 1 is low rist, and 5 is high risk), take into account what may happen if a user acts by the instructions given in the email.
|
|
32
|
-
-
|
|
39
|
+
- 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.
|
|
40
|
+
- Respond with a JSON formatted structure with numeric risk score as "risk" property, and the assessment as the "assessment" property. Do not write any other explanations.
|
|
33
41
|
|
|
34
42
|
Facts:
|
|
35
43
|
- An email consists of message headers, attachments list, and text content
|
|
44
|
+
- An email might contain an "authentication-results" result header that indicate if the message passed SPF, DKIM, ARC and DMARC checks
|
|
45
|
+
- Having a valid SPF, DKIM and DMARC increases the chances that the sender domain name is not spoofed
|
|
46
|
+
- For valid SPF the "authentication-results" header must include "spf=pass"
|
|
47
|
+
- For valid DKIM the "authentication-results" header must include "dkim=pass"
|
|
48
|
+
- For valid DMARC the "authentication-results" header must include "dmarc=pass"
|
|
49
|
+
- For valid ARC the "authentication-results" header must include "arc=pass"
|
|
36
50
|
- Throwaway email addresses might contain a word or a name and more than one number in the username of the email address
|
|
37
51
|
- Throwaway email addresses might use a randomly generated or hex text string as the username of the email address
|
|
38
52
|
- Throwaway email addresses might use free email services like gmail.com, outlook.com, hotmail.com, yahoo.com, aol.com etc
|
|
@@ -47,33 +61,21 @@ Analyze the following email:
|
|
|
47
61
|
`.trim();
|
|
48
62
|
|
|
49
63
|
async function riskAnalysis(message, apiToken) {
|
|
50
|
-
let text = message.text;
|
|
64
|
+
let text = message.text || '';
|
|
51
65
|
if (message.html && (!text || message.html.length > text.length * 2)) {
|
|
52
66
|
text = htmlToText(message.html);
|
|
53
67
|
}
|
|
54
68
|
|
|
55
|
-
|
|
56
|
-
if (text.length > MAX_TEXT_LENGTH) {
|
|
57
|
-
text = text.substring(0, MAX_TEXT_LENGTH);
|
|
58
|
-
}
|
|
69
|
+
let prompt;
|
|
59
70
|
|
|
60
71
|
const headerSeen = new Set();
|
|
61
72
|
const content = {
|
|
62
73
|
headers: []
|
|
63
74
|
.concat(message.headers || [])
|
|
64
75
|
.filter(
|
|
65
|
-
//
|
|
76
|
+
// use a whitelist
|
|
66
77
|
header =>
|
|
67
|
-
|
|
68
|
-
'x-received',
|
|
69
|
-
'received',
|
|
70
|
-
'dkim-signature',
|
|
71
|
-
'domainkey-signature',
|
|
72
|
-
'arc-seal',
|
|
73
|
-
'arc-message-signature',
|
|
74
|
-
'arc-authentication-results',
|
|
75
|
-
'x-google-dkim-signature'
|
|
76
|
-
].includes(header.key)
|
|
78
|
+
['from', 'to', 'cc', 'bcc', 'subject', 'mime-version', 'authentication-results', 'date', 'content-type', 'list-id'].includes(header.key)
|
|
77
79
|
)
|
|
78
80
|
.filter(header => {
|
|
79
81
|
// only keept the latest
|
|
@@ -85,10 +87,50 @@ async function riskAnalysis(message, apiToken) {
|
|
|
85
87
|
}
|
|
86
88
|
return true;
|
|
87
89
|
}),
|
|
88
|
-
attachments: [].concat(message.attachments || []).map(attachment => ({ filename: attachment.filename, contentType: attachment.contentType }))
|
|
89
|
-
text
|
|
90
|
+
attachments: [].concat(message.attachments || []).map(attachment => ({ filename: attachment.filename, contentType: attachment.contentType }))
|
|
90
91
|
};
|
|
91
92
|
|
|
93
|
+
let charactersRemoved = 0;
|
|
94
|
+
let promptText = text;
|
|
95
|
+
|
|
96
|
+
if (promptText.length > MAX_ALLOWED_TEXT_LENGTH) {
|
|
97
|
+
charactersRemoved += promptText.length - MAX_ALLOWED_TEXT_LENGTH;
|
|
98
|
+
promptText = promptText.substr(0, MAX_ALLOWED_TEXT_LENGTH);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
while (promptText.length) {
|
|
102
|
+
content.text = promptText;
|
|
103
|
+
prompt = `${USER_PROMPT}
|
|
104
|
+
|
|
105
|
+
${JSON.stringify(content)}`;
|
|
106
|
+
|
|
107
|
+
let tokens = tokenizer.encode(prompt);
|
|
108
|
+
if (tokens.text.length <= MAX_ALLOWED_TOKENS) {
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
if (promptText.length > 2 * 1024 * 1024) {
|
|
112
|
+
promptText = promptText.substring(0, promptText.length - 1024 * 1024);
|
|
113
|
+
charactersRemoved += 1024 * 1024;
|
|
114
|
+
} else if (promptText.length > 2 * 1024) {
|
|
115
|
+
promptText = promptText.substring(0, promptText.length - 1024);
|
|
116
|
+
charactersRemoved += 1024;
|
|
117
|
+
} else if (promptText.length > 2 * 256) {
|
|
118
|
+
promptText = promptText.substring(0, promptText.length - 256);
|
|
119
|
+
charactersRemoved += 255;
|
|
120
|
+
} else if (promptText.length > 2 * 100) {
|
|
121
|
+
promptText = promptText.substring(0, promptText.length - 100);
|
|
122
|
+
charactersRemoved += 100;
|
|
123
|
+
} else if (promptText.length > 2 * 10) {
|
|
124
|
+
promptText = promptText.substring(0, promptText.length - 10);
|
|
125
|
+
charactersRemoved += 10;
|
|
126
|
+
} else if (promptText.length > 1) {
|
|
127
|
+
promptText = promptText.substring(0, promptText.length - 1);
|
|
128
|
+
charactersRemoved += 1;
|
|
129
|
+
} else {
|
|
130
|
+
throw new Error(`Prompt too long. Removed ${charactersRemoved} characters.`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
92
134
|
let headers = {
|
|
93
135
|
'User-Agent': `${packageData.name}/${packageData.version}`,
|
|
94
136
|
Authorization: `Bearer ${apiToken}`,
|
|
@@ -104,19 +146,21 @@ async function riskAnalysis(message, apiToken) {
|
|
|
104
146
|
},
|
|
105
147
|
{
|
|
106
148
|
role: 'user',
|
|
107
|
-
content:
|
|
108
|
-
|
|
109
|
-
${JSON.stringify(content)}`
|
|
149
|
+
content: prompt
|
|
110
150
|
}
|
|
111
151
|
]
|
|
112
152
|
};
|
|
113
153
|
|
|
154
|
+
const reqStartTime = Date.now();
|
|
155
|
+
|
|
114
156
|
let res = await fetchCmd(OPENAI_API_URL, {
|
|
115
157
|
method: 'post',
|
|
116
158
|
headers,
|
|
117
159
|
body: JSON.stringify(payload)
|
|
118
160
|
});
|
|
119
161
|
|
|
162
|
+
const reqEndTime = Date.now();
|
|
163
|
+
|
|
120
164
|
let data = await res.json();
|
|
121
165
|
|
|
122
166
|
if (!res.ok) {
|
|
@@ -163,7 +207,11 @@ ${JSON.stringify(content)}`
|
|
|
163
207
|
|
|
164
208
|
const response = Object.assign({ id: null, tokens: null }, values, {
|
|
165
209
|
id: data && data.id,
|
|
166
|
-
tokens: data && data.usage && data.usage.total_tokens
|
|
210
|
+
tokens: data && data.usage && data.usage.total_tokens,
|
|
211
|
+
text: content.text,
|
|
212
|
+
|
|
213
|
+
_time: reqEndTime - reqStartTime,
|
|
214
|
+
_cr: charactersRemoved
|
|
167
215
|
});
|
|
168
216
|
|
|
169
217
|
return response;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@postalsys/email-text-tools",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Email text tools",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"dompurify": "3.0.1",
|
|
27
|
+
"gpt3-tokenizer": "1.1.5",
|
|
27
28
|
"handlebars": "4.7.7",
|
|
28
29
|
"he": "1.2.0",
|
|
29
30
|
"html-to-text": "9.0.5",
|