@realtimex/email-automator 2.11.2 → 2.12.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/api/src/routes/index.ts +2 -0
- package/api/src/routes/rulePacks.ts +204 -0
- package/api/src/services/RulePackService.ts +354 -0
- package/api/src/services/SDKService.ts +18 -7
- package/api/src/services/gmail.ts +68 -0
- package/api/src/services/intelligence.ts +160 -22
- package/api/src/services/microsoft.ts +99 -0
- package/api/src/services/processor.ts +146 -12
- package/api/src/services/rulePacks/developer.ts +179 -0
- package/api/src/services/rulePacks/executive.ts +143 -0
- package/api/src/services/rulePacks/index.ts +63 -0
- package/api/src/services/rulePacks/operations.ts +183 -0
- package/api/src/services/rulePacks/sales.ts +160 -0
- package/api/src/services/rulePacks/types.ts +116 -0
- package/api/src/services/rulePacks/universal.ts +83 -0
- package/api/src/services/supabase.ts +40 -0
- package/dist/api/src/routes/index.js +2 -0
- package/dist/api/src/routes/rulePacks.js +179 -0
- package/dist/api/src/services/RulePackService.js +296 -0
- package/dist/api/src/services/SDKService.js +18 -7
- package/dist/api/src/services/gmail.js +56 -0
- package/dist/api/src/services/intelligence.js +153 -21
- package/dist/api/src/services/microsoft.js +79 -0
- package/dist/api/src/services/processor.js +133 -12
- package/dist/api/src/services/rulePacks/developer.js +176 -0
- package/dist/api/src/services/rulePacks/executive.js +140 -0
- package/dist/api/src/services/rulePacks/index.js +58 -0
- package/dist/api/src/services/rulePacks/operations.js +180 -0
- package/dist/api/src/services/rulePacks/sales.js +157 -0
- package/dist/api/src/services/rulePacks/types.js +7 -0
- package/dist/api/src/services/rulePacks/universal.js +80 -0
- package/dist/assets/index-B5rXh3y8.css +1 -0
- package/dist/assets/index-B62ViZum.js +105 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/supabase/migrations/20260131000000_add_rule_packs.sql +176 -0
- package/supabase/migrations/20260131000001_set_zero_config_defaults.sql +51 -0
- package/supabase/migrations/20260131095000_backfill_user_settings.sql +36 -0
- package/supabase/migrations/20260131100000_rule_templates_table.sql +154 -0
- package/supabase/migrations/20260131110000_auto_init_user_data.sql +90 -0
- package/supabase/migrations/20260131120000_backfill_universal_pack.sql +84 -0
- package/supabase/migrations/20260131130000_simplify_rules_with_categories.sql +87 -0
- package/supabase/migrations/20260131140000_fix_action_constraint.sql +11 -0
- package/supabase/migrations/20260131150000_fix_trigger_error_handling.sql +71 -0
- package/supabase/migrations/20260131160000_enable_intelligent_rename_by_default.sql +14 -0
- package/dist/assets/index-BuWrl4UD.js +0 -105
- package/dist/assets/index-CtDzSy0n.css +0 -1
|
@@ -441,6 +441,74 @@ export class GmailService {
|
|
|
441
441
|
logger.debug('Message starred', { messageId });
|
|
442
442
|
}
|
|
443
443
|
|
|
444
|
+
/**
|
|
445
|
+
* Get or create a label by name (supports nested labels like "Finance/Receipts")
|
|
446
|
+
* Returns the label ID
|
|
447
|
+
*/
|
|
448
|
+
async getOrCreateLabel(account: EmailAccount, labelPath: string): Promise<string> {
|
|
449
|
+
const gmail = await this.getAuthenticatedClient(account);
|
|
450
|
+
|
|
451
|
+
// List existing labels
|
|
452
|
+
const { data: labelsData } = await gmail.users.labels.list({ userId: 'me' });
|
|
453
|
+
const existingLabels = labelsData.labels || [];
|
|
454
|
+
|
|
455
|
+
// Check if label already exists
|
|
456
|
+
const existingLabel = existingLabels.find(l => l.name === labelPath);
|
|
457
|
+
if (existingLabel?.id) {
|
|
458
|
+
logger.debug('Label already exists', { labelPath, labelId: existingLabel.id });
|
|
459
|
+
return existingLabel.id;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Create nested labels if needed (e.g., "Finance/Receipts" creates "Finance" then "Finance/Receipts")
|
|
463
|
+
const parts = labelPath.split('/');
|
|
464
|
+
let currentPath = '';
|
|
465
|
+
let parentId: string | undefined;
|
|
466
|
+
|
|
467
|
+
for (const part of parts) {
|
|
468
|
+
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
|
469
|
+
|
|
470
|
+
// Check if this level exists
|
|
471
|
+
const existing = existingLabels.find(l => l.name === currentPath);
|
|
472
|
+
if (existing?.id) {
|
|
473
|
+
parentId = existing.id;
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Create this level
|
|
478
|
+
const { data: newLabel } = await gmail.users.labels.create({
|
|
479
|
+
userId: 'me',
|
|
480
|
+
requestBody: {
|
|
481
|
+
name: currentPath,
|
|
482
|
+
labelListVisibility: 'labelShow',
|
|
483
|
+
messageListVisibility: 'show'
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
if (!newLabel.id) {
|
|
488
|
+
throw new Error(`Failed to create label: ${currentPath}`);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
logger.info('Created label', { labelPath: currentPath, labelId: newLabel.id });
|
|
492
|
+
parentId = newLabel.id;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (!parentId) {
|
|
496
|
+
throw new Error(`Failed to get or create label: ${labelPath}`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return parentId;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Apply a label to a message by label name (creates label if needed)
|
|
504
|
+
* Supports nested labels like "Finance/Receipts"
|
|
505
|
+
*/
|
|
506
|
+
async applyLabelByName(account: EmailAccount, messageId: string, labelName: string): Promise<void> {
|
|
507
|
+
const labelId = await this.getOrCreateLabel(account, labelName);
|
|
508
|
+
await this.addLabel(account, messageId, [labelId]);
|
|
509
|
+
logger.debug('Applied label to message', { messageId, labelName, labelId });
|
|
510
|
+
}
|
|
511
|
+
|
|
444
512
|
private async fetchAttachment(supabase: SupabaseClient, path: string): Promise<Uint8Array> {
|
|
445
513
|
const { data, error } = await supabase.storage
|
|
446
514
|
.from('rule-attachments')
|
|
@@ -91,7 +91,7 @@ export class IntelligenceService {
|
|
|
91
91
|
return this.isConfigured && !!SDKService.getSDK();
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
async analyzeEmail(content: string, context: EmailContext, eventLogger?: EventLogger, emailId?: string): Promise<(EmailAnalysis & { _metadata?: any }) | null> {
|
|
94
|
+
async analyzeEmail(content: string, context: EmailContext, eventLogger?: EventLogger, emailId?: string, llmSettings?: { llm_provider?: string; llm_model?: string }): Promise<(EmailAnalysis & { _metadata?: any }) | null> {
|
|
95
95
|
const sdk = SDKService.getSDK();
|
|
96
96
|
if (!sdk) {
|
|
97
97
|
logger.warn('Intelligence service not ready, skipping analysis');
|
|
@@ -101,7 +101,10 @@ export class IntelligenceService {
|
|
|
101
101
|
return null;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
const { provider, model, isDefaultFallback } = await SDKService.resolveChatProvider({
|
|
104
|
+
const { provider, model, isDefaultFallback } = await SDKService.resolveChatProvider({
|
|
105
|
+
llm_provider: llmSettings?.llm_provider,
|
|
106
|
+
llm_model: llmSettings?.llm_model
|
|
107
|
+
});
|
|
105
108
|
|
|
106
109
|
const cleanedContent = ContentCleaner.cleanEmailBody(content).substring(0, 2500);
|
|
107
110
|
|
|
@@ -134,10 +137,10 @@ REQUIRED JSON STRUCTURE:
|
|
|
134
137
|
|
|
135
138
|
if (eventLogger) {
|
|
136
139
|
await eventLogger.info('Thinking', `Analyzing email: ${context.subject}`, {
|
|
137
|
-
model
|
|
138
|
-
provider,
|
|
140
|
+
provider: `${provider}/${model}`,
|
|
139
141
|
is_fallback: isDefaultFallback,
|
|
140
|
-
|
|
142
|
+
signals: metadataSignals,
|
|
143
|
+
content_preview: cleanedContent.substring(0, 100) + '...'
|
|
141
144
|
}, emailId);
|
|
142
145
|
}
|
|
143
146
|
|
|
@@ -147,8 +150,27 @@ REQUIRED JSON STRUCTURE:
|
|
|
147
150
|
{ role: 'user', content: cleanedContent || '[Empty body]' }
|
|
148
151
|
], { provider, model });
|
|
149
152
|
|
|
153
|
+
// Check if SDK call failed
|
|
154
|
+
if (!response.success || response.error) {
|
|
155
|
+
const errorMsg = response.error || 'Unknown SDK error';
|
|
156
|
+
logger.error('SDK chat failed for email analysis', {
|
|
157
|
+
provider,
|
|
158
|
+
model,
|
|
159
|
+
error: errorMsg,
|
|
160
|
+
code: response.code
|
|
161
|
+
});
|
|
162
|
+
if (eventLogger) await eventLogger.error('SDK Error', `${errorMsg} (${provider}/${model})`, emailId);
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
150
166
|
const rawResponse = response.response?.content || '';
|
|
151
|
-
|
|
167
|
+
if (!rawResponse) {
|
|
168
|
+
logger.warn('SDK returned empty response for analysis', { provider, model });
|
|
169
|
+
if (eventLogger) await eventLogger.error('Empty Response', `LLM (${provider}/${model}) returned no content`, emailId);
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const validated = this.parseRobustJSON<EmailAnalysis>(rawResponse, EmailAnalysisSchema, eventLogger, emailId);
|
|
152
174
|
|
|
153
175
|
const result = validated ? {
|
|
154
176
|
...validated,
|
|
@@ -165,6 +187,11 @@ REQUIRED JSON STRUCTURE:
|
|
|
165
187
|
...result,
|
|
166
188
|
_raw_response: rawResponse
|
|
167
189
|
});
|
|
190
|
+
} else if (eventLogger && !result) {
|
|
191
|
+
await eventLogger.error('Malformed Response', {
|
|
192
|
+
message: 'AI returned data that did not match the required schema',
|
|
193
|
+
raw_response: rawResponse.substring(0, 500)
|
|
194
|
+
}, emailId);
|
|
168
195
|
}
|
|
169
196
|
|
|
170
197
|
return result;
|
|
@@ -177,12 +204,16 @@ REQUIRED JSON STRUCTURE:
|
|
|
177
204
|
|
|
178
205
|
async generateDraftReply(
|
|
179
206
|
originalEmail: { subject: string; sender: string; body: string },
|
|
180
|
-
instructions?: string
|
|
207
|
+
instructions?: string,
|
|
208
|
+
llmSettings?: { llm_provider?: string; llm_model?: string }
|
|
181
209
|
): Promise<string | null> {
|
|
182
210
|
const sdk = SDKService.getSDK();
|
|
183
211
|
if (!sdk) return null;
|
|
184
212
|
|
|
185
|
-
const { provider, model } = await SDKService.resolveChatProvider({
|
|
213
|
+
const { provider, model } = await SDKService.resolveChatProvider({
|
|
214
|
+
llm_provider: llmSettings?.llm_provider,
|
|
215
|
+
llm_model: llmSettings?.llm_model
|
|
216
|
+
});
|
|
186
217
|
|
|
187
218
|
try {
|
|
188
219
|
const response = await sdk.llm.chat([
|
|
@@ -196,6 +227,17 @@ REQUIRED JSON STRUCTURE:
|
|
|
196
227
|
},
|
|
197
228
|
], { provider, model });
|
|
198
229
|
|
|
230
|
+
// Check if SDK call failed
|
|
231
|
+
if (!response.success || response.error) {
|
|
232
|
+
logger.error('SDK chat failed for draft generation', {
|
|
233
|
+
provider,
|
|
234
|
+
model,
|
|
235
|
+
error: response.error,
|
|
236
|
+
code: response.code
|
|
237
|
+
});
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
199
241
|
return response.response?.content || null;
|
|
200
242
|
} catch (error) {
|
|
201
243
|
logger.error('Draft generation failed', error);
|
|
@@ -208,12 +250,16 @@ REQUIRED JSON STRUCTURE:
|
|
|
208
250
|
context: EmailContext,
|
|
209
251
|
compiledRulesContext: string | RuleContext[],
|
|
210
252
|
eventLogger?: EventLogger,
|
|
211
|
-
emailId?: string
|
|
253
|
+
emailId?: string,
|
|
254
|
+
llmSettings?: { llm_provider?: string; llm_model?: string }
|
|
212
255
|
): Promise<(ContextAwareAnalysis & { _metadata?: any }) | null> {
|
|
213
256
|
const sdk = SDKService.getSDK();
|
|
214
257
|
if (!sdk) return null;
|
|
215
258
|
|
|
216
|
-
const { provider, model, isDefaultFallback } = await SDKService.resolveChatProvider({
|
|
259
|
+
const { provider, model, isDefaultFallback } = await SDKService.resolveChatProvider({
|
|
260
|
+
llm_provider: llmSettings?.llm_provider,
|
|
261
|
+
llm_model: llmSettings?.llm_model
|
|
262
|
+
});
|
|
217
263
|
const cleanedContent = ContentCleaner.cleanEmailBody(content).substring(0, 2500);
|
|
218
264
|
|
|
219
265
|
let rulesContext: string;
|
|
@@ -223,24 +269,71 @@ REQUIRED JSON STRUCTURE:
|
|
|
223
269
|
rulesContext = compiledRulesContext.map(r => `- ${r.name}: ${r.intent}`).join('\n');
|
|
224
270
|
}
|
|
225
271
|
|
|
226
|
-
const systemPrompt = `You are an AI Automation Agent.
|
|
272
|
+
const systemPrompt = `You are an AI Automation Agent. Analyze the email and match it against the user's rules.
|
|
273
|
+
|
|
274
|
+
Rules Context:
|
|
275
|
+
${rulesContext}
|
|
276
|
+
|
|
277
|
+
REQUIRED JSON STRUCTURE:
|
|
278
|
+
{
|
|
279
|
+
"summary": "A brief summary of the email content",
|
|
280
|
+
"category": "spam|newsletter|promotional|transactional|social|support|client|internal|personal|other",
|
|
281
|
+
"priority": "High|Medium|Low",
|
|
282
|
+
"matched_rule": {
|
|
283
|
+
"rule_id": "string or null",
|
|
284
|
+
"rule_name": "string or null",
|
|
285
|
+
"confidence": 0.0 to 1.0,
|
|
286
|
+
"reasoning": "Brief explanation"
|
|
287
|
+
},
|
|
288
|
+
"actions_to_execute": ["none"|"delete"|"archive"|"draft"|"read"|"star"],
|
|
289
|
+
"draft_content": "Suggested reply if drafting, otherwise null"
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
IMPORTANT:
|
|
293
|
+
- Use "draft" action only if a rule explicitly requests it or if it's very clear a reply is needed.
|
|
294
|
+
- Categorize accurately.
|
|
295
|
+
- Confidence 0.7+ is required for automatic execution.`;
|
|
227
296
|
|
|
228
297
|
if (eventLogger) {
|
|
229
298
|
await eventLogger.info('Thinking', `Context-aware analysis: ${context.subject}`, {
|
|
230
|
-
model
|
|
231
|
-
|
|
232
|
-
|
|
299
|
+
provider: `${provider}/${model}`,
|
|
300
|
+
is_fallback: isDefaultFallback,
|
|
301
|
+
rules_count: Array.isArray(compiledRulesContext) ? compiledRulesContext.length : 'compiled'
|
|
233
302
|
}, emailId);
|
|
234
303
|
}
|
|
235
304
|
|
|
236
305
|
try {
|
|
306
|
+
logger.debug('Calling SDK chat for rule analysis', { provider, model, promptLength: systemPrompt.length });
|
|
237
307
|
const response = await sdk.llm.chat([
|
|
238
308
|
{ role: 'system', content: systemPrompt },
|
|
239
309
|
{ role: 'user', content: cleanedContent || '[Empty body]' }
|
|
240
310
|
], { provider, model });
|
|
241
311
|
|
|
312
|
+
// Check if SDK call failed
|
|
313
|
+
if (!response.success || response.error) {
|
|
314
|
+
const errorMsg = response.error || 'Unknown SDK error';
|
|
315
|
+
logger.error('SDK chat failed for rule analysis', {
|
|
316
|
+
provider,
|
|
317
|
+
model,
|
|
318
|
+
error: errorMsg,
|
|
319
|
+
code: response.code
|
|
320
|
+
});
|
|
321
|
+
if (eventLogger) await eventLogger.error('SDK Error', `${errorMsg} (${provider}/${model})`, emailId);
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
|
|
242
325
|
const rawResponse = response.response?.content || '';
|
|
243
|
-
|
|
326
|
+
if (!rawResponse) {
|
|
327
|
+
logger.warn('SDK returned empty response for rule analysis', {
|
|
328
|
+
provider,
|
|
329
|
+
model,
|
|
330
|
+
success: response.success
|
|
331
|
+
});
|
|
332
|
+
if (eventLogger) await eventLogger.error('Empty Response', `LLM (${provider}/${model}) returned no content`, emailId);
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const validated = this.parseRobustJSON<ContextAwareAnalysis>(rawResponse, ContextAwareAnalysisSchema, eventLogger, emailId);
|
|
244
337
|
|
|
245
338
|
const result = validated ? {
|
|
246
339
|
...validated,
|
|
@@ -257,12 +350,23 @@ REQUIRED JSON STRUCTURE:
|
|
|
257
350
|
...result,
|
|
258
351
|
_raw_response: rawResponse
|
|
259
352
|
});
|
|
353
|
+
} else if (eventLogger && !result) {
|
|
354
|
+
await eventLogger.error('Malformed Response', {
|
|
355
|
+
message: 'AI returned rule analysis that did not match the required schema',
|
|
356
|
+
raw_response: rawResponse.substring(0, 500)
|
|
357
|
+
}, emailId);
|
|
260
358
|
}
|
|
261
359
|
|
|
262
360
|
return result;
|
|
263
361
|
} catch (error: any) {
|
|
264
|
-
logger.error('Rule analysis failed',
|
|
265
|
-
|
|
362
|
+
logger.error('Rule analysis failed', {
|
|
363
|
+
error: error.message,
|
|
364
|
+
stack: error.stack,
|
|
365
|
+
provider,
|
|
366
|
+
model,
|
|
367
|
+
errorType: error.constructor.name
|
|
368
|
+
});
|
|
369
|
+
if (eventLogger) await eventLogger.error('Error', `${error.message} (${provider}/${model})`, emailId);
|
|
266
370
|
return null;
|
|
267
371
|
}
|
|
268
372
|
}
|
|
@@ -283,14 +387,48 @@ REQUIRED JSON STRUCTURE:
|
|
|
283
387
|
}
|
|
284
388
|
}
|
|
285
389
|
|
|
286
|
-
private parseRobustJSON<T>(input: string, schema: z.ZodSchema<T
|
|
390
|
+
private parseRobustJSON<T>(input: string, schema: z.ZodSchema<T>, eventLogger?: EventLogger, emailId?: string): T | null {
|
|
287
391
|
try {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
392
|
+
// 1. Remove common LLM artifacts and markdown blocks
|
|
393
|
+
let cleaned = input.trim();
|
|
394
|
+
|
|
395
|
+
// Handle markdown blocks
|
|
396
|
+
if (cleaned.includes('```json')) {
|
|
397
|
+
cleaned = cleaned.split('```json')[1].split('```')[0].trim();
|
|
398
|
+
} else if (cleaned.includes('```')) {
|
|
399
|
+
cleaned = cleaned.split('```')[1].split('```')[0].trim();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// 2. Extract the first { ... } block if visible
|
|
403
|
+
const jsonMatch = cleaned.match(/\{[\s\S]*\}/);
|
|
404
|
+
if (jsonMatch) {
|
|
405
|
+
cleaned = jsonMatch[0];
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// 3. Strip aggressive local LLM tokens
|
|
409
|
+
cleaned = cleaned.replace(/<\|[\s\S]*?\|>/g, '').trim();
|
|
410
|
+
|
|
411
|
+
// 4. Parse and Normalize
|
|
291
412
|
const parsed = JSON.parse(cleaned);
|
|
413
|
+
|
|
414
|
+
// Normalize actions_to_execute: convert string to array if needed
|
|
415
|
+
if (parsed && typeof parsed === 'object' && 'actions_to_execute' in parsed) {
|
|
416
|
+
if (typeof parsed.actions_to_execute === 'string') {
|
|
417
|
+
parsed.actions_to_execute = [parsed.actions_to_execute];
|
|
418
|
+
logger.debug('Normalized actions_to_execute from string to array', { original: parsed.actions_to_execute[0] });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// 5. Validate with Zod
|
|
292
423
|
return schema.parse(parsed);
|
|
293
|
-
} catch (e) {
|
|
424
|
+
} catch (e: any) {
|
|
425
|
+
logger.error('JSON Robust Parsing failed', { error: e.message, input: input.substring(0, 200) });
|
|
426
|
+
if (eventLogger && emailId) {
|
|
427
|
+
eventLogger.error('JSON Parse Error', {
|
|
428
|
+
error: e.message,
|
|
429
|
+
raw_input_preview: input.substring(0, 500)
|
|
430
|
+
}, emailId).catch(() => { });
|
|
431
|
+
}
|
|
294
432
|
return null;
|
|
295
433
|
}
|
|
296
434
|
}
|
|
@@ -403,6 +403,105 @@ export class MicrosoftService {
|
|
|
403
403
|
return draft.id;
|
|
404
404
|
}
|
|
405
405
|
|
|
406
|
+
/**
|
|
407
|
+
* Get or create a folder by path (supports nested folders like "Finance/Receipts")
|
|
408
|
+
* Returns the folder ID
|
|
409
|
+
*/
|
|
410
|
+
async getOrCreateFolder(account: EmailAccount, folderPath: string): Promise<string> {
|
|
411
|
+
const accessToken = account.access_token || '';
|
|
412
|
+
|
|
413
|
+
// Split path into parts
|
|
414
|
+
const parts = folderPath.split('/');
|
|
415
|
+
let parentFolderId: string | null = null;
|
|
416
|
+
|
|
417
|
+
for (const folderName of parts) {
|
|
418
|
+
// List folders (either root or under parent)
|
|
419
|
+
const listUrl: string = parentFolderId
|
|
420
|
+
? `https://graph.microsoft.com/v1.0/me/mailFolders/${parentFolderId}/childFolders`
|
|
421
|
+
: 'https://graph.microsoft.com/v1.0/me/mailFolders';
|
|
422
|
+
|
|
423
|
+
const listResponse: Response = await fetch(listUrl, {
|
|
424
|
+
headers: {
|
|
425
|
+
Authorization: `Bearer ${accessToken}`,
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
if (!listResponse.ok) {
|
|
430
|
+
throw new Error(`Failed to list folders: ${listResponse.statusText}`);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const foldersData: any = await listResponse.json();
|
|
434
|
+
const folders: any[] = foldersData.value || [];
|
|
435
|
+
|
|
436
|
+
// Check if folder exists at this level
|
|
437
|
+
const existingFolder: any = folders.find((f: any) => f.displayName === folderName);
|
|
438
|
+
if (existingFolder) {
|
|
439
|
+
parentFolderId = existingFolder.id;
|
|
440
|
+
logger.debug('Folder exists', { folderName, folderId: existingFolder.id });
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Create folder at this level
|
|
445
|
+
const createUrl = parentFolderId
|
|
446
|
+
? `https://graph.microsoft.com/v1.0/me/mailFolders/${parentFolderId}/childFolders`
|
|
447
|
+
: 'https://graph.microsoft.com/v1.0/me/mailFolders';
|
|
448
|
+
|
|
449
|
+
const createResponse = await fetch(createUrl, {
|
|
450
|
+
method: 'POST',
|
|
451
|
+
headers: {
|
|
452
|
+
Authorization: `Bearer ${accessToken}`,
|
|
453
|
+
'Content-Type': 'application/json',
|
|
454
|
+
},
|
|
455
|
+
body: JSON.stringify({
|
|
456
|
+
displayName: folderName,
|
|
457
|
+
}),
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
if (!createResponse.ok) {
|
|
461
|
+
throw new Error(`Failed to create folder: ${folderName}`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const newFolder = await createResponse.json();
|
|
465
|
+
parentFolderId = newFolder.id;
|
|
466
|
+
logger.info('Created folder', { folderName, folderId: newFolder.id });
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (!parentFolderId) {
|
|
470
|
+
throw new Error(`Failed to get or create folder: ${folderPath}`);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return parentFolderId;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Move a message to a folder by folder path (creates folder if needed)
|
|
478
|
+
* Supports nested folders like "Finance/Receipts"
|
|
479
|
+
*/
|
|
480
|
+
async moveToFolderByPath(account: EmailAccount, messageId: string, folderPath: string): Promise<void> {
|
|
481
|
+
const folderId = await this.getOrCreateFolder(account, folderPath);
|
|
482
|
+
const accessToken = account.access_token || '';
|
|
483
|
+
|
|
484
|
+
const response = await fetch(
|
|
485
|
+
`https://graph.microsoft.com/v1.0/me/messages/${messageId}/move`,
|
|
486
|
+
{
|
|
487
|
+
method: 'POST',
|
|
488
|
+
headers: {
|
|
489
|
+
Authorization: `Bearer ${accessToken}`,
|
|
490
|
+
'Content-Type': 'application/json',
|
|
491
|
+
},
|
|
492
|
+
body: JSON.stringify({
|
|
493
|
+
destinationId: folderId,
|
|
494
|
+
}),
|
|
495
|
+
}
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
if (!response.ok) {
|
|
499
|
+
throw new Error(`Failed to move message to folder: ${folderPath}`);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
logger.debug('Moved message to folder', { messageId, folderPath, folderId });
|
|
503
|
+
}
|
|
504
|
+
|
|
406
505
|
async getProfile(account: EmailAccount): Promise<{ emailAddress: string; displayName: string }> {
|
|
407
506
|
const accessToken = account.access_token || '';
|
|
408
507
|
|