@postalsys/email-text-tools 1.3.0 → 1.3.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/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
- async function main() {
9
- const eml = await fs.readFile(process.argv[2]);
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
- console.log(result);
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
- main();
77
+ console.log('[');
78
+ main().then(() => {
79
+ console.log(']');
80
+ });
@@ -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 MAX_TEXT_LENGTH = 4 * 1024;
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 = `
@@ -16,29 +20,39 @@ I want you to act as are an IT security expert that monitors emails for suspicio
16
20
 
17
21
  const USER_PROMPT = `
18
22
  Instructions:
19
- - You are an IT security expert.
20
- - Your task is to monitor and analyze incoming emails which consist of the message headers, a list of attachments, and text content
21
- - Your analysis should contain (but is not limited to) the following:
23
+ - You are an IT security expert scanning incoming emails to detect and block fraud attempts.
24
+ - Your task is to monitor and analyze incoming emails, which consist of the message headers, a list of attachments, and text content
25
+ - Your analysis should contain (but is not limited to) the following risk factors:
22
26
  - 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
23
- - Does the sender address of the email match the persona or organization the sender email claims to be
24
- - Does the email promise the user extremely good financial outcome
27
+ - Does the sender address of the email not seem to match the persona or organization the sender email claims to be
28
+ - Does the email promise the user an 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
- - Does the sender email look like a throwaway address
34
+ - Does the sender's email look like a throwaway address
35
+ - Does the sender's 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
31
- - 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
- - Respond with a JSON formatted structure with numeric risk score as "risk" property, and a single-paragraph assessment as the "assessment" property. Do not write any other explanations.
37
+ - Does the email contain attachments that might be executable files
38
+ - 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.
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 a 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
- - An email consists of message headers, attachments list, and text content
43
+ - An email consists of message headers, an attachments list, and text content
44
+ - An email might contain an "authentication-results" result header that indicates 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
- - Throwaway email addresses might use free email services like gmail.com, outlook.com, hotmail.com, yahoo.com, aol.com etc
52
+ - Throwaway email addresses might use free email services like gmail.com, outlook.com, hotmail.com, yahoo.com, aol.com, etc
39
53
  - The email to analyze is formatted in a JSON format
40
54
  - The email structure includes a property "headers" that contains an array of header values.
41
- - Each header contains of two properties, "key" as the header field key name, and "value" as the header value without the key prefix
55
+ - Each header contains two properties, "key" as the header field key name and "value" as the header value without the key prefix
42
56
  - The email structure includes a property "attachments" that contains an array of attachments.
43
57
  - Each attachment includes a "filename" property that describes the file name, "contentType" property that describes the Content-Type value of the attachment
44
58
  - The email includes a "text" property for the text content
@@ -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
- text = text.trim();
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
- // remove unneeded
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: `${USER_PROMPT}
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
+
212
+ _text: content.text,
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.0",
3
+ "version": "1.3.2",
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",