@auxiora/email-intelligence 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/triage.js ADDED
@@ -0,0 +1,109 @@
1
+ import { getLogger } from '@auxiora/logger';
2
+ const logger = getLogger('email-intelligence:triage');
3
+ const URGENT_KEYWORDS = ['urgent', 'asap', 'emergency', 'critical'];
4
+ const ACTION_KEYWORDS = ['please', 'could you', 'can you', 'would you', 'need you to', 'let me know', 'rsvp'];
5
+ export class EmailTriageEngine {
6
+ config;
7
+ constructor(config = {}) {
8
+ this.config = config;
9
+ }
10
+ triage(emails) {
11
+ return emails.map(email => this.triageSingle(email));
12
+ }
13
+ triageSingle(email) {
14
+ const urgent = this.checkUrgent(email);
15
+ if (urgent)
16
+ return urgent;
17
+ const action = this.checkAction(email);
18
+ if (action)
19
+ return action;
20
+ const newsletter = this.checkNewsletter(email);
21
+ if (newsletter)
22
+ return newsletter;
23
+ const spam = this.checkSpam(email);
24
+ if (spam)
25
+ return spam;
26
+ logger.debug('Email triaged as FYI', { emailId: email.id });
27
+ return {
28
+ emailId: email.id,
29
+ priority: 'fyi',
30
+ reason: 'No specific signals detected',
31
+ suggestedAction: 'none',
32
+ confidence: 0.5,
33
+ };
34
+ }
35
+ checkUrgent(email) {
36
+ const senderDomain = email.from.split('@')[1]?.toLowerCase() ?? '';
37
+ const senderAddress = email.from.toLowerCase();
38
+ const subjectLower = email.subject.toLowerCase();
39
+ // VIP sender
40
+ if (this.config.urgentSenders?.some(s => senderAddress.includes(s.toLowerCase()))) {
41
+ return this.makeResult(email.id, 'urgent', 'From VIP sender', 'reply', 0.9);
42
+ }
43
+ // VIP domain
44
+ if (this.config.vipDomains?.some(d => senderDomain === d.toLowerCase())) {
45
+ return this.makeResult(email.id, 'urgent', 'From VIP domain', 'reply', 0.9);
46
+ }
47
+ // High/urgent importance
48
+ if (email.importance === 'urgent' || email.importance === 'high') {
49
+ return this.makeResult(email.id, 'urgent', `Marked as ${email.importance} importance`, 'reply', 0.9);
50
+ }
51
+ // Urgent keywords in subject
52
+ if (URGENT_KEYWORDS.some(kw => subjectLower.includes(kw))) {
53
+ const matched = URGENT_KEYWORDS.find(kw => subjectLower.includes(kw));
54
+ return this.makeResult(email.id, 'urgent', `Subject contains "${matched}"`, 'reply', 0.9);
55
+ }
56
+ return null;
57
+ }
58
+ checkAction(email) {
59
+ if (!email.isDirect)
60
+ return null;
61
+ const bodyLower = (email.body ?? email.bodyPreview).toLowerCase();
62
+ // Check for question marks
63
+ if (bodyLower.includes('?')) {
64
+ return this.makeResult(email.id, 'action', 'Contains a question directed to you', 'reply', 0.8);
65
+ }
66
+ // Check for action keywords
67
+ const matched = ACTION_KEYWORDS.find(kw => bodyLower.includes(kw));
68
+ if (matched) {
69
+ return this.makeResult(email.id, 'action', `Contains request keyword "${matched}"`, 'reply', 0.7);
70
+ }
71
+ return null;
72
+ }
73
+ checkNewsletter(email) {
74
+ if (email.hasUnsubscribe) {
75
+ return this.makeResult(email.id, 'newsletter', 'Has List-Unsubscribe header', 'unsubscribe', 0.85);
76
+ }
77
+ const senderLower = email.from.toLowerCase();
78
+ if (this.config.newsletterSenders?.some(ns => senderLower.includes(ns.toLowerCase()))) {
79
+ return this.makeResult(email.id, 'newsletter', 'From known newsletter sender', 'unsubscribe', 0.85);
80
+ }
81
+ return null;
82
+ }
83
+ checkSpam(email) {
84
+ const senderLower = email.from.toLowerCase();
85
+ // Check spam patterns
86
+ if (this.config.spamPatterns?.some(sp => senderLower.includes(sp.toLowerCase()) || email.subject.toLowerCase().includes(sp.toLowerCase()))) {
87
+ return this.makeResult(email.id, 'spam', 'Matches spam pattern', 'archive', 0.6);
88
+ }
89
+ // Excessive caps in subject (more than 50% uppercase, at least 5 letters)
90
+ const letters = email.subject.replace(/[^a-zA-Z]/g, '');
91
+ if (letters.length >= 5) {
92
+ const upperCount = (email.subject.match(/[A-Z]/g) ?? []).length;
93
+ if (upperCount / letters.length > 0.5) {
94
+ return this.makeResult(email.id, 'spam', 'Subject has excessive capitalization', 'archive', 0.6);
95
+ }
96
+ }
97
+ // Body contains 'unsubscribe' but no List-Unsubscribe header
98
+ const bodyLower = (email.body ?? email.bodyPreview).toLowerCase();
99
+ if (bodyLower.includes('unsubscribe') && !email.hasUnsubscribe) {
100
+ return this.makeResult(email.id, 'spam', 'Contains "unsubscribe" without proper header', 'archive', 0.6);
101
+ }
102
+ return null;
103
+ }
104
+ makeResult(emailId, priority, reason, suggestedAction, confidence) {
105
+ logger.debug(`Email triaged as ${priority}`, { emailId, reason });
106
+ return { emailId, priority, reason, suggestedAction, confidence };
107
+ }
108
+ }
109
+ //# sourceMappingURL=triage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"triage.js","sourceRoot":"","sources":["../src/triage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ export interface EmailMessage {
2
+ id: string;
3
+ from: string;
4
+ to: string[];
5
+ cc?: string[];
6
+ subject: string;
7
+ bodyPreview: string;
8
+ body?: string;
9
+ receivedDateTime: string;
10
+ importance: 'low' | 'normal' | 'high' | 'urgent';
11
+ isRead: boolean;
12
+ hasAttachments: boolean;
13
+ conversationId: string;
14
+ categories?: string[];
15
+ /** Whether user is in TO (direct) or CC */
16
+ isDirect: boolean;
17
+ /** List-Unsubscribe header present */
18
+ hasUnsubscribe?: boolean;
19
+ }
20
+ export type TriagePriority = 'urgent' | 'action' | 'fyi' | 'spam' | 'newsletter';
21
+ export interface TriageResult {
22
+ emailId: string;
23
+ priority: TriagePriority;
24
+ reason: string;
25
+ suggestedAction: 'reply' | 'archive' | 'flag' | 'unsubscribe' | 'none';
26
+ confidence: number;
27
+ }
28
+ export interface SmartReplyDraft {
29
+ emailId: string;
30
+ replyBody: string;
31
+ tone: 'formal' | 'casual' | 'brief';
32
+ confidence: number;
33
+ }
34
+ export interface FollowUp {
35
+ id: string;
36
+ emailId: string;
37
+ promiseText: string;
38
+ detectedAt: number;
39
+ dueDate?: number;
40
+ status: 'pending' | 'completed' | 'overdue';
41
+ reminderSent: boolean;
42
+ }
43
+ export interface ThreadSummary {
44
+ conversationId: string;
45
+ summary: string;
46
+ messageCount: number;
47
+ participants: string[];
48
+ keyPoints: string[];
49
+ actionItems: string[];
50
+ latestTimestamp: string;
51
+ }
52
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,EAAE,CAAC;IACb,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAC;IACjD,MAAM,EAAE,OAAO,CAAC;IAChB,cAAc,EAAE,OAAO,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,2CAA2C;IAC3C,QAAQ,EAAE,OAAO,CAAC;IAClB,sCAAsC;IACtC,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,GAAG,YAAY,CAAC;AAEjF,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,cAAc,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,aAAa,GAAG,MAAM,CAAC;IACvE,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;IACpC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,SAAS,CAAC;IAC5C,YAAY,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;CACzB"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@auxiora/email-intelligence",
3
+ "version": "1.0.0",
4
+ "description": "Email intelligence: triage, smart reply, follow-up tracking, thread summarization",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "dependencies": {
15
+ "@auxiora/logger": "1.0.0",
16
+ "@auxiora/audit": "1.0.0"
17
+ },
18
+ "engines": {
19
+ "node": ">=22.0.0"
20
+ },
21
+ "scripts": {
22
+ "build": "tsc",
23
+ "clean": "rm -rf dist",
24
+ "typecheck": "tsc --noEmit"
25
+ }
26
+ }
@@ -0,0 +1,94 @@
1
+ import * as crypto from 'node:crypto';
2
+ import { getLogger } from '@auxiora/logger';
3
+ import type { EmailMessage, FollowUp } from './types.js';
4
+
5
+ const logger = getLogger('email-intelligence:follow-up');
6
+
7
+ const PROMISE_PATTERNS = [
8
+ /i[''\u2019]ll send/i,
9
+ /i will send/i,
10
+ /i[''\u2019]ll get back/i,
11
+ /i will get back/i,
12
+ /i[''\u2019]ll follow up/i,
13
+ /i will follow up/i,
14
+ /let me check/i,
15
+ /i[''\u2019]ll look into/i,
16
+ /i will look into/i,
17
+ ];
18
+
19
+ const DAY_NAMES = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
20
+
21
+ export class FollowUpTracker {
22
+ detectPromises(sentEmail: EmailMessage): FollowUp[] {
23
+ const body = sentEmail.body ?? sentEmail.bodyPreview;
24
+ const sentences = body.split(/[.!?\n]+/).map(s => s.trim()).filter(Boolean);
25
+ const followUps: FollowUp[] = [];
26
+ const now = Date.now();
27
+
28
+ for (const sentence of sentences) {
29
+ for (const pattern of PROMISE_PATTERNS) {
30
+ if (pattern.test(sentence)) {
31
+ const dueDate = this.extractDueDate(sentence, now);
32
+ followUps.push({
33
+ id: crypto.randomUUID(),
34
+ emailId: sentEmail.id,
35
+ promiseText: sentence,
36
+ detectedAt: now,
37
+ dueDate,
38
+ status: 'pending',
39
+ reminderSent: false,
40
+ });
41
+ break; // One match per sentence
42
+ }
43
+ }
44
+ }
45
+
46
+ logger.debug(`Detected ${followUps.length} promises`, { emailId: sentEmail.id });
47
+ return followUps;
48
+ }
49
+
50
+ checkOverdue(followUps: FollowUp[]): FollowUp[] {
51
+ const now = Date.now();
52
+ return followUps.filter(fu =>
53
+ fu.status === 'pending' && fu.dueDate !== undefined && fu.dueDate < now
54
+ );
55
+ }
56
+
57
+ markCompleted(followUps: FollowUp[], id: string): FollowUp[] {
58
+ return followUps.map(fu =>
59
+ fu.id === id ? { ...fu, status: 'completed' as const } : fu
60
+ );
61
+ }
62
+
63
+ private extractDueDate(sentence: string, now: number): number | undefined {
64
+ const lower = sentence.toLowerCase();
65
+
66
+ // "by end of day" / "by end of week"
67
+ if (lower.includes('by end of day') || lower.includes('by eod')) {
68
+ const date = new Date(now);
69
+ date.setHours(23, 59, 59, 999);
70
+ return date.getTime();
71
+ }
72
+
73
+ if (lower.includes('by end of week') || lower.includes('by eow')) {
74
+ const date = new Date(now);
75
+ const daysUntilFriday = (5 - date.getDay() + 7) % 7 || 7;
76
+ date.setDate(date.getDate() + daysUntilFriday);
77
+ date.setHours(23, 59, 59, 999);
78
+ return date.getTime();
79
+ }
80
+
81
+ // "by Monday", "by Tuesday", etc.
82
+ for (let i = 0; i < DAY_NAMES.length; i++) {
83
+ if (lower.includes(`by ${DAY_NAMES[i]}`)) {
84
+ const date = new Date(now);
85
+ const daysUntil = (i - date.getDay() + 7) % 7 || 7;
86
+ date.setDate(date.getDate() + daysUntil);
87
+ date.setHours(23, 59, 59, 999);
88
+ return date.getTime();
89
+ }
90
+ }
91
+
92
+ return undefined;
93
+ }
94
+ }
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ export type {
2
+ EmailMessage,
3
+ TriagePriority,
4
+ TriageResult,
5
+ SmartReplyDraft,
6
+ FollowUp,
7
+ ThreadSummary,
8
+ } from './types.js';
9
+
10
+ export { EmailTriageEngine, type TriageConfig } from './triage.js';
11
+ export { SmartReplyGenerator } from './smart-reply.js';
12
+ export { FollowUpTracker } from './follow-up.js';
13
+ export { ThreadSummarizer } from './thread-summarizer.js';
@@ -0,0 +1,100 @@
1
+ import { getLogger } from '@auxiora/logger';
2
+ import type { EmailMessage, SmartReplyDraft } from './types.js';
3
+
4
+ const logger = getLogger('email-intelligence:smart-reply');
5
+
6
+ export class SmartReplyGenerator {
7
+ generateQuickReplies(email: EmailMessage): SmartReplyDraft[] {
8
+ logger.debug('Generating quick replies', { emailId: email.id });
9
+
10
+ const bodyLower = (email.body ?? email.bodyPreview).toLowerCase();
11
+ const isMeetingInvite = this.isMeetingInvite(email);
12
+ const question = this.extractQuestion(email);
13
+
14
+ const replies: SmartReplyDraft[] = [];
15
+
16
+ // 1. Brief acknowledgment
17
+ if (isMeetingInvite) {
18
+ replies.push({
19
+ emailId: email.id,
20
+ replyBody: 'Thanks for the invite. I\'ll be there.',
21
+ tone: 'brief',
22
+ confidence: 0.7,
23
+ });
24
+ } else if (question) {
25
+ replies.push({
26
+ emailId: email.id,
27
+ replyBody: 'Got it, will review and get back to you shortly.',
28
+ tone: 'brief',
29
+ confidence: 0.6,
30
+ });
31
+ } else {
32
+ replies.push({
33
+ emailId: email.id,
34
+ replyBody: 'Thanks, received.',
35
+ tone: 'brief',
36
+ confidence: 0.7,
37
+ });
38
+ }
39
+
40
+ // 2. Detailed response template
41
+ if (isMeetingInvite) {
42
+ replies.push({
43
+ emailId: email.id,
44
+ replyBody: `Thank you for the meeting invitation regarding "${email.subject}". I have reviewed the details and will attend as scheduled. Please let me know if any preparation is needed on my end.`,
45
+ tone: 'formal',
46
+ confidence: 0.6,
47
+ });
48
+ } else if (question) {
49
+ replies.push({
50
+ emailId: email.id,
51
+ replyBody: `Thank you for your email regarding "${email.subject}". Regarding your question: "${question}" - I will look into this and provide a detailed response.`,
52
+ tone: 'formal',
53
+ confidence: 0.5,
54
+ });
55
+ } else if (this.isRequest(bodyLower)) {
56
+ replies.push({
57
+ emailId: email.id,
58
+ replyBody: `Thank you for your email regarding "${email.subject}". I have noted the request and will take the necessary action. I will follow up once completed.`,
59
+ tone: 'formal',
60
+ confidence: 0.5,
61
+ });
62
+ } else {
63
+ replies.push({
64
+ emailId: email.id,
65
+ replyBody: `Thank you for your email regarding "${email.subject}". I have reviewed the information and will follow up if needed.`,
66
+ tone: 'formal',
67
+ confidence: 0.5,
68
+ });
69
+ }
70
+
71
+ // 3. Decline/defer
72
+ replies.push({
73
+ emailId: email.id,
74
+ replyBody: 'Thanks for reaching out. I\'ll get back to you on this.',
75
+ tone: 'formal',
76
+ confidence: 0.6,
77
+ });
78
+
79
+ return replies;
80
+ }
81
+
82
+ private isMeetingInvite(email: EmailMessage): boolean {
83
+ const subjectLower = email.subject.toLowerCase();
84
+ const bodyLower = (email.body ?? email.bodyPreview).toLowerCase();
85
+ const meetingKeywords = ['meeting', 'invite', 'invitation', 'calendar', 'schedule', 'call', 'sync'];
86
+ return meetingKeywords.some(kw => subjectLower.includes(kw) || bodyLower.includes(kw));
87
+ }
88
+
89
+ private extractQuestion(email: EmailMessage): string | null {
90
+ const body = email.body ?? email.bodyPreview;
91
+ const sentences = body.split(/[.!?\n]+/).map(s => s.trim()).filter(Boolean);
92
+ const questionSentence = sentences.find(s => s.includes('?'));
93
+ return questionSentence ? questionSentence.replace(/\?$/, '').trim() + '?' : null;
94
+ }
95
+
96
+ private isRequest(bodyLower: string): boolean {
97
+ const requestKeywords = ['please', 'could you', 'can you', 'would you', 'need you to', 'kindly'];
98
+ return requestKeywords.some(kw => bodyLower.includes(kw));
99
+ }
100
+ }
@@ -0,0 +1,103 @@
1
+ import { getLogger } from '@auxiora/logger';
2
+ import type { EmailMessage, ThreadSummary } from './types.js';
3
+
4
+ const logger = getLogger('email-intelligence:thread-summarizer');
5
+
6
+ const ACTION_ITEM_PREFIXES = ['action:', 'todo:', 'task:'];
7
+ const ACTION_ITEM_KEYWORDS = ['need to', 'should', 'must', 'will'];
8
+
9
+ export class ThreadSummarizer {
10
+ summarize(messages: EmailMessage[]): ThreadSummary {
11
+ if (messages.length === 0) {
12
+ throw new Error('Cannot summarize an empty thread');
13
+ }
14
+
15
+ const conversationId = messages[0].conversationId;
16
+ const participants = this.extractParticipants(messages);
17
+ const keyPoints = this.extractKeyPoints(messages);
18
+ const actionItems = this.extractActionItems(messages);
19
+ const latestTimestamp = this.getLatestTimestamp(messages);
20
+ const subject = messages[0].subject;
21
+
22
+ const participantList = participants.length <= 3
23
+ ? participants.join(', ')
24
+ : `${participants.slice(0, 3).join(', ')} and ${participants.length - 3} others`;
25
+
26
+ const summary = `${messages.length} messages between ${participantList}. Topic: ${subject}. ${keyPoints.length} key points, ${actionItems.length} action items.`;
27
+
28
+ logger.debug('Thread summarized', { conversationId, messageCount: messages.length });
29
+
30
+ return {
31
+ conversationId,
32
+ summary,
33
+ messageCount: messages.length,
34
+ participants,
35
+ keyPoints,
36
+ actionItems,
37
+ latestTimestamp,
38
+ };
39
+ }
40
+
41
+ private extractParticipants(messages: EmailMessage[]): string[] {
42
+ const participants = new Set<string>();
43
+ for (const msg of messages) {
44
+ participants.add(msg.from);
45
+ for (const to of msg.to) {
46
+ participants.add(to);
47
+ }
48
+ if (msg.cc) {
49
+ for (const cc of msg.cc) {
50
+ participants.add(cc);
51
+ }
52
+ }
53
+ }
54
+ return [...participants];
55
+ }
56
+
57
+ private extractKeyPoints(messages: EmailMessage[]): string[] {
58
+ const keyPoints: string[] = [];
59
+ for (const msg of messages) {
60
+ const body = msg.body ?? msg.bodyPreview;
61
+ const sentences = body.split(/[.\n]+/).map(s => s.trim()).filter(Boolean);
62
+ for (const sentence of sentences) {
63
+ // Questions
64
+ if (sentence.includes('?')) {
65
+ keyPoints.push(sentence);
66
+ continue;
67
+ }
68
+ // Sentences with action verbs or dates
69
+ if (/\b(decided|agreed|confirmed|approved|rejected|deadline|due|scheduled)\b/i.test(sentence)) {
70
+ keyPoints.push(sentence);
71
+ }
72
+ }
73
+ }
74
+ return keyPoints;
75
+ }
76
+
77
+ private extractActionItems(messages: EmailMessage[]): string[] {
78
+ const actionItems: string[] = [];
79
+ for (const msg of messages) {
80
+ const body = msg.body ?? msg.bodyPreview;
81
+ const lines = body.split('\n').map(l => l.trim()).filter(Boolean);
82
+ for (const line of lines) {
83
+ const lineLower = line.toLowerCase();
84
+ // Lines starting with action/todo/task prefixes
85
+ if (ACTION_ITEM_PREFIXES.some(prefix => lineLower.startsWith(prefix))) {
86
+ actionItems.push(line);
87
+ continue;
88
+ }
89
+ // Lines containing action keywords
90
+ if (ACTION_ITEM_KEYWORDS.some(kw => lineLower.includes(kw))) {
91
+ actionItems.push(line);
92
+ }
93
+ }
94
+ }
95
+ return actionItems;
96
+ }
97
+
98
+ private getLatestTimestamp(messages: EmailMessage[]): string {
99
+ return messages.reduce((latest, msg) =>
100
+ msg.receivedDateTime > latest ? msg.receivedDateTime : latest
101
+ , messages[0].receivedDateTime);
102
+ }
103
+ }
package/src/triage.ts ADDED
@@ -0,0 +1,147 @@
1
+ import { getLogger } from '@auxiora/logger';
2
+ import type { EmailMessage, TriageResult, TriagePriority } from './types.js';
3
+
4
+ const logger = getLogger('email-intelligence:triage');
5
+
6
+ export interface TriageConfig {
7
+ urgentSenders?: string[];
8
+ vipDomains?: string[];
9
+ spamPatterns?: string[];
10
+ newsletterSenders?: string[];
11
+ }
12
+
13
+ const URGENT_KEYWORDS = ['urgent', 'asap', 'emergency', 'critical'];
14
+ const ACTION_KEYWORDS = ['please', 'could you', 'can you', 'would you', 'need you to', 'let me know', 'rsvp'];
15
+
16
+ export class EmailTriageEngine {
17
+ private config: TriageConfig;
18
+
19
+ constructor(config: TriageConfig = {}) {
20
+ this.config = config;
21
+ }
22
+
23
+ triage(emails: EmailMessage[]): TriageResult[] {
24
+ return emails.map(email => this.triageSingle(email));
25
+ }
26
+
27
+ triageSingle(email: EmailMessage): TriageResult {
28
+ const urgent = this.checkUrgent(email);
29
+ if (urgent) return urgent;
30
+
31
+ const action = this.checkAction(email);
32
+ if (action) return action;
33
+
34
+ const newsletter = this.checkNewsletter(email);
35
+ if (newsletter) return newsletter;
36
+
37
+ const spam = this.checkSpam(email);
38
+ if (spam) return spam;
39
+
40
+ logger.debug('Email triaged as FYI', { emailId: email.id });
41
+ return {
42
+ emailId: email.id,
43
+ priority: 'fyi',
44
+ reason: 'No specific signals detected',
45
+ suggestedAction: 'none',
46
+ confidence: 0.5,
47
+ };
48
+ }
49
+
50
+ private checkUrgent(email: EmailMessage): TriageResult | null {
51
+ const senderDomain = email.from.split('@')[1]?.toLowerCase() ?? '';
52
+ const senderAddress = email.from.toLowerCase();
53
+ const subjectLower = email.subject.toLowerCase();
54
+
55
+ // VIP sender
56
+ if (this.config.urgentSenders?.some(s => senderAddress.includes(s.toLowerCase()))) {
57
+ return this.makeResult(email.id, 'urgent', 'From VIP sender', 'reply', 0.9);
58
+ }
59
+
60
+ // VIP domain
61
+ if (this.config.vipDomains?.some(d => senderDomain === d.toLowerCase())) {
62
+ return this.makeResult(email.id, 'urgent', 'From VIP domain', 'reply', 0.9);
63
+ }
64
+
65
+ // High/urgent importance
66
+ if (email.importance === 'urgent' || email.importance === 'high') {
67
+ return this.makeResult(email.id, 'urgent', `Marked as ${email.importance} importance`, 'reply', 0.9);
68
+ }
69
+
70
+ // Urgent keywords in subject
71
+ if (URGENT_KEYWORDS.some(kw => subjectLower.includes(kw))) {
72
+ const matched = URGENT_KEYWORDS.find(kw => subjectLower.includes(kw))!;
73
+ return this.makeResult(email.id, 'urgent', `Subject contains "${matched}"`, 'reply', 0.9);
74
+ }
75
+
76
+ return null;
77
+ }
78
+
79
+ private checkAction(email: EmailMessage): TriageResult | null {
80
+ if (!email.isDirect) return null;
81
+
82
+ const bodyLower = (email.body ?? email.bodyPreview).toLowerCase();
83
+
84
+ // Check for question marks
85
+ if (bodyLower.includes('?')) {
86
+ return this.makeResult(email.id, 'action', 'Contains a question directed to you', 'reply', 0.8);
87
+ }
88
+
89
+ // Check for action keywords
90
+ const matched = ACTION_KEYWORDS.find(kw => bodyLower.includes(kw));
91
+ if (matched) {
92
+ return this.makeResult(email.id, 'action', `Contains request keyword "${matched}"`, 'reply', 0.7);
93
+ }
94
+
95
+ return null;
96
+ }
97
+
98
+ private checkNewsletter(email: EmailMessage): TriageResult | null {
99
+ if (email.hasUnsubscribe) {
100
+ return this.makeResult(email.id, 'newsletter', 'Has List-Unsubscribe header', 'unsubscribe', 0.85);
101
+ }
102
+
103
+ const senderLower = email.from.toLowerCase();
104
+ if (this.config.newsletterSenders?.some(ns => senderLower.includes(ns.toLowerCase()))) {
105
+ return this.makeResult(email.id, 'newsletter', 'From known newsletter sender', 'unsubscribe', 0.85);
106
+ }
107
+
108
+ return null;
109
+ }
110
+
111
+ private checkSpam(email: EmailMessage): TriageResult | null {
112
+ const senderLower = email.from.toLowerCase();
113
+
114
+ // Check spam patterns
115
+ if (this.config.spamPatterns?.some(sp => senderLower.includes(sp.toLowerCase()) || email.subject.toLowerCase().includes(sp.toLowerCase()))) {
116
+ return this.makeResult(email.id, 'spam', 'Matches spam pattern', 'archive', 0.6);
117
+ }
118
+
119
+ // Excessive caps in subject (more than 50% uppercase, at least 5 letters)
120
+ const letters = email.subject.replace(/[^a-zA-Z]/g, '');
121
+ if (letters.length >= 5) {
122
+ const upperCount = (email.subject.match(/[A-Z]/g) ?? []).length;
123
+ if (upperCount / letters.length > 0.5) {
124
+ return this.makeResult(email.id, 'spam', 'Subject has excessive capitalization', 'archive', 0.6);
125
+ }
126
+ }
127
+
128
+ // Body contains 'unsubscribe' but no List-Unsubscribe header
129
+ const bodyLower = (email.body ?? email.bodyPreview).toLowerCase();
130
+ if (bodyLower.includes('unsubscribe') && !email.hasUnsubscribe) {
131
+ return this.makeResult(email.id, 'spam', 'Contains "unsubscribe" without proper header', 'archive', 0.6);
132
+ }
133
+
134
+ return null;
135
+ }
136
+
137
+ private makeResult(
138
+ emailId: string,
139
+ priority: TriagePriority,
140
+ reason: string,
141
+ suggestedAction: TriageResult['suggestedAction'],
142
+ confidence: number,
143
+ ): TriageResult {
144
+ logger.debug(`Email triaged as ${priority}`, { emailId, reason });
145
+ return { emailId, priority, reason, suggestedAction, confidence };
146
+ }
147
+ }