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.
Files changed (140) hide show
  1. package/dist/admin-portal.d.ts +43 -0
  2. package/dist/admin-portal.d.ts.map +1 -0
  3. package/dist/admin-portal.js +166 -0
  4. package/dist/admin-portal.js.map +1 -0
  5. package/dist/analysis/entities.d.ts +73 -0
  6. package/dist/analysis/entities.d.ts.map +1 -0
  7. package/dist/analysis/entities.js +378 -0
  8. package/dist/analysis/entities.js.map +1 -0
  9. package/dist/analysis/index.d.ts +44 -0
  10. package/dist/analysis/index.d.ts.map +1 -0
  11. package/dist/analysis/index.js +243 -0
  12. package/dist/analysis/index.js.map +1 -0
  13. package/dist/analysis/intent.d.ts +49 -0
  14. package/dist/analysis/intent.d.ts.map +1 -0
  15. package/dist/analysis/intent.js +320 -0
  16. package/dist/analysis/intent.js.map +1 -0
  17. package/dist/analysis/sentiment.d.ts +57 -0
  18. package/dist/analysis/sentiment.d.ts.map +1 -0
  19. package/dist/analysis/sentiment.js +351 -0
  20. package/dist/analysis/sentiment.js.map +1 -0
  21. package/dist/brand/compliance.d.ts +122 -0
  22. package/dist/brand/compliance.d.ts.map +1 -0
  23. package/dist/brand/compliance.js +378 -0
  24. package/dist/brand/compliance.js.map +1 -0
  25. package/dist/brand/forbidden-terms.d.ts +99 -0
  26. package/dist/brand/forbidden-terms.d.ts.map +1 -0
  27. package/dist/brand/forbidden-terms.js +265 -0
  28. package/dist/brand/forbidden-terms.js.map +1 -0
  29. package/dist/brand/index.d.ts +10 -0
  30. package/dist/brand/index.d.ts.map +1 -0
  31. package/dist/brand/index.js +12 -0
  32. package/dist/brand/index.js.map +1 -0
  33. package/dist/config.d.ts +325 -0
  34. package/dist/config.d.ts.map +1 -0
  35. package/dist/config.js +492 -0
  36. package/dist/config.js.map +1 -0
  37. package/dist/delivery/index.d.ts +84 -0
  38. package/dist/delivery/index.d.ts.map +1 -0
  39. package/dist/delivery/index.js +435 -0
  40. package/dist/delivery/index.js.map +1 -0
  41. package/dist/embeddings/cache.d.ts +96 -0
  42. package/dist/embeddings/cache.d.ts.map +1 -0
  43. package/dist/embeddings/cache.js +193 -0
  44. package/dist/embeddings/cache.js.map +1 -0
  45. package/dist/embeddings/index.d.ts +152 -0
  46. package/dist/embeddings/index.d.ts.map +1 -0
  47. package/dist/embeddings/index.js +337 -0
  48. package/dist/embeddings/index.js.map +1 -0
  49. package/dist/embeddings/openai-client.d.ts +67 -0
  50. package/dist/embeddings/openai-client.d.ts.map +1 -0
  51. package/dist/embeddings/openai-client.js +190 -0
  52. package/dist/embeddings/openai-client.js.map +1 -0
  53. package/dist/errors.d.ts +302 -0
  54. package/dist/errors.d.ts.map +1 -0
  55. package/dist/errors.js +508 -0
  56. package/dist/errors.js.map +1 -0
  57. package/dist/escalation/index.d.ts +93 -0
  58. package/dist/escalation/index.d.ts.map +1 -0
  59. package/dist/escalation/index.js +436 -0
  60. package/dist/escalation/index.js.map +1 -0
  61. package/dist/extraction/deduplication.d.ts +97 -0
  62. package/dist/extraction/deduplication.d.ts.map +1 -0
  63. package/dist/extraction/deduplication.js +271 -0
  64. package/dist/extraction/deduplication.js.map +1 -0
  65. package/dist/extraction/gmail-extractor.d.ts +160 -0
  66. package/dist/extraction/gmail-extractor.d.ts.map +1 -0
  67. package/dist/extraction/gmail-extractor.js +396 -0
  68. package/dist/extraction/gmail-extractor.js.map +1 -0
  69. package/dist/extraction/gmail-token-manager.d.ts +36 -0
  70. package/dist/extraction/gmail-token-manager.d.ts.map +1 -0
  71. package/dist/extraction/gmail-token-manager.js +146 -0
  72. package/dist/extraction/gmail-token-manager.js.map +1 -0
  73. package/dist/extraction/index.d.ts +13 -0
  74. package/dist/extraction/index.d.ts.map +1 -0
  75. package/dist/extraction/index.js +20 -0
  76. package/dist/extraction/index.js.map +1 -0
  77. package/dist/extraction/pii-handler.d.ts +100 -0
  78. package/dist/extraction/pii-handler.d.ts.map +1 -0
  79. package/dist/extraction/pii-handler.js +295 -0
  80. package/dist/extraction/pii-handler.js.map +1 -0
  81. package/dist/extraction/pipeline.d.ts +94 -0
  82. package/dist/extraction/pipeline.d.ts.map +1 -0
  83. package/dist/extraction/pipeline.js +380 -0
  84. package/dist/extraction/pipeline.js.map +1 -0
  85. package/dist/extraction/quality-filter.d.ts +99 -0
  86. package/dist/extraction/quality-filter.d.ts.map +1 -0
  87. package/dist/extraction/quality-filter.js +370 -0
  88. package/dist/extraction/quality-filter.js.map +1 -0
  89. package/dist/extraction/rate-limiter.d.ts +90 -0
  90. package/dist/extraction/rate-limiter.d.ts.map +1 -0
  91. package/dist/extraction/rate-limiter.js +242 -0
  92. package/dist/extraction/rate-limiter.js.map +1 -0
  93. package/dist/extraction/state-manager.d.ts +126 -0
  94. package/dist/extraction/state-manager.d.ts.map +1 -0
  95. package/dist/extraction/state-manager.js +344 -0
  96. package/dist/extraction/state-manager.js.map +1 -0
  97. package/dist/generation/index.d.ts +75 -0
  98. package/dist/generation/index.d.ts.map +1 -0
  99. package/dist/generation/index.js +641 -0
  100. package/dist/generation/index.js.map +1 -0
  101. package/dist/index.d.ts +96 -0
  102. package/dist/index.d.ts.map +1 -0
  103. package/dist/index.js +233 -0
  104. package/dist/index.js.map +1 -0
  105. package/dist/intake/index.d.ts +15 -0
  106. package/dist/intake/index.d.ts.map +1 -0
  107. package/dist/intake/index.js +19 -0
  108. package/dist/intake/index.js.map +1 -0
  109. package/dist/intake/normalizer.d.ts +163 -0
  110. package/dist/intake/normalizer.d.ts.map +1 -0
  111. package/dist/intake/normalizer.js +309 -0
  112. package/dist/intake/normalizer.js.map +1 -0
  113. package/dist/intake/postmark.d.ts +72 -0
  114. package/dist/intake/postmark.d.ts.map +1 -0
  115. package/dist/intake/postmark.js +276 -0
  116. package/dist/intake/postmark.js.map +1 -0
  117. package/dist/intake/slack.d.ts +106 -0
  118. package/dist/intake/slack.d.ts.map +1 -0
  119. package/dist/intake/slack.js +378 -0
  120. package/dist/intake/slack.js.map +1 -0
  121. package/dist/intake/twilio.d.ts +86 -0
  122. package/dist/intake/twilio.d.ts.map +1 -0
  123. package/dist/intake/twilio.js +283 -0
  124. package/dist/intake/twilio.js.map +1 -0
  125. package/dist/knowledge/index.d.ts +100 -0
  126. package/dist/knowledge/index.d.ts.map +1 -0
  127. package/dist/knowledge/index.js +516 -0
  128. package/dist/knowledge/index.js.map +1 -0
  129. package/dist/knowledge/invoice-resolver.d.ts +62 -0
  130. package/dist/knowledge/invoice-resolver.d.ts.map +1 -0
  131. package/dist/knowledge/invoice-resolver.js +267 -0
  132. package/dist/knowledge/invoice-resolver.js.map +1 -0
  133. package/dist/types.d.ts +535 -0
  134. package/dist/types.d.ts.map +1 -0
  135. package/dist/types.js +48 -0
  136. package/dist/types.js.map +1 -0
  137. package/ga-service-account.json +13 -0
  138. package/gmail-knowledge-migration.sql +149 -0
  139. package/nul +1 -0
  140. 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