@vantageos/vantage-crm-mcp 0.1.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 (48) hide show
  1. package/README.md +260 -0
  2. package/dist/convex/crm/_helpers.js +24 -0
  3. package/dist/convex/crm/activities.js +220 -0
  4. package/dist/convex/crm/briefing.js +198 -0
  5. package/dist/convex/crm/calendarCron.js +92 -0
  6. package/dist/convex/crm/calendarCronDispatch.js +83 -0
  7. package/dist/convex/crm/calendarSync.js +294 -0
  8. package/dist/convex/crm/companies.js +323 -0
  9. package/dist/convex/crm/contacts.js +346 -0
  10. package/dist/convex/crm/deals.js +481 -0
  11. package/dist/convex/crm/emailActions.js +158 -0
  12. package/dist/convex/crm/emailCron.js +210 -0
  13. package/dist/convex/crm/emailCronDispatch.js +76 -0
  14. package/dist/convex/crm/emailSync.js +260 -0
  15. package/dist/convex/crm/onboarding.js +185 -0
  16. package/dist/convex/crm/stats.js +75 -0
  17. package/dist/convex/crm/tasks.js +109 -0
  18. package/dist/convex/crons.js +25 -0
  19. package/dist/convex/integrations.js +183 -0
  20. package/dist/convex/lib/auditLog.js +109 -0
  21. package/dist/convex/lib/auth.js +372 -0
  22. package/dist/convex/lib/rbac.js +123 -0
  23. package/dist/convex/lib/workspace.js +171 -0
  24. package/dist/convex/organizations.js +192 -0
  25. package/dist/convex/schema.js +690 -0
  26. package/dist/convex/users.js +217 -0
  27. package/dist/convex/workspaces.js +603 -0
  28. package/dist/mcp-server/lib/convexClient.js +50 -0
  29. package/dist/mcp-server/lib/scopeEnforcement.js +76 -0
  30. package/dist/mcp-server/registry.js +116 -0
  31. package/dist/mcp-server/server.js +97 -0
  32. package/dist/mcp-server/tests/registry.test.js +163 -0
  33. package/dist/mcp-server/tests/scopeEnforcement.test.js +137 -0
  34. package/dist/mcp-server/tests/security.test.js +257 -0
  35. package/dist/mcp-server/tests/tools.test.js +272 -0
  36. package/dist/mcp-server/tools/activities.js +207 -0
  37. package/dist/mcp-server/tools/admin.js +190 -0
  38. package/dist/mcp-server/tools/companies.js +233 -0
  39. package/dist/mcp-server/tools/contacts.js +306 -0
  40. package/dist/mcp-server/tools/customFields.js +222 -0
  41. package/dist/mcp-server/tools/customObjects.js +235 -0
  42. package/dist/mcp-server/tools/deals.js +297 -0
  43. package/dist/mcp-server/tools/rbac.js +177 -0
  44. package/dist/mcp-server/tools/search.js +155 -0
  45. package/dist/mcp-server/tools/workflows.js +234 -0
  46. package/dist/mcp-server/transport/http.js +257 -0
  47. package/dist/mcp-server/transport/stdio.js +90 -0
  48. package/package.json +45 -0
