catalist-support-agent 1.0.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/dist/admin-portal.d.ts +43 -0
- package/dist/admin-portal.d.ts.map +1 -0
- package/dist/admin-portal.js +166 -0
- package/dist/admin-portal.js.map +1 -0
- package/dist/analysis/entities.d.ts +73 -0
- package/dist/analysis/entities.d.ts.map +1 -0
- package/dist/analysis/entities.js +378 -0
- package/dist/analysis/entities.js.map +1 -0
- package/dist/analysis/index.d.ts +44 -0
- package/dist/analysis/index.d.ts.map +1 -0
- package/dist/analysis/index.js +243 -0
- package/dist/analysis/index.js.map +1 -0
- package/dist/analysis/intent.d.ts +49 -0
- package/dist/analysis/intent.d.ts.map +1 -0
- package/dist/analysis/intent.js +320 -0
- package/dist/analysis/intent.js.map +1 -0
- package/dist/analysis/sentiment.d.ts +57 -0
- package/dist/analysis/sentiment.d.ts.map +1 -0
- package/dist/analysis/sentiment.js +351 -0
- package/dist/analysis/sentiment.js.map +1 -0
- package/dist/brand/compliance.d.ts +122 -0
- package/dist/brand/compliance.d.ts.map +1 -0
- package/dist/brand/compliance.js +378 -0
- package/dist/brand/compliance.js.map +1 -0
- package/dist/brand/forbidden-terms.d.ts +99 -0
- package/dist/brand/forbidden-terms.d.ts.map +1 -0
- package/dist/brand/forbidden-terms.js +265 -0
- package/dist/brand/forbidden-terms.js.map +1 -0
- package/dist/brand/index.d.ts +10 -0
- package/dist/brand/index.d.ts.map +1 -0
- package/dist/brand/index.js +12 -0
- package/dist/brand/index.js.map +1 -0
- package/dist/config.d.ts +325 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +492 -0
- package/dist/config.js.map +1 -0
- package/dist/delivery/index.d.ts +84 -0
- package/dist/delivery/index.d.ts.map +1 -0
- package/dist/delivery/index.js +435 -0
- package/dist/delivery/index.js.map +1 -0
- package/dist/embeddings/cache.d.ts +96 -0
- package/dist/embeddings/cache.d.ts.map +1 -0
- package/dist/embeddings/cache.js +193 -0
- package/dist/embeddings/cache.js.map +1 -0
- package/dist/embeddings/index.d.ts +152 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/index.js +337 -0
- package/dist/embeddings/index.js.map +1 -0
- package/dist/embeddings/openai-client.d.ts +67 -0
- package/dist/embeddings/openai-client.d.ts.map +1 -0
- package/dist/embeddings/openai-client.js +190 -0
- package/dist/embeddings/openai-client.js.map +1 -0
- package/dist/errors.d.ts +302 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +508 -0
- package/dist/errors.js.map +1 -0
- package/dist/escalation/index.d.ts +93 -0
- package/dist/escalation/index.d.ts.map +1 -0
- package/dist/escalation/index.js +436 -0
- package/dist/escalation/index.js.map +1 -0
- package/dist/extraction/deduplication.d.ts +97 -0
- package/dist/extraction/deduplication.d.ts.map +1 -0
- package/dist/extraction/deduplication.js +271 -0
- package/dist/extraction/deduplication.js.map +1 -0
- package/dist/extraction/gmail-extractor.d.ts +160 -0
- package/dist/extraction/gmail-extractor.d.ts.map +1 -0
- package/dist/extraction/gmail-extractor.js +396 -0
- package/dist/extraction/gmail-extractor.js.map +1 -0
- package/dist/extraction/gmail-token-manager.d.ts +36 -0
- package/dist/extraction/gmail-token-manager.d.ts.map +1 -0
- package/dist/extraction/gmail-token-manager.js +146 -0
- package/dist/extraction/gmail-token-manager.js.map +1 -0
- package/dist/extraction/index.d.ts +13 -0
- package/dist/extraction/index.d.ts.map +1 -0
- package/dist/extraction/index.js +20 -0
- package/dist/extraction/index.js.map +1 -0
- package/dist/extraction/pii-handler.d.ts +100 -0
- package/dist/extraction/pii-handler.d.ts.map +1 -0
- package/dist/extraction/pii-handler.js +295 -0
- package/dist/extraction/pii-handler.js.map +1 -0
- package/dist/extraction/pipeline.d.ts +94 -0
- package/dist/extraction/pipeline.d.ts.map +1 -0
- package/dist/extraction/pipeline.js +380 -0
- package/dist/extraction/pipeline.js.map +1 -0
- package/dist/extraction/quality-filter.d.ts +99 -0
- package/dist/extraction/quality-filter.d.ts.map +1 -0
- package/dist/extraction/quality-filter.js +370 -0
- package/dist/extraction/quality-filter.js.map +1 -0
- package/dist/extraction/rate-limiter.d.ts +90 -0
- package/dist/extraction/rate-limiter.d.ts.map +1 -0
- package/dist/extraction/rate-limiter.js +242 -0
- package/dist/extraction/rate-limiter.js.map +1 -0
- package/dist/extraction/state-manager.d.ts +126 -0
- package/dist/extraction/state-manager.d.ts.map +1 -0
- package/dist/extraction/state-manager.js +344 -0
- package/dist/extraction/state-manager.js.map +1 -0
- package/dist/generation/index.d.ts +75 -0
- package/dist/generation/index.d.ts.map +1 -0
- package/dist/generation/index.js +641 -0
- package/dist/generation/index.js.map +1 -0
- package/dist/index.d.ts +96 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +233 -0
- package/dist/index.js.map +1 -0
- package/dist/intake/index.d.ts +15 -0
- package/dist/intake/index.d.ts.map +1 -0
- package/dist/intake/index.js +19 -0
- package/dist/intake/index.js.map +1 -0
- package/dist/intake/normalizer.d.ts +163 -0
- package/dist/intake/normalizer.d.ts.map +1 -0
- package/dist/intake/normalizer.js +309 -0
- package/dist/intake/normalizer.js.map +1 -0
- package/dist/intake/postmark.d.ts +72 -0
- package/dist/intake/postmark.d.ts.map +1 -0
- package/dist/intake/postmark.js +276 -0
- package/dist/intake/postmark.js.map +1 -0
- package/dist/intake/slack.d.ts +106 -0
- package/dist/intake/slack.d.ts.map +1 -0
- package/dist/intake/slack.js +378 -0
- package/dist/intake/slack.js.map +1 -0
- package/dist/intake/twilio.d.ts +86 -0
- package/dist/intake/twilio.d.ts.map +1 -0
- package/dist/intake/twilio.js +283 -0
- package/dist/intake/twilio.js.map +1 -0
- package/dist/knowledge/index.d.ts +100 -0
- package/dist/knowledge/index.d.ts.map +1 -0
- package/dist/knowledge/index.js +516 -0
- package/dist/knowledge/index.js.map +1 -0
- package/dist/knowledge/invoice-resolver.d.ts +62 -0
- package/dist/knowledge/invoice-resolver.d.ts.map +1 -0
- package/dist/knowledge/invoice-resolver.js +267 -0
- package/dist/knowledge/invoice-resolver.js.map +1 -0
- package/dist/types.d.ts +535 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +48 -0
- package/dist/types.js.map +1 -0
- package/ga-service-account.json +13 -0
- package/gmail-knowledge-migration.sql +149 -0
- package/nul +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message Normalizer
|
|
3
|
+
*
|
|
4
|
+
* Transforms channel-specific message formats into a unified InboundMessage interface.
|
|
5
|
+
* Handles content extraction, sender identification, and metadata normalization.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash } from 'crypto';
|
|
8
|
+
import { createMessageId } from '../types.js';
|
|
9
|
+
import { IntakeError } from '../errors.js';
|
|
10
|
+
import { scanMessageContent } from '../brand/index.js';
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// Normalizer Class
|
|
13
|
+
// =============================================================================
|
|
14
|
+
export class MessageNormalizer {
|
|
15
|
+
/**
|
|
16
|
+
* Normalize a Postmark inbound email
|
|
17
|
+
*/
|
|
18
|
+
normalizePostmarkEmail(email) {
|
|
19
|
+
const messageId = createMessageId(email.MessageID);
|
|
20
|
+
// Extract sender info
|
|
21
|
+
const sender = {
|
|
22
|
+
email: email.FromFull?.Email || email.From,
|
|
23
|
+
name: email.FromFull?.Name || email.FromName,
|
|
24
|
+
};
|
|
25
|
+
// Build email metadata
|
|
26
|
+
const metadata = {
|
|
27
|
+
type: 'email',
|
|
28
|
+
messageId: email.MessageID,
|
|
29
|
+
inReplyTo: email.InReplyTo,
|
|
30
|
+
references: email.References?.split(/\s+/).filter(Boolean),
|
|
31
|
+
headers: this.headersToRecord(email.Headers),
|
|
32
|
+
fromAddress: sender.email || '',
|
|
33
|
+
toAddresses: email.ToFull?.map((t) => t.Email) || [email.To],
|
|
34
|
+
ccAddresses: email.CcFull?.map((c) => c.Email),
|
|
35
|
+
};
|
|
36
|
+
// Extract content (prefer stripped reply, fall back to full body)
|
|
37
|
+
const text = email.StrippedTextReply || email.TextBody || '';
|
|
38
|
+
const html = email.HtmlBody;
|
|
39
|
+
// Normalize attachments
|
|
40
|
+
const attachments = this.normalizePostmarkAttachments(email.Attachments);
|
|
41
|
+
return {
|
|
42
|
+
id: messageId,
|
|
43
|
+
channel: 'email',
|
|
44
|
+
direction: 'inbound',
|
|
45
|
+
content: {
|
|
46
|
+
text,
|
|
47
|
+
html,
|
|
48
|
+
subject: email.Subject,
|
|
49
|
+
},
|
|
50
|
+
sender,
|
|
51
|
+
receivedAt: new Date(email.Date).toISOString(),
|
|
52
|
+
metadata,
|
|
53
|
+
attachments,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Normalize a Twilio inbound SMS
|
|
58
|
+
*/
|
|
59
|
+
normalizeTwilioSms(sms) {
|
|
60
|
+
const messageId = createMessageId(sms.MessageSid);
|
|
61
|
+
// Extract sender info
|
|
62
|
+
const sender = {
|
|
63
|
+
phone: sms.From,
|
|
64
|
+
};
|
|
65
|
+
// Build SMS metadata
|
|
66
|
+
const metadata = {
|
|
67
|
+
type: 'sms',
|
|
68
|
+
messageSid: sms.MessageSid,
|
|
69
|
+
fromNumber: sms.From,
|
|
70
|
+
toNumber: sms.To,
|
|
71
|
+
numSegments: sms.NumSegments ? parseInt(sms.NumSegments, 10) : undefined,
|
|
72
|
+
};
|
|
73
|
+
// Handle MMS attachments
|
|
74
|
+
const attachments = this.normalizeTwilioMedia(sms);
|
|
75
|
+
return {
|
|
76
|
+
id: messageId,
|
|
77
|
+
channel: 'sms',
|
|
78
|
+
direction: 'inbound',
|
|
79
|
+
content: {
|
|
80
|
+
text: sms.Body || '',
|
|
81
|
+
},
|
|
82
|
+
sender,
|
|
83
|
+
receivedAt: new Date().toISOString(),
|
|
84
|
+
metadata,
|
|
85
|
+
attachments,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Normalize a Slack message event
|
|
90
|
+
*/
|
|
91
|
+
normalizeSlackMessage(event, teamId) {
|
|
92
|
+
const messageId = createMessageId(`${event.channel}-${event.ts}`);
|
|
93
|
+
// Extract sender info
|
|
94
|
+
const sender = {
|
|
95
|
+
slackUserId: event.user,
|
|
96
|
+
};
|
|
97
|
+
// Build Slack metadata
|
|
98
|
+
const metadata = {
|
|
99
|
+
type: 'slack',
|
|
100
|
+
teamId,
|
|
101
|
+
channelId: event.channel,
|
|
102
|
+
userId: event.user,
|
|
103
|
+
threadTs: event.thread_ts,
|
|
104
|
+
eventTs: event.event_ts,
|
|
105
|
+
};
|
|
106
|
+
// Handle Slack file attachments
|
|
107
|
+
const attachments = this.normalizeSlackFiles(event.files);
|
|
108
|
+
return {
|
|
109
|
+
id: messageId,
|
|
110
|
+
channel: 'slack',
|
|
111
|
+
direction: 'inbound',
|
|
112
|
+
content: {
|
|
113
|
+
text: event.text || '',
|
|
114
|
+
},
|
|
115
|
+
sender,
|
|
116
|
+
receivedAt: new Date(parseFloat(event.ts) * 1000).toISOString(),
|
|
117
|
+
metadata,
|
|
118
|
+
attachments,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Convert Postmark headers array to record
|
|
123
|
+
*/
|
|
124
|
+
headersToRecord(headers) {
|
|
125
|
+
if (!headers || headers.length === 0) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
const record = {};
|
|
129
|
+
for (const header of headers) {
|
|
130
|
+
record[header.Name] = header.Value;
|
|
131
|
+
}
|
|
132
|
+
return record;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Normalize Postmark attachments
|
|
136
|
+
*/
|
|
137
|
+
normalizePostmarkAttachments(attachments) {
|
|
138
|
+
if (!attachments || attachments.length === 0) {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
return attachments.map((att, index) => ({
|
|
142
|
+
id: att.ContentID || `attachment-${index}`,
|
|
143
|
+
filename: att.Name,
|
|
144
|
+
contentType: att.ContentType,
|
|
145
|
+
size: att.ContentLength,
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Normalize Twilio MMS media
|
|
150
|
+
*/
|
|
151
|
+
normalizeTwilioMedia(sms) {
|
|
152
|
+
const numMedia = parseInt(sms.NumMedia || '0', 10);
|
|
153
|
+
if (numMedia === 0) {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
const attachments = [];
|
|
157
|
+
for (let i = 0; i < numMedia; i++) {
|
|
158
|
+
const urlKey = `MediaUrl${i}`;
|
|
159
|
+
const typeKey = `MediaContentType${i}`;
|
|
160
|
+
const url = sms[urlKey];
|
|
161
|
+
const contentType = sms[typeKey];
|
|
162
|
+
if (url) {
|
|
163
|
+
attachments.push({
|
|
164
|
+
id: `media-${i}`,
|
|
165
|
+
filename: `media-${i}`,
|
|
166
|
+
contentType: contentType || 'application/octet-stream',
|
|
167
|
+
size: 0, // Size not provided by Twilio
|
|
168
|
+
url,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return attachments.length > 0 ? attachments : undefined;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Normalize Slack file attachments
|
|
176
|
+
*/
|
|
177
|
+
normalizeSlackFiles(files) {
|
|
178
|
+
if (!files || files.length === 0) {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
return files.map((file) => ({
|
|
182
|
+
id: file.id,
|
|
183
|
+
filename: file.name,
|
|
184
|
+
contentType: file.mimetype,
|
|
185
|
+
size: file.size,
|
|
186
|
+
url: file.url_private_download || file.url_private,
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// =============================================================================
|
|
191
|
+
// Utility Functions
|
|
192
|
+
// =============================================================================
|
|
193
|
+
/**
|
|
194
|
+
* Generate a content hash for deduplication
|
|
195
|
+
*/
|
|
196
|
+
export function generateContentHash(message) {
|
|
197
|
+
const content = [
|
|
198
|
+
message.channel,
|
|
199
|
+
message.sender.email || message.sender.phone || message.sender.slackUserId,
|
|
200
|
+
message.content.text,
|
|
201
|
+
message.content.subject,
|
|
202
|
+
]
|
|
203
|
+
.filter(Boolean)
|
|
204
|
+
.join('|');
|
|
205
|
+
return createHash('sha256').update(content).digest('hex').substring(0, 32);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Extract conversation thread identifier from message
|
|
209
|
+
*/
|
|
210
|
+
export function extractThreadIdentifier(message) {
|
|
211
|
+
if (message.metadata.type === 'email') {
|
|
212
|
+
// Use References header for email threading
|
|
213
|
+
const references = message.metadata.references;
|
|
214
|
+
if (references && references.length > 0) {
|
|
215
|
+
return references[0]; // First reference is usually the original message
|
|
216
|
+
}
|
|
217
|
+
return message.metadata.inReplyTo;
|
|
218
|
+
}
|
|
219
|
+
if (message.metadata.type === 'slack') {
|
|
220
|
+
// Use thread_ts for Slack threading
|
|
221
|
+
return message.metadata.threadTs;
|
|
222
|
+
}
|
|
223
|
+
// SMS doesn't have built-in threading - use sender identifier
|
|
224
|
+
if (message.metadata.type === 'sms') {
|
|
225
|
+
return message.metadata.fromNumber;
|
|
226
|
+
}
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Get sender identifier for customer matching
|
|
231
|
+
*/
|
|
232
|
+
export function getSenderIdentifier(message) {
|
|
233
|
+
if (message.sender.email) {
|
|
234
|
+
return { type: 'email', value: message.sender.email.toLowerCase() };
|
|
235
|
+
}
|
|
236
|
+
if (message.sender.phone) {
|
|
237
|
+
return { type: 'phone', value: normalizePhoneNumber(message.sender.phone) };
|
|
238
|
+
}
|
|
239
|
+
if (message.sender.slackUserId) {
|
|
240
|
+
return { type: 'slack', value: message.sender.slackUserId };
|
|
241
|
+
}
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Normalize phone number to E.164 format
|
|
246
|
+
*/
|
|
247
|
+
export function normalizePhoneNumber(phone) {
|
|
248
|
+
// Remove all non-digit characters except leading +
|
|
249
|
+
const cleaned = phone.replace(/[^\d+]/g, '');
|
|
250
|
+
// If it starts with +, assume it's already E.164
|
|
251
|
+
if (cleaned.startsWith('+')) {
|
|
252
|
+
return cleaned;
|
|
253
|
+
}
|
|
254
|
+
// If it's 10 digits, assume US number
|
|
255
|
+
if (cleaned.length === 10) {
|
|
256
|
+
return `+1${cleaned}`;
|
|
257
|
+
}
|
|
258
|
+
// If it's 11 digits starting with 1, add +
|
|
259
|
+
if (cleaned.length === 11 && cleaned.startsWith('1')) {
|
|
260
|
+
return `+${cleaned}`;
|
|
261
|
+
}
|
|
262
|
+
// Return with + prefix
|
|
263
|
+
return cleaned.startsWith('+') ? cleaned : `+${cleaned}`;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Validate message content is not empty
|
|
267
|
+
*/
|
|
268
|
+
export function validateMessageContent(message) {
|
|
269
|
+
const hasContent = message.content.text?.trim() || message.content.html?.trim() || message.content.subject?.trim();
|
|
270
|
+
if (!hasContent) {
|
|
271
|
+
throw new IntakeError('Message has no content', message.channel, 'normalization', {
|
|
272
|
+
messageId: message.id,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Check message for forbidden terms and annotate
|
|
278
|
+
*/
|
|
279
|
+
export function scanAndAnnotateMessage(message) {
|
|
280
|
+
const scanResult = scanMessageContent(message.content);
|
|
281
|
+
return {
|
|
282
|
+
message,
|
|
283
|
+
hasForbiddenTerms: scanResult.hasForbiddenTerms,
|
|
284
|
+
detectedTerms: scanResult.detectedTerms,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
// =============================================================================
|
|
288
|
+
// Singleton
|
|
289
|
+
// =============================================================================
|
|
290
|
+
let normalizer = null;
|
|
291
|
+
export function getMessageNormalizer() {
|
|
292
|
+
if (!normalizer) {
|
|
293
|
+
normalizer = new MessageNormalizer();
|
|
294
|
+
}
|
|
295
|
+
return normalizer;
|
|
296
|
+
}
|
|
297
|
+
// =============================================================================
|
|
298
|
+
// High-Level Normalization Functions
|
|
299
|
+
// =============================================================================
|
|
300
|
+
export function normalizePostmarkEmail(email) {
|
|
301
|
+
return getMessageNormalizer().normalizePostmarkEmail(email);
|
|
302
|
+
}
|
|
303
|
+
export function normalizeTwilioSms(sms) {
|
|
304
|
+
return getMessageNormalizer().normalizeTwilioSms(sms);
|
|
305
|
+
}
|
|
306
|
+
export function normalizeSlackMessage(event, teamId) {
|
|
307
|
+
return getMessageNormalizer().normalizeSlackMessage(event, teamId);
|
|
308
|
+
}
|
|
309
|
+
//# sourceMappingURL=normalizer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalizer.js","sourceRoot":"","sources":["../../src/intake/normalizer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Postmark Inbound Email Handler
|
|
3
|
+
*
|
|
4
|
+
* Receives incoming emails via Postmark webhook, validates signatures,
|
|
5
|
+
* and normalizes email content into the standard message format.
|
|
6
|
+
*/
|
|
7
|
+
import type { InboundMessage } from '../types.js';
|
|
8
|
+
import { type PostmarkInboundEmail } from './normalizer.js';
|
|
9
|
+
export interface PostmarkWebhookPayload extends PostmarkInboundEmail {
|
|
10
|
+
OriginalRecipient?: string;
|
|
11
|
+
Tag?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface PostmarkProcessResult {
|
|
14
|
+
success: boolean;
|
|
15
|
+
message?: InboundMessage;
|
|
16
|
+
contentHash?: string;
|
|
17
|
+
hasForbiddenTerms: boolean;
|
|
18
|
+
detectedTerms: string[];
|
|
19
|
+
error?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Validate Postmark webhook signature
|
|
23
|
+
*
|
|
24
|
+
* Note: Postmark uses a webhook token that's included in the URL
|
|
25
|
+
* rather than signature-based validation. However, we can validate
|
|
26
|
+
* the token if configured.
|
|
27
|
+
*/
|
|
28
|
+
export declare function validatePostmarkWebhook(payload: unknown, webhookToken?: string): payload is PostmarkWebhookPayload;
|
|
29
|
+
/**
|
|
30
|
+
* Generate HMAC signature for webhook validation (if using custom signing)
|
|
31
|
+
*/
|
|
32
|
+
export declare function generatePostmarkSignature(body: string, secret: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Process an inbound Postmark email
|
|
35
|
+
*/
|
|
36
|
+
export declare function processPostmarkEmail(payload: PostmarkWebhookPayload): Promise<PostmarkProcessResult>;
|
|
37
|
+
/**
|
|
38
|
+
* Full webhook handler for Postmark inbound emails
|
|
39
|
+
*/
|
|
40
|
+
export declare function handlePostmarkInbound(rawBody: string, webhookToken?: string): Promise<PostmarkProcessResult>;
|
|
41
|
+
/**
|
|
42
|
+
* Check if email is likely an auto-reply or out-of-office
|
|
43
|
+
*/
|
|
44
|
+
export declare function isAutoReply(email: PostmarkInboundEmail): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Check if email is likely spam or marketing
|
|
47
|
+
*/
|
|
48
|
+
export declare function isLikelySpam(email: PostmarkInboundEmail): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Classify email for processing
|
|
51
|
+
*/
|
|
52
|
+
export declare function classifyEmail(email: PostmarkInboundEmail): {
|
|
53
|
+
shouldProcess: boolean;
|
|
54
|
+
reason?: string;
|
|
55
|
+
classification: 'support' | 'auto_reply' | 'spam_marketing' | 'unknown';
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Extract thread information from email headers
|
|
59
|
+
*/
|
|
60
|
+
export declare function extractEmailThread(email: PostmarkInboundEmail): {
|
|
61
|
+
isReply: boolean;
|
|
62
|
+
originalMessageId?: string;
|
|
63
|
+
threadMessageIds: string[];
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Extract quoted content from email body
|
|
67
|
+
*/
|
|
68
|
+
export declare function extractQuotedContent(body: string): {
|
|
69
|
+
newContent: string;
|
|
70
|
+
quotedContent?: string;
|
|
71
|
+
};
|
|
72
|
+
//# sourceMappingURL=postmark.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postmark.d.ts","sourceRoot":"","sources":["../../src/intake/postmark.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAKL,KAAK,oBAAoB,EAC1B,MAAM,iBAAiB,CAAC;AAMzB,MAAM,WAAW,sBAAuB,SAAQ,oBAAoB;IAElE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAMD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,OAAO,EAChB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,IAAI,sBAAsB,CAmBnC;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAE9E;AAMD;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,qBAAqB,CAAC,CAsChC;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,MAAM,EACf,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,qBAAqB,CAAC,CAuBhC;AAMD;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAkDhE;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CA4CjE;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,oBAAoB,GAAG;IAC1D,aAAa,EAAE,OAAO,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,SAAS,GAAG,YAAY,GAAG,gBAAgB,GAAG,SAAS,CAAC;CACzE,CA+BA;AAMD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,oBAAoB,GAAG;IAC/D,OAAO,EAAE,OAAO,CAAC;IACjB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B,CAYA;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG;IAClD,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAuBA"}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Postmark Inbound Email Handler
|
|
3
|
+
*
|
|
4
|
+
* Receives incoming emails via Postmark webhook, validates signatures,
|
|
5
|
+
* and normalizes email content into the standard message format.
|
|
6
|
+
*/
|
|
7
|
+
import { createHmac } from 'crypto';
|
|
8
|
+
import { config } from '../config.js';
|
|
9
|
+
import { IntakeError, WebhookValidationError } from '../errors.js';
|
|
10
|
+
import { normalizePostmarkEmail, generateContentHash, validateMessageContent, scanAndAnnotateMessage, } from './normalizer.js';
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// Signature Validation
|
|
13
|
+
// =============================================================================
|
|
14
|
+
/**
|
|
15
|
+
* Validate Postmark webhook signature
|
|
16
|
+
*
|
|
17
|
+
* Note: Postmark uses a webhook token that's included in the URL
|
|
18
|
+
* rather than signature-based validation. However, we can validate
|
|
19
|
+
* the token if configured.
|
|
20
|
+
*/
|
|
21
|
+
export function validatePostmarkWebhook(payload, webhookToken) {
|
|
22
|
+
// If webhook token is configured, validate it was provided
|
|
23
|
+
if (config.postmark.webhookToken && webhookToken !== config.postmark.webhookToken) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
// Validate required fields exist
|
|
27
|
+
if (!payload || typeof payload !== 'object') {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const email = payload;
|
|
31
|
+
// Must have From and MessageID at minimum
|
|
32
|
+
if (!email.From || !email.MessageID) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Generate HMAC signature for webhook validation (if using custom signing)
|
|
39
|
+
*/
|
|
40
|
+
export function generatePostmarkSignature(body, secret) {
|
|
41
|
+
return createHmac('sha256', secret).update(body).digest('hex');
|
|
42
|
+
}
|
|
43
|
+
// =============================================================================
|
|
44
|
+
// Email Processing
|
|
45
|
+
// =============================================================================
|
|
46
|
+
/**
|
|
47
|
+
* Process an inbound Postmark email
|
|
48
|
+
*/
|
|
49
|
+
export async function processPostmarkEmail(payload) {
|
|
50
|
+
try {
|
|
51
|
+
// Normalize the email into standard message format
|
|
52
|
+
const message = normalizePostmarkEmail(payload);
|
|
53
|
+
// Validate content exists
|
|
54
|
+
validateMessageContent(message);
|
|
55
|
+
// Generate content hash for deduplication
|
|
56
|
+
const contentHash = generateContentHash(message);
|
|
57
|
+
// Scan for forbidden terms
|
|
58
|
+
const { hasForbiddenTerms, detectedTerms } = scanAndAnnotateMessage(message);
|
|
59
|
+
return {
|
|
60
|
+
success: true,
|
|
61
|
+
message,
|
|
62
|
+
contentHash,
|
|
63
|
+
hasForbiddenTerms,
|
|
64
|
+
detectedTerms,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (error instanceof IntakeError) {
|
|
69
|
+
return {
|
|
70
|
+
success: false,
|
|
71
|
+
hasForbiddenTerms: false,
|
|
72
|
+
detectedTerms: [],
|
|
73
|
+
error: error.message,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
success: false,
|
|
78
|
+
hasForbiddenTerms: false,
|
|
79
|
+
detectedTerms: [],
|
|
80
|
+
error: error instanceof Error ? error.message : 'Unknown error processing email',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Full webhook handler for Postmark inbound emails
|
|
86
|
+
*/
|
|
87
|
+
export async function handlePostmarkInbound(rawBody, webhookToken) {
|
|
88
|
+
// Parse JSON body
|
|
89
|
+
let payload;
|
|
90
|
+
try {
|
|
91
|
+
payload = JSON.parse(rawBody);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return {
|
|
95
|
+
success: false,
|
|
96
|
+
hasForbiddenTerms: false,
|
|
97
|
+
detectedTerms: [],
|
|
98
|
+
error: 'Invalid JSON payload',
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// Validate webhook
|
|
102
|
+
if (!validatePostmarkWebhook(payload, webhookToken)) {
|
|
103
|
+
throw new WebhookValidationError('email', {
|
|
104
|
+
context: { reason: 'Invalid webhook token or missing required fields' },
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
// Process the email
|
|
108
|
+
return processPostmarkEmail(payload);
|
|
109
|
+
}
|
|
110
|
+
// =============================================================================
|
|
111
|
+
// Spam/Auto-Reply Detection
|
|
112
|
+
// =============================================================================
|
|
113
|
+
/**
|
|
114
|
+
* Check if email is likely an auto-reply or out-of-office
|
|
115
|
+
*/
|
|
116
|
+
export function isAutoReply(email) {
|
|
117
|
+
const subject = email.Subject?.toLowerCase() ?? '';
|
|
118
|
+
const headers = email.Headers ?? [];
|
|
119
|
+
// Check common auto-reply headers
|
|
120
|
+
const autoReplyHeaders = ['Auto-Submitted', 'X-Auto-Response-Suppress', 'X-Autoreply'];
|
|
121
|
+
for (const header of headers) {
|
|
122
|
+
if (autoReplyHeaders.includes(header.Name)) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Check subject line patterns
|
|
127
|
+
const autoReplyPatterns = [
|
|
128
|
+
/^(re:\s*)?out of (the )?office/i,
|
|
129
|
+
/^(re:\s*)?automatic reply/i,
|
|
130
|
+
/^(re:\s*)?auto(matic)?[:\s-]?reply/i,
|
|
131
|
+
/^(re:\s*)?away from/i,
|
|
132
|
+
/vacation/i,
|
|
133
|
+
/\boof\b/i,
|
|
134
|
+
/do not reply/i,
|
|
135
|
+
/undeliverable/i,
|
|
136
|
+
/delivery (status )?notification/i,
|
|
137
|
+
/mail delivery (failed|failure)/i,
|
|
138
|
+
];
|
|
139
|
+
for (const pattern of autoReplyPatterns) {
|
|
140
|
+
if (pattern.test(subject)) {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Check From address patterns
|
|
145
|
+
const from = email.From.toLowerCase();
|
|
146
|
+
const noReplyPatterns = [
|
|
147
|
+
/noreply/,
|
|
148
|
+
/no-reply/,
|
|
149
|
+
/donotreply/,
|
|
150
|
+
/do-not-reply/,
|
|
151
|
+
/mailer-daemon/,
|
|
152
|
+
/postmaster/,
|
|
153
|
+
];
|
|
154
|
+
for (const pattern of noReplyPatterns) {
|
|
155
|
+
if (pattern.test(from)) {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Check if email is likely spam or marketing
|
|
163
|
+
*/
|
|
164
|
+
export function isLikelySpam(email) {
|
|
165
|
+
const subject = email.Subject?.toLowerCase() ?? '';
|
|
166
|
+
const body = (email.TextBody || email.HtmlBody || '').toLowerCase();
|
|
167
|
+
// Check for common spam indicators
|
|
168
|
+
const spamPatterns = [
|
|
169
|
+
/unsubscribe/i,
|
|
170
|
+
/click here to opt out/i,
|
|
171
|
+
/view in browser/i,
|
|
172
|
+
/update your preferences/i,
|
|
173
|
+
/manage your subscription/i,
|
|
174
|
+
/you are receiving this email because/i,
|
|
175
|
+
/this is an automated message/i,
|
|
176
|
+
];
|
|
177
|
+
// Marketing indicators (not necessarily spam, but probably not support)
|
|
178
|
+
const marketingPatterns = [
|
|
179
|
+
/limited time offer/i,
|
|
180
|
+
/act now/i,
|
|
181
|
+
/special offer/i,
|
|
182
|
+
/exclusive deal/i,
|
|
183
|
+
/free shipping/i,
|
|
184
|
+
/discount code/i,
|
|
185
|
+
/% off/i,
|
|
186
|
+
];
|
|
187
|
+
// Check patterns
|
|
188
|
+
const combinedText = `${subject} ${body}`;
|
|
189
|
+
let spamScore = 0;
|
|
190
|
+
for (const pattern of spamPatterns) {
|
|
191
|
+
if (pattern.test(combinedText)) {
|
|
192
|
+
spamScore += 2;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
for (const pattern of marketingPatterns) {
|
|
196
|
+
if (pattern.test(combinedText)) {
|
|
197
|
+
spamScore += 1;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Threshold for likely spam/marketing
|
|
201
|
+
return spamScore >= 3;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Classify email for processing
|
|
205
|
+
*/
|
|
206
|
+
export function classifyEmail(email) {
|
|
207
|
+
if (isAutoReply(email)) {
|
|
208
|
+
return {
|
|
209
|
+
shouldProcess: false,
|
|
210
|
+
reason: 'Auto-reply or out-of-office message',
|
|
211
|
+
classification: 'auto_reply',
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
if (isLikelySpam(email)) {
|
|
215
|
+
return {
|
|
216
|
+
shouldProcess: false,
|
|
217
|
+
reason: 'Likely spam or marketing email',
|
|
218
|
+
classification: 'spam_marketing',
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
// Check if email has meaningful content
|
|
222
|
+
const hasContent = email.TextBody?.trim() || email.StrippedTextReply?.trim();
|
|
223
|
+
if (!hasContent && !email.Subject?.trim()) {
|
|
224
|
+
return {
|
|
225
|
+
shouldProcess: false,
|
|
226
|
+
reason: 'Email has no meaningful content',
|
|
227
|
+
classification: 'unknown',
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
shouldProcess: true,
|
|
232
|
+
classification: 'support',
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
// =============================================================================
|
|
236
|
+
// Email Threading
|
|
237
|
+
// =============================================================================
|
|
238
|
+
/**
|
|
239
|
+
* Extract thread information from email headers
|
|
240
|
+
*/
|
|
241
|
+
export function extractEmailThread(email) {
|
|
242
|
+
const inReplyTo = email.InReplyTo?.trim();
|
|
243
|
+
const references = email.References?.split(/\s+/).filter(Boolean) ?? [];
|
|
244
|
+
// Extract message ID from angle brackets
|
|
245
|
+
const cleanMessageId = (id) => id.replace(/^<|>$/g, '');
|
|
246
|
+
return {
|
|
247
|
+
isReply: !!inReplyTo || references.length > 0,
|
|
248
|
+
originalMessageId: inReplyTo ? cleanMessageId(inReplyTo) : references[0],
|
|
249
|
+
threadMessageIds: references.map(cleanMessageId),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Extract quoted content from email body
|
|
254
|
+
*/
|
|
255
|
+
export function extractQuotedContent(body) {
|
|
256
|
+
// Common reply separators
|
|
257
|
+
const separatorPatterns = [
|
|
258
|
+
/^-{2,}.*original message.*-{2,}/im,
|
|
259
|
+
/^on .+ wrote:$/im,
|
|
260
|
+
/^from:.*sent:.*to:.*subject:/ims,
|
|
261
|
+
/^>.*$/gm,
|
|
262
|
+
/^_{10,}/m,
|
|
263
|
+
];
|
|
264
|
+
let newContent = body;
|
|
265
|
+
let quotedContent;
|
|
266
|
+
for (const pattern of separatorPatterns) {
|
|
267
|
+
const match = newContent.match(pattern);
|
|
268
|
+
if (match && match.index !== undefined) {
|
|
269
|
+
quotedContent = newContent.slice(match.index);
|
|
270
|
+
newContent = newContent.slice(0, match.index).trim();
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return { newContent, quotedContent };
|
|
275
|
+
}
|
|
276
|
+
//# sourceMappingURL=postmark.js.map
|