@postalsys/email-text-tools 1.3.1 → 1.4.0

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
@@ -21,7 +21,12 @@ async function analyzeEmail(path) {
21
21
  html: parsed.html,
22
22
  text: parsed.text
23
23
  },
24
- process.env.OPENAI_API_KEY
24
+ process.env.OPENAI_API_KEY,
25
+ {
26
+ //gptModel: 'gpt-3.5-turbo'
27
+ gptModel: 'gpt-4',
28
+ maxTokens: 6000
29
+ }
25
30
  );
26
31
 
27
32
  result.file = Path.basename(path);
@@ -3,6 +3,7 @@
3
3
  const fs = require('fs').promises;
4
4
  const generateSummary = require('../lib/generate-summary');
5
5
  const simpleParser = require('mailparser').simpleParser;
6
+ const libmime = require('libmime');
6
7
 
7
8
  async function main() {
8
9
  const eml = await fs.readFile(process.argv[2]);
@@ -11,12 +12,17 @@ async function main() {
11
12
 
12
13
  const summary = await generateSummary(
13
14
  {
14
- from: parsed.from.value,
15
- subject: parsed.subject,
15
+ headers: parsed.headerLines.map(header => libmime.decodeHeader(header.line)),
16
+ attachments: parsed.attachments,
16
17
  html: parsed.html,
17
18
  text: parsed.text
18
19
  },
19
- process.env.OPENAI_API_KEY
20
+ process.env.OPENAI_API_KEY,
21
+ {
22
+ //gptModel: 'gpt-3.5-turbo'
23
+ gptModel: 'gpt-4',
24
+ maxTokens: 6000
25
+ }
20
26
  );
21
27
 
22
28
  console.log(summary);
@@ -3,9 +3,14 @@
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;
7
+
6
8
  const fetchCmd = global.fetch || nodeFetch;
7
9
 
8
- const MAX_TEXT_LENGTH = 4 * 1024;
10
+ const tokenizer = new GPT3Tokenizer({ type: 'gpt3' });
11
+
12
+ const MAX_ALLOWED_TEXT_LENGTH = 32 * 1024;
13
+ const MAX_ALLOWED_TOKENS = 4000;
9
14
  const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions';
10
15
 
11
16
  const SYSTEM_PROMPT = `
@@ -16,38 +21,89 @@ You do not comment or explain anything.
16
21
  .trim();
17
22
 
18
23
  const USER_PROMPT = `
19
- Describe the sentiment of the email using one word. Use either "positive", "neutral" or "negative". Prefix the sentiment with "Sentiment:".
20
- On a separate line, write a one-sentence summary of the email. Prefix the summary with "Summary:".
21
- I want you to only reply with the sentiment and summary, do not write explanations.
22
- All the text that follows from now on is the email message:
23
- `
24
- .replace(/\s+/g, ' ')
25
- .trim();
26
-
27
- async function generateSummary(message, apiToken) {
28
- let text = message.text;
29
- if (message.html && (!text || message.html.length > text.length * 10)) {
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)) {
30
55
  text = htmlToText(message.html);
31
56
  }
32
57
 
33
- text = text.trim();
34
- if (text.length > MAX_TEXT_LENGTH) {
35
- text = text.substring(0, MAX_TEXT_LENGTH);
36
- }
58
+ let prompt;
37
59
 
38
- let header = [];
39
- if (message.subject) {
40
- header.push(`Subject: ${message.subject}`);
41
- }
42
- if (message.from) {
43
- let from = [].concat(message.from || []).shift();
44
- header.push(`From: ${from.name || from.address}`);
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);
45
74
  }
46
75
 
47
- if (header.length) {
48
- text = `${header.join('\n')}
76
+ while (promptText.length) {
77
+ content.text = promptText;
78
+ prompt = `${USER_PROMPT}
49
79
 
50
- ${text}`;
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
+ }
51
107
  }
52
108
 
53
109
  let headers = {
@@ -57,7 +113,7 @@ ${text}`;
57
113
  };
58
114
 
59
115
  let payload = {
60
- model: 'gpt-3.5-turbo',
116
+ model: gptModel,
61
117
  messages: [
62
118
  {
63
119
  role: 'system',
@@ -65,19 +121,21 @@ ${text}`;
65
121
  },
66
122
  {
67
123
  role: 'user',
68
- content: `${USER_PROMPT}
69
-
70
- ${text}`
124
+ content: prompt
71
125
  }
72
126
  ]