@@ -0,0 +1,210 @@
1
+ "use strict";
2
+ /**
3
+ * CRM Email Cron — Gmail Sync Action
4
+ *
5
+ * Convex internalAction that fetches recent emails from Gmail via Composio REST API.
6
+ * Stores HTML bodies in file storage, upserts into emailSync table.
7
+ * Triggered every 5 minutes by crons.ts.
8
+ *
9
+ * NOTE: Convex actions cannot import from repo-root lib/, so we call
10
+ * Composio's REST API directly via fetch().
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.pullFromGmail = void 0;
14
+ const server_1 = require("../_generated/server");
15
+ const api_1 = require("../_generated/api");
16
+ const values_1 = require("convex/values");
17
+ // Composio REST API base URL
18
+ const COMPOSIO_API_BASE = 'https://backend.composio.dev/api/v2';
19
+ // =============================================================================
20
+ // PULL FROM GMAIL — Cron-triggered action
21
+ // =============================================================================
22
+ exports.pullFromGmail = (0, server_1.internalAction)({
23
+ args: {
24
+ workspaceId: values_1.v.id('workspaces'),
25
+ entityId: values_1.v.string(),
26
+ },
27
+ handler: async (ctx, args) => {
28
+ const apiKey = process.env.COMPOSIO_API_KEY;
29
+ if (!apiKey) {
30
+ console.error('[EmailCron] COMPOSIO_API_KEY not set');
31
+ return { synced: 0, error: 'No API key' };
32
+ }
33
+ console.log('[EmailCron] Starting Gmail sync for workspace:', args.workspaceId);
34
+ try {
35
+ // Step 1: List recent messages via Composio
36
+ const listResponse = await executeComposioAction(apiKey, args.entityId, {
37
+ action: 'GMAIL_LIST_MESSAGES',
38
+ params: {
39
+ max_results: 20,
40
+ label_ids: ['INBOX'],
41
+ },
42
+ });
43
+ const messages = extractMessages(listResponse);
44
+ if (!messages || messages.length === 0) {
45
+ console.log('[EmailCron] No messages returned from Gmail');
46
+ return { synced: 0 };
47
+ }
48
+ console.log('[EmailCron] Found', messages.length, 'messages to process');
49
+ let syncedCount = 0;
50
+ for (const msg of messages) {
51
+ try {
52
+ // Step 2: Get full message details
53
+ const detailResponse = await executeComposioAction(apiKey, args.entityId, {
54
+ action: 'GMAIL_GET_MESSAGE',
55
+ params: {
56
+ message_id: msg.id,
57
+ format: 'full',
58
+ },
59
+ });
60
+ const detail = extractMessageDetail(detailResponse);
61
+ if (!detail) {
62
+ console.warn('[EmailCron] Could not parse message:', msg.id);
63
+ continue;
64
+ }
65
+ // Parse email fields
66
+ const headers = detail.payload?.headers ?? [];
67
+ const subject = getHeader(headers, 'Subject') || '(no subject)';
68
+ const from = getHeader(headers, 'From') || '';
69
+ const toRaw = getHeader(headers, 'To') || '';
70
+ const to = toRaw
71
+ .split(',')
72
+ .map((s) => s.trim())
73
+ .filter(Boolean);
74
+ // Extract body
75
+ const { plain } = extractBody(detail);
76
+ // Truncate plain text
77
+ const MAX_PLAIN = 10_000;
78
+ let bodyPlain = plain;
79
+ if (bodyPlain && bodyPlain.length > MAX_PLAIN) {
80
+ bodyPlain = bodyPlain.substring(0, MAX_PLAIN);
81
+ }
82
+ const snippet = detail.snippet || bodyPlain?.substring(0, 200);
83
+ // Determine sentAt from internalDate
84
+ const sentAt = detail.internalDate
85
+ ? parseInt(detail.internalDate, 10)
86
+ : Date.now();
87
+ // Step 3: Upsert into emailSync table
88
+ await ctx.runMutation(api_1.internal.crm.emailSync.upsertFromSync, {
89
+ workspaceId: args.workspaceId,
90
+ gmailId: msg.id,
91
+ threadId: msg.threadId || msg.id,
92
+ subject,
93
+ snippet,
94
+ from,
95
+ to,
96
+ bodyPlain,
97
+ sentAt,
98
+ });
99
+ syncedCount++;
100
+ }
101
+ catch (emailError) {
102
+ console.error('[EmailCron] Failed to process message:', msg.id, emailError);
103
+ }
104
+ }
105
+ console.log('[EmailCron] Synced', syncedCount, 'emails');
106
+ return { synced: syncedCount };
107
+ }
108
+ catch (error) {
109
+ console.error('[EmailCron] Gmail sync failed:', error);
110
+ return { synced: 0, error: String(error) };
111
+ }
112
+ },
113
+ });
114
+ // =============================================================================
115
+ // COMPOSIO REST API HELPERS
116
+ // =============================================================================
117
+ async function executeComposioAction(apiKey, entityId, body) {
118
+ const response = await fetch(`${COMPOSIO_API_BASE}/actions/execute`, {
119
+ method: 'POST',
120
+ headers: {
121
+ 'Content-Type': 'application/json',
122
+ 'x-api-key': apiKey,
123
+ },
124
+ body: JSON.stringify({
125
+ actionName: body.action,
126
+ connectedAccountId: undefined,
127
+ entityId,
128
+ input: body.params,
129
+ }),
130
+ });
131
+ if (!response.ok) {
132
+ const text = await response.text();
133
+ throw new Error(`Composio API error (${response.status}): ${text}`);
134
+ }
135
+ return (await response.json());
136
+ }
137
+ function extractMessages(response) {
138
+ // Composio wraps responses in data.response_data
139
+ const data = response?.data?.response_data ?? response?.data ?? response;
140
+ if (Array.isArray(data)) {
141
+ return data;
142
+ }
143
+ // Sometimes nested under 'messages'
144
+ const obj = data;
145
+ if (obj?.messages && Array.isArray(obj.messages)) {
146
+ return obj.messages;
147
+ }
148
+ return [];
149
+ }
150
+ function extractMessageDetail(response) {
151
+ const data = response?.data?.response_data ?? response?.data ?? response;
152
+ if (data && typeof data === 'object' && 'id' in data) {
153
+ return data;
154
+ }
155
+ return null;
156
+ }
157
+ function getHeader(headers, name) {
158
+ return headers.find((h) => h.name.toLowerCase() === name.toLowerCase())?.value;
159
+ }
160
+ function extractBody(detail) {
161
+ let plain;
162
+ let html;
163
+ const payload = detail.payload;
164
+ if (!payload)
165
+ return { plain, html };
166
+ // Single-part message
167
+ if (payload.body?.data) {
168
+ const decoded = decodeBase64Url(payload.body.data);
169
+ if (payload.mimeType === 'text/html') {
170
+ html = decoded;
171
+ }
172
+ else {
173
+ plain = decoded;
174
+ }
175
+ }
176
+ // Multi-part message
177
+ if (payload.parts) {
178
+ for (const part of payload.parts) {
179
+ if (part.mimeType === 'text/plain' && part.body?.data) {
180
+ plain = decodeBase64Url(part.body.data);
181
+ }
182
+ else if (part.mimeType === 'text/html' && part.body?.data) {
183
+ html = decodeBase64Url(part.body.data);
184
+ }
185
+ // Nested multipart (e.g., multipart/alternative inside multipart/mixed)
186
+ if (part.parts) {
187
+ for (const subpart of part.parts) {
188
+ if (subpart.mimeType === 'text/plain' && subpart.body?.data) {
189
+ plain = plain || decodeBase64Url(subpart.body.data);
190
+ }
191
+ else if (subpart.mimeType === 'text/html' &&
192
+ subpart.body?.data) {
193
+ html = html || decodeBase64Url(subpart.body.data);
194
+ }
195
+ }
196
+ }
197
+ }
198
+ }
199
+ return { plain, html };
200
+ }
201
+ function decodeBase64Url(encoded) {
202
+ // Gmail API uses URL-safe base64 encoding
203
+ const base64 = encoded.replace(/-/g, '+').replace(/_/g, '/');
204
+ try {
205
+ return Buffer.from(base64, 'base64').toString('utf-8');
206
+ }
207
+ catch {
208
+ return encoded;
209
+ }
210
+ }
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ /**
3
+ * Email Cron Dispatcher
4
+ *
5
+ * Finds all workspaces with Gmail sync enabled and triggers
6
+ * sync for each one. Mirrors calendarCronDispatch.ts pattern.
7
+ *
8
+ * The cron calls this dispatcher every 5 minutes.
9
+ * The dispatcher fans out to per-workspace sync actions.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.getEmailSyncWorkspaces = exports.dispatchEmailSync = void 0;
13
+ const server_1 = require("../_generated/server");
14
+ const api_1 = require("../_generated/api");
15
+ // =============================================================================
16
+ // DISPATCH — find workspaces and fan out sync actions
17
+ // =============================================================================
18
+ exports.dispatchEmailSync = (0, server_1.internalAction)({
19
+ args: {},
20
+ handler: async (ctx) => {
21
+ console.log('[EmailCronDispatch] Starting dispatch...');
22
+ // Get all workspaces with email sync enabled
23
+ const workspaces = await ctx.runQuery(api_1.internal.crm.emailCronDispatch.getEmailSyncWorkspaces);
24
+ if (workspaces.length === 0) {
25
+ console.log('[EmailCronDispatch] No workspaces with email sync enabled');
26
+ return { dispatched: 0 };
27
+ }
28
+ console.log('[EmailCronDispatch] Dispatching sync for', workspaces.length, 'workspaces');
29
+ let dispatched = 0;
30
+ for (const ws of workspaces) {
31
+ try {
32
+ await ctx.runAction(api_1.internal.crm.emailCron.pullFromGmail, {
33
+ workspaceId: ws.workspaceId,
34
+ entityId: ws.entityId,
35
+ });
36
+ dispatched++;
37
+ }
38
+ catch (error) {
39
+ console.error('[EmailCronDispatch] Failed for workspace:', ws.workspaceId, error);
40
+ }
41
+ }
42
+ console.log('[EmailCronDispatch] Dispatched', dispatched, 'sync jobs');
43
+ return { dispatched };
44
+ },
45
+ });
46
+ // =============================================================================
47
+ // QUERY — get workspaces with email sync enabled
48
+ // =============================================================================
49
+ exports.getEmailSyncWorkspaces = (0, server_1.internalQuery)({
50
+ args: {},
51
+ handler: async (ctx) => {
52
+ const workspaces = await ctx.db.query('workspaces').collect();
53
+ const results = [];
54
+ for (const ws of workspaces) {
55
+ // Only sync workspaces that have email sync enabled
56
+ const settings = ws.settings;
57
+ if (!settings?.emailSyncEnabled)
58
+ continue;
59
+ // Determine entityId
60
+ let entityId;
61
+ if (ws.ownerType === 'user' && ws.ownerId) {
62
+ const user = await ctx.db.get(ws.ownerId);
63
+ if (user && 'clerkId' in user) {
64
+ entityId = user.clerkId;
65
+ }
66
+ }
67
+ else if (ws.ownerType === 'organization' && ws.orgId) {
68
+ entityId = ws.orgId;
69
+ }
70
+ if (entityId) {
71
+ results.push({ workspaceId: ws._id, entityId });
72
+ }
73
+ }
74
+ return results;
75
+ },
76
+ });
@@ -0,0 +1,260 @@
1
+ "use strict";
2
+ /**
3
+ * CRM Email Sync — Mutations & Queries
4
+ *
5
+ * Syncs Gmail messages into the emailSync table via Composio.
6
+ * Auto-links to contacts (by email) and companies (by domain).
7
+ * V0.1.0: simplified schema — sentAt replaces receivedAt, cc/isRead/isStarred/hasAttachments removed.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.internalGenerateUploadUrl = exports.generateUploadUrl = exports.list = exports.getByGmailId = exports.search = exports.linkToContact = exports.upsertFromSync = void 0;
11
+ const server_1 = require("../_generated/server");
12
+ const values_1 = require("convex/values");
13
+ const workspace_1 = require("../lib/workspace");
14
+ // =============================================================================
15
+ // CONSTANTS
16
+ // =============================================================================
17
+ const MAX_BODY_PLAIN_LENGTH = 10_000; // 10KB truncation limit
18
+ const SNIPPET_LENGTH = 200;
19
+ // =============================================================================
20
+ // SYNC EMAILS (upsert by gmailId)
21
+ // =============================================================================
22
+ /**
23
+ * Internal mutation used by the cron action to upsert synced emails.
24
+ * Skips auth since cron runs server-side.
25
+ * OQ-1: actorId='agent:composio' tracked in activities created from this sync.
26
+ */
27
+ exports.upsertFromSync = (0, server_1.internalMutation)({
28
+ args: {
29
+ workspaceId: values_1.v.id('workspaces'),
30
+ gmailId: values_1.v.string(),
31
+ threadId: values_1.v.optional(values_1.v.string()),
32
+ subject: values_1.v.string(),
33
+ snippet: values_1.v.optional(values_1.v.string()),
34
+ from: values_1.v.string(),
35
+ to: values_1.v.optional(values_1.v.array(values_1.v.string())),
36
+ bodyPlain: values_1.v.optional(values_1.v.string()),
37
+ sentAt: values_1.v.number(),
38
+ },
39
+ returns: values_1.v.id('emailSync'),
40
+ handler: async (ctx, args) => {
41
+ // Check if email already synced
42
+ const existing = await ctx.db
43
+ .query('emailSync')
44
+ .withIndex('by_gmail_id', (q) => q.eq('gmailId', args.gmailId))
45
+ .first();
46
+ if (existing) {
47
+ return existing._id;
48
+ }
49
+ // Truncate bodyPlain
50
+ let bodyPlain = args.bodyPlain;
51
+ if (bodyPlain && bodyPlain.length > MAX_BODY_PLAIN_LENGTH) {
52
+ bodyPlain = bodyPlain.substring(0, MAX_BODY_PLAIN_LENGTH);
53
+ }
54
+ // Generate snippet from bodyPlain if not provided
55
+ const snippet = args.snippet ||
56
+ (bodyPlain ? bodyPlain.substring(0, SNIPPET_LENGTH) : undefined);
57
+ const now = Date.now();
58
+ // Auto-link: match from email against contacts
59
+ const fromEmail = extractEmail(args.from);
60
+ let matchedContactId;
61
+ let matchedCompanyId;
62
+ if (fromEmail) {
63
+ const contact = await ctx.db
64
+ .query('contacts')
65
+ .withIndex('by_workspace', (q) => q.eq('workspaceId', args.workspaceId))
66
+ .filter((q) => q.eq(q.field('email'), fromEmail))
67
+ .first();
68
+ if (contact) {
69
+ matchedContactId = contact._id;
70
+ if (contact.companyId) {
71
+ matchedCompanyId = contact.companyId;
72
+ }
73
+ }
74
+ else {
75
+ // Try by domain
76
+ const domain = extractDomain(fromEmail);
77
+ if (domain) {
78
+ const company = await ctx.db
79
+ .query('companies')
80
+ .withIndex('by_domain', (q) => q.eq('domain', domain))
81
+ .filter((q) => q.eq(q.field('workspaceId'), args.workspaceId))
82
+ .first();
83
+ if (company) {
84
+ matchedCompanyId = company._id;
85
+ }
86
+ }
87
+ }
88
+ }
89
+ const emailId = await ctx.db.insert('emailSync', {
90
+ gmailId: args.gmailId,
91
+ threadId: args.threadId,
92
+ subject: args.subject,
93
+ snippet,
94
+ from: args.from,
95
+ to: args.to,
96
+ bodyPlain,
97
+ contactId: matchedContactId,
98
+ companyId: matchedCompanyId,
99
+ workspaceId: args.workspaceId,
100
+ syncedAt: now,
101
+ sentAt: args.sentAt,
102
+ });
103
+ return emailId;
104
+ },
105
+ });
106
+ // =============================================================================
107
+ // LINK TO CONTACT (manual linking)
108
+ // =============================================================================
109
+ exports.linkToContact = (0, server_1.mutation)({
110
+ args: {
111
+ emailId: values_1.v.id('emailSync'),
112
+ contactId: values_1.v.optional(values_1.v.id('contacts')),
113
+ companyId: values_1.v.optional(values_1.v.id('companies')),
114
+ },
115
+ returns: values_1.v.id('emailSync'),
116
+ handler: async (ctx, args) => {
117
+ const email = await ctx.db.get(args.emailId);
118
+ if (!email)
119
+ throw new Error('Email not found');
120
+ const { canWrite } = await (0, workspace_1.validateWorkspaceAccess)(ctx, email.workspaceId);
121
+ if (!canWrite)
122
+ throw new Error('Insufficient permissions');
123
+ await ctx.db.patch(args.emailId, {
124
+ ...(args.contactId !== undefined && { contactId: args.contactId }),
125
+ ...(args.companyId !== undefined && { companyId: args.companyId }),
126
+ });
127
+ return args.emailId;
128
+ },
129
+ });
130
+ // =============================================================================
131
+ // SEARCH
132
+ // =============================================================================
133
+ exports.search = (0, server_1.query)({
134
+ args: {
135
+ workspaceId: values_1.v.id('workspaces'),
136
+ contactId: values_1.v.optional(values_1.v.id('contacts')),
137
+ subject: values_1.v.optional(values_1.v.string()),
138
+ dateFrom: values_1.v.optional(values_1.v.number()),
139
+ dateTo: values_1.v.optional(values_1.v.number()),
140
+ limit: values_1.v.optional(values_1.v.number()),
141
+ },
142
+ returns: values_1.v.array(values_1.v.any()),
143
+ handler: async (ctx, args) => {
144
+ await (0, workspace_1.validateWorkspaceAccess)(ctx, args.workspaceId);
145
+ const limit = args.limit ?? 20;
146
+ if (args.contactId) {
147
+ const results = await ctx.db
148
+ .query('emailSync')
149
+ .withIndex('by_contact', (q) => q.eq('contactId', args.contactId))
150
+ .order('desc')
151
+ .take(limit + 20);
152
+ return filterEmails(results, args).slice(0, limit);
153
+ }
154
+ const allResults = await ctx.db
155
+ .query('emailSync')
156
+ .withIndex('by_workspace_sent', (q) => q.eq('workspaceId', args.workspaceId))
157
+ .order('desc')
158
+ .take(limit * 5);
159
+ return filterEmails(allResults, args).slice(0, limit);
160
+ },
161
+ });
162
+ // =============================================================================
163
+ // GET BY GMAIL ID (for dedup check from actions)
164
+ // =============================================================================
165
+ exports.getByGmailId = (0, server_1.query)({
166
+ args: {
167
+ gmailId: values_1.v.string(),
168
+ },
169
+ returns: values_1.v.union(values_1.v.null(), values_1.v.any()),
170
+ handler: async (ctx, args) => {
171
+ return await ctx.db
172
+ .query('emailSync')
173
+ .withIndex('by_gmail_id', (q) => q.eq('gmailId', args.gmailId))
174
+ .first();
175
+ },
176
+ });
177
+ // =============================================================================
178
+ // LIST (for a workspace, ordered by sentAt)
179
+ // =============================================================================
180
+ exports.list = (0, server_1.query)({
181
+ args: {
182
+ workspaceId: values_1.v.id('workspaces'),
183
+ limit: values_1.v.optional(values_1.v.number()),
184
+ },
185
+ returns: values_1.v.array(values_1.v.any()),
186
+ handler: async (ctx, args) => {
187
+ await (0, workspace_1.validateWorkspaceAccess)(ctx, args.workspaceId);
188
+ const limit = args.limit ?? 20;
189
+ return await ctx.db
190
+ .query('emailSync')
191
+ .withIndex('by_workspace_sent', (q) => q.eq('workspaceId', args.workspaceId))
192
+ .order('desc')
193
+ .take(limit);
194
+ },
195
+ });
196
+ // =============================================================================
197
+ // GENERATE UPLOAD URL (for storing email HTML bodies in file storage)
198
+ // =============================================================================
199
+ exports.generateUploadUrl = (0, server_1.mutation)({
200
+ args: {},
201
+ returns: values_1.v.string(),
202
+ handler: async (ctx) => {
203
+ const identity = await ctx.auth.getUserIdentity();
204
+ if (!identity)
205
+ throw new Error('Unauthorized');
206
+ return await ctx.storage.generateUploadUrl();
207
+ },
208
+ });
209
+ exports.internalGenerateUploadUrl = (0, server_1.internalMutation)({
210
+ args: {},
211
+ returns: values_1.v.string(),
212
+ handler: async (ctx) => {
213
+ return await ctx.storage.generateUploadUrl();
214
+ },
215
+ });
216
+ // =============================================================================
217
+ // HELPERS
218
+ // =============================================================================
219
+ function extractEmail(fromHeader) {
220
+ const match = fromHeader.match(/<([^>]+)>/);
221
+ if (match)
222
+ return match[1].toLowerCase();
223
+ if (fromHeader.includes('@'))
224
+ return fromHeader.trim().toLowerCase();
225
+ return null;
226
+ }
227
+ function extractDomain(email) {
228
+ const parts = email.split('@');
229
+ if (parts.length !== 2)
230
+ return null;
231
+ const domain = parts[1].toLowerCase();
232
+ const genericDomains = [
233
+ 'gmail.com',
234
+ 'yahoo.com',
235
+ 'hotmail.com',
236
+ 'outlook.com',
237
+ 'live.com',
238
+ 'icloud.com',
239
+ 'protonmail.com',
240
+ 'aol.com',
241
+ 'mail.com',
242
+ ];
243
+ if (genericDomains.includes(domain))
244
+ return null;
245
+ return domain;
246
+ }
247
+ function filterEmails(results, args) {
248
+ let filtered = results;
249
+ if (args.subject) {
250
+ const subjectLower = args.subject.toLowerCase();
251
+ filtered = filtered.filter((e) => e.subject.toLowerCase().includes(subjectLower));
252
+ }
253
+ if (args.dateFrom) {
254
+ filtered = filtered.filter((e) => e.sentAt >= args.dateFrom);
255
+ }
256
+ if (args.dateTo) {
257
+ filtered = filtered.filter((e) => e.sentAt <= args.dateTo);
258
+ }
259
+ return filtered;
260
+ }