@postalsys/email-text-tools 1.2.6 → 1.3.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.
@@ -0,0 +1,26 @@
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
+
8
+ async function main() {
9
+ const eml = await fs.readFile(process.argv[2]);
10
+
11
+ const parsed = await simpleParser(eml);
12
+
13
+ const result = await riskAnalysis(
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
+
23
+ console.log(result);
24
+ }
25
+
26
+ main();
package/index.js CHANGED
@@ -6,5 +6,14 @@ const inlineHtml = require('./lib/inline-html');
6
6
  const inlineText = require('./lib/inline-text');
7
7
  const mimeHtml = require('./lib/mime-html');
8
8
  const generateSummary = require('./lib/generate-summary');
9
+ const riskAnalysis = require('./lib/risk-analysis');
9
10
 
10
- module.exports = { textToHtml, htmlToText, inlineHtml, inlineText, mimeHtml, generateSummary };
11
+ module.exports = {
12
+ textToHtml,
13
+ htmlToText,
14
+ inlineHtml,
15
+ inlineText,
16
+ mimeHtml,
17
+ generateSummary,
18
+ riskAnalysis
19
+ };
@@ -5,7 +5,7 @@ const htmlToText = require('../lib/html-to-text');
5
5
  const nodeFetch = require('node-fetch');
6
6
  const fetchCmd = global.fetch || nodeFetch;
7
7
 
8
- const MAX_TEXT_LENGTH = 6 * 1024;
8
+ const MAX_TEXT_LENGTH = 4 * 1024;
9
9
  const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions';
10
10
 
11
11
  const SYSTEM_PROMPT = `
@@ -0,0 +1,172 @@
1
+ 'use strict';
2
+
3
+ const packageData = require('../package.json');
4
+ const htmlToText = require('../lib/html-to-text');
5
+ const nodeFetch = require('node-fetch');
6
+ const fetchCmd = global.fetch || nodeFetch;
7
+
8
+ const MAX_TEXT_LENGTH = 4 * 1024;
9
+ const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions';
10
+
11
+ const SYSTEM_PROMPT = `
12
+ I want you to act as are an IT security expert that monitors emails for suspicious and risky activity.
13
+ `
14
+ .replace(/\s+/g, ' ')
15
+ .trim();
16
+
17
+ const USER_PROMPT = `
18
+ 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:
22
+ - 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
25
+ - Does the email promise unclaimed money or goods
26
+ - Does the email suggest there is a severe penalty if the user does not act as requested
27
+ - Does the email claim that there are technical issues with the user's email account
28
+ - Does the email offer services or activities that are not suitable for underage persons
29
+ - Does the sender email look like a throwaway address
30
+ - 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.
33
+
34
+ Facts:
35
+ - An email consists of message headers, attachments list, and text content
36
+ - Throwaway email addresses might contain a word or a name and more than one number in the username of the email address
37
+ - 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
39
+ - The email to analyze is formatted in a JSON format
40
+ - 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
42
+ - The email structure includes a property "attachments" that contains an array of attachments.
43
+ - Each attachment includes a "filename" property that describes the file name, "contentType" property that describes the Content-Type value of the attachment
44
+ - The email includes a "text" property for the text content
45
+
46
+ Analyze the following email:
47
+ `.trim();
48
+
49
+ async function riskAnalysis(message, apiToken) {
50
+ let text = message.text;
51
+ if (message.html && (!text || message.html.length > text.length * 2)) {
52
+ text = htmlToText(message.html);
53
+ }
54
+
55
+ text = text.trim();
56
+ if (text.length > MAX_TEXT_LENGTH) {
57
+ text = text.substring(0, MAX_TEXT_LENGTH);
58
+ }
59
+
60
+ const headerSeen = new Set();
61
+ const content = {
62
+ headers: []
63
+ .concat(message.headers || [])
64
+ .filter(
65
+ // remove unneeded
66
+ 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)
77
+ )
78
+ .filter(header => {
79
+ // only keept the latest
80
+ if (['authentication-results'].includes(header.key)) {
81
+ if (headerSeen.has(header.key)) {
82
+ return false;
83
+ }
84
+ headerSeen.add(header.key);
85
+ }
86
+ return true;
87
+ }),
88
+ attachments: [].concat(message.attachments || []).map(attachment => ({ filename: attachment.filename, contentType: attachment.contentType })),
89
+ text
90
+ };
91
+
92
+ let headers = {
93
+ 'User-Agent': `${packageData.name}/${packageData.version}`,
94
+ Authorization: `Bearer ${apiToken}`,
95
+ 'Content-Type': 'application/json'
96
+ };
97
+
98
+ let payload = {
99
+ model: 'gpt-3.5-turbo',
100
+ messages: [
101
+ {
102
+ role: 'system',
103
+ content: `${SYSTEM_PROMPT}`
104
+ },
105
+ {
106
+ role: 'user',
107
+ content: `${USER_PROMPT}
108
+
109
+ ${JSON.stringify(content)}`
110
+ }
111
+ ]
112
+ };
113
+
114
+ let res = await fetchCmd(OPENAI_API_URL, {
115
+ method: 'post',
116
+ headers,
117
+ body: JSON.stringify(payload)
118
+ });
119
+
120
+ let data = await res.json();
121
+
122
+ if (!res.ok) {
123
+ if (data && data.error) {
124
+ let error = new Error(data.error.message || data.error);
125
+ if (data.error.code) {
126
+ error.code = data.error.code;
127
+ }
128
+
129
+ error.statusCode = res.status;
130
+ throw error;
131
+ }
132
+
133
+ let error = new Error('Failed to run API request');
134
+ error.statusCode = res.status;
135
+ throw error;
136
+ }
137
+
138
+ if (!data) {
139
+ throw new Error(`Failed to POST API request`);
140
+ }
141
+
142
+ let values;
143
+ const output =
144
+ data &&
145
+ data.choices &&
146
+ data.choices
147
+ .filter(msg => msg && msg.message && msg.message.role === 'assistant' && msg.message.content)
148
+ .sort((a, b) => ((a && a.index) || 0) - ((b && b.index) || 0))
149
+ .map(msg => msg.message.content)
150
+ .join('\n')
151
+ .trim()
152
+ .replace(/^\s*"|"\s*$/g, '')
153
+ .trim();
154
+
155
+ try {
156
+ values = JSON.parse(output);
157
+ values.risk = Number(values.risk) || -1;
158
+ } catch (err) {
159
+ let error = new Error('Failed to parse output from OpenAI API');
160
+ error.textContent = output;
161
+ throw error;
162
+ }
163
+
164
+ const response = Object.assign({ id: null, tokens: null }, values, {
165
+ id: data && data.id,
166
+ tokens: data && data.usage && data.usage.total_tokens
167
+ });
168
+
169
+ return response;
170
+ }
171
+
172
+ module.exports = riskAnalysis;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postalsys/email-text-tools",
3
- "version": "1.2.6",
3
+ "version": "1.3.0",
4
4
  "description": "Email text tools",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -19,21 +19,21 @@
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.7.0",
22
+ "eslint-config-prettier": "8.8.0",
23
23
  "mailparser": "3.6.4"
24
24
  },
25
25
  "dependencies": {
26
26
  "dompurify": "3.0.1",
27
27
  "handlebars": "4.7.7",
28
28
  "he": "1.2.0",
29
- "html-to-text": "9.0.4",
29
+ "html-to-text": "9.0.5",
30
30
  "js-beautify": "1.14.7",
31
- "jsdom": "21.1.0",
32
- "juice": "8.1.0",
31
+ "jsdom": "21.1.1",
32
+ "juice": "9.0.0",
33
33
  "linkify-it": "4.0.1",
34
- "moment-timezone": "0.5.41",
34
+ "moment-timezone": "0.5.43",
35
35
  "node-fetch": "2.6.7",
36
36
  "node-html-parser": "6.1.5",
37
- "tlds": "1.237.0"
37
+ "tlds": "1.238.0"
38
38
  }
39
39
  }