73
127
  };
74
128
 
129
+ const reqStartTime = Date.now();
130
+
75
131
  let res = await fetchCmd(OPENAI_API_URL, {
76
132
  method: 'post',
77
133
  headers,
78
134
  body: JSON.stringify(payload)
79
135
  });
80
136
 
137
+ const reqEndTime = Date.now();
138
+
81
139
  let data = await res.json();
82
140
 
83
141
  if (!res.ok) {
@@ -100,7 +158,8 @@ ${text}`
100
158
  throw new Error(`Failed to POST API request`);
101
159
  }
102
160
 
103
- let output =
161
+ let values;
162
+ const output =
104
163
  data &&
105
164
  data.choices &&
106
165
  data.choices
@@ -112,25 +171,26 @@ ${text}`
112
171
  .replace(/^\s*"|"\s*$/g, '')
113
172
  .trim();
114
173
 
115
- let structured = {};
116
-
117
- let summaryMatch = output.match(/\bsummary:\s*(.*)$/im);
118
- if (summaryMatch) {
119
- structured.summary = summaryMatch[1].replace(/^["'\s]+|["'\s]+$/g, '');
174
+ try {
175
+ values = JSON.parse(output);
176
+ if (typeof values.shouldReply === 'string') {
177
+ values.shouldReply = values.shouldReply === 'true';
178
+ }
179
+ } catch (err) {
180
+ let error = new Error('Failed to parse output from OpenAI API');
181
+ error.textContent = output;
182
+ throw error;
120
183
  }
121
184
 
122
- let sentimentMatch = output.match(/\bsentiment:\s*(.*)$/im);
123
- if (sentimentMatch) {
124
- structured.sentiment = sentimentMatch[1].replace(/^["'\s]+|["'\s]+$/g, '').toLowerCase();
125
- }
185
+ const response = Object.assign({ id: null, tokens: null, model: null }, values, {
186
+ id: data && data.id,
187
+ tokens: data && data.usage && data.usage.total_tokens,
188
+ model: gptModel,
126
189
 
127
- const response = Object.assign(
128
- {
129
- id: data && data.id,
130
- tokens: data && data.usage && data.usage.total_tokens
131
- },
132
- structured
133
- );
190
+ _text: content.text,
191
+ _time: reqEndTime - reqStartTime,
192
+ _cr: charactersRemoved
193
+ });
134
194
 
135
195
  return response;
136
196
  }
@@ -4,6 +4,7 @@ const packageData = require('../package.json');
4
4
  const htmlToText = require('../lib/html-to-text');
5
5
  const nodeFetch = require('node-fetch');
6
6
  const GPT3Tokenizer = require('gpt3-tokenizer').default;
7
+
7
8
  const fetchCmd = global.fetch || nodeFetch;
8
9
 
9
10
  const tokenizer = new GPT3Tokenizer({ type: 'gpt3' });
@@ -20,39 +21,39 @@ I want you to act as are an IT security expert that monitors emails for suspicio
20
21
 
21
22
  const USER_PROMPT = `
22
23
  Instructions:
23
- - You are an IT security expert.
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:
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:
26
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
27
- - Does the sender address of the email match the persona or organization the sender email claims to be
28
- - Does the email promise the user extremely good financial outcome
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
29
30
  - Does the email promise unclaimed money or goods
30
31
  - Does the email offer a vague business opportunity with no specific details
31
32
  - Does the email suggest there is a severe penalty if the user does not act as requested
32
33
  - Does the email claim that there are technical issues with the user's email account
33
34
  - Does the email offer services or activities that are not suitable for underage persons
34
- - Does the sender email look like a throwaway address
35
- - Does the sender email look like it might be spoofed
35
+ - Does the sender's email look like a throwaway address
36
+ - Does the sender's email look like it might be spoofed
36
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
37
38
  - 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 rist, and 5 is high risk), take into account what may happen if a user acts by the instructions given in the email.
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.
39
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.
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.
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.
41
42
 
42
43
  Facts:
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"
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"
50
51
  - Throwaway email addresses might contain a word or a name and more than one number in the username of the email address
51
52
  - Throwaway email addresses might use a randomly generated or hex text string as the username of the email address
52
- - Throwaway email addresses might use free email services like gmail.com, outlook.com, hotmail.com, yahoo.com, aol.com etc
53
+ - Throwaway email addresses might use free email services like gmail.com, outlook.com, hotmail.com, yahoo.com, aol.com, etc
53
54
  - The email to analyze is formatted in a JSON format
54
55
  - The email structure includes a property "headers" that contains an array of header values.
55
- - Each header contains of two properties, "key" as the header field key name, and "value" as the header value without the key prefix
56
+ - Each header contains two properties, "key" as the header field key name and "value" as the header value without the key prefix
56
57
  - The email structure includes a property "attachments" that contains an array of attachments.
57
58
  - Each attachment includes a "filename" property that describes the file name, "contentType" property that describes the Content-Type value of the attachment
58
59
  - The email includes a "text" property for the text content
@@ -60,7 +61,12 @@ Facts:
60
61
  Analyze the following email:
61
62
  `.trim();
62
63
 
63
- async function riskAnalysis(message, apiToken) {
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
+
64
70
  let text = message.text || '';
65
71
  if (message.html && (!text || message.html.length > text.length * 2)) {
66
72
  text = htmlToText(message.html);
@@ -105,7 +111,7 @@ async function riskAnalysis(message, apiToken) {
105
111
  ${JSON.stringify(content)}`;
106
112
 
107
113
  let tokens = tokenizer.encode(prompt);
108
- if (tokens.text.length <= MAX_ALLOWED_TOKENS) {
114
+ if (tokens.text.length <= maxAllowedTokens) {
109
115
  break;
110
116
  }
111
117
  if (promptText.length > 2 * 1024 * 1024) {
@@ -138,7 +144,7 @@ ${JSON.stringify(content)}`;
138
144
  };
139
145
 
140
146
  let payload = {
141
- model: 'gpt-3.5-turbo',
147
+ model: gptModel,
142
148
  messages: [
143
149
  {
144
150
  role: 'system',
@@ -205,11 +211,12 @@ ${JSON.stringify(content)}`;
205
211
  throw error;
206
212
  }
207
213
 
208
- const response = Object.assign({ id: null, tokens: null }, values, {
214
+ const response = Object.assign({ id: null, tokens: null, model: null }, values, {
209
215
  id: data && data.id,
210
216
  tokens: data && data.usage && data.usage.total_tokens,
211
- text: content.text,
217
+ model: gptModel,
212
218
 
219
+ _text: content.text,
213
220
  _time: reqEndTime - reqStartTime,
214
221
  _cr: charactersRemoved
215
222
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postalsys/email-text-tools",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "description": "Email text tools",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -23,7 +23,7 @@
23
23
  "mailparser": "3.6.4"
24
24
  },
25
25
  "dependencies": {
26
- "dompurify": "3.0.1",
26
+ "dompurify": "3.0.2",
27
27
  "gpt3-tokenizer": "1.1.5",
28
28
  "handlebars": "4.7.7",
29
29
  "he": "1.2.0",