@realtimex/email-automator 2.11.3 → 2.12.1

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 (44) hide show
  1. package/api/src/routes/index.ts +2 -0
  2. package/api/src/routes/rulePacks.ts +204 -0
  3. package/api/src/services/RulePackService.ts +354 -0
  4. package/api/src/services/gmail.ts +68 -0
  5. package/api/src/services/microsoft.ts +99 -0
  6. package/api/src/services/processor.ts +137 -10
  7. package/api/src/services/rulePacks/developer.ts +179 -0
  8. package/api/src/services/rulePacks/executive.ts +143 -0
  9. package/api/src/services/rulePacks/index.ts +63 -0
  10. package/api/src/services/rulePacks/operations.ts +183 -0
  11. package/api/src/services/rulePacks/sales.ts +160 -0
  12. package/api/src/services/rulePacks/types.ts +116 -0
  13. package/api/src/services/rulePacks/universal.ts +83 -0
  14. package/api/src/services/supabase.ts +40 -0
  15. package/dist/api/src/routes/index.js +2 -0
  16. package/dist/api/src/routes/rulePacks.js +179 -0
  17. package/dist/api/src/services/RulePackService.js +296 -0
  18. package/dist/api/src/services/gmail.js +56 -0
  19. package/dist/api/src/services/microsoft.js +79 -0
  20. package/dist/api/src/services/processor.js +125 -10
  21. package/dist/api/src/services/rulePacks/developer.js +176 -0
  22. package/dist/api/src/services/rulePacks/executive.js +140 -0
  23. package/dist/api/src/services/rulePacks/index.js +58 -0
  24. package/dist/api/src/services/rulePacks/operations.js +180 -0
  25. package/dist/api/src/services/rulePacks/sales.js +157 -0
  26. package/dist/api/src/services/rulePacks/types.js +7 -0
  27. package/dist/api/src/services/rulePacks/universal.js +80 -0
  28. package/dist/assets/index-B5rXh3y8.css +1 -0
  29. package/dist/assets/index-Cn89wpYc.js +105 -0
  30. package/dist/index.html +2 -2
  31. package/package.json +1 -1
  32. package/supabase/migrations/20260131000000_add_rule_packs.sql +176 -0
  33. package/supabase/migrations/20260131000001_set_zero_config_defaults.sql +51 -0
  34. package/supabase/migrations/20260131095000_backfill_user_settings.sql +36 -0
  35. package/supabase/migrations/20260131100000_rule_templates_table.sql +154 -0
  36. package/supabase/migrations/20260131110000_auto_init_user_data.sql +90 -0
  37. package/supabase/migrations/20260131120000_backfill_universal_pack.sql +84 -0
  38. package/supabase/migrations/20260131130000_simplify_rules_with_categories.sql +87 -0
  39. package/supabase/migrations/20260131140000_fix_action_constraint.sql +11 -0
  40. package/supabase/migrations/20260131150000_fix_trigger_error_handling.sql +71 -0
  41. package/supabase/migrations/20260131160000_enable_intelligent_rename_by_default.sql +18 -0
  42. package/supabase/migrations/20260131170000_fix_rule_categories.sql +140 -0
  43. package/dist/assets/index-BgMWzGP9.js +0 -105
  44. package/dist/assets/index-CtDzSy0n.css +0 -1
@@ -8,6 +8,7 @@ import settingsRoutes from './settings.js';
8
8
  import emailsRoutes from './emails.js';
9
9
  import migrateRoutes from './migrate.js';
10
10
  import sdkRoutes from './sdk.js';
11
+ import rulePacksRoutes from './rulePacks.js';
11
12
 
12
13
  const router = Router();
13
14
 
@@ -20,5 +21,6 @@ router.use('/settings', settingsRoutes);
20
21
  router.use('/emails', emailsRoutes);
21
22
  router.use('/migrate', migrateRoutes);
22
23
  router.use('/sdk', sdkRoutes);
24
+ router.use('/rule-packs', rulePacksRoutes);
23
25
 
24
26
  export default router;
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Rule Pack Routes - Zero-Configuration UX
3
+ *
4
+ * Endpoints for managing rule pack installations and user role selection
5
+ */
6
+
7
+ import { Router, Request, Response } from 'express';
8
+ import { createLogger } from '../utils/logger.js';
9
+ import { RulePackService } from '../services/RulePackService.js';
10
+ import { getAllPacks, getPackById, getPacksForRole } from '../services/rulePacks/index.js';
11
+ import type { UserRole } from '../services/rulePacks/types.js';
12
+
13
+ const router = Router();
14
+ const logger = createLogger('RulePackRoutes');
15
+
16
+ /**
17
+ * GET /api/rule-packs
18
+ * List all available rule packs
19
+ */
20
+ router.get('/', async (req: Request, res: Response) => {
21
+ try {
22
+ const packs = getAllPacks();
23
+
24
+ res.json({
25
+ success: true,
26
+ packs: packs.map(pack => ({
27
+ id: pack.id,
28
+ name: pack.name,
29
+ description: pack.description,
30
+ icon: pack.icon,
31
+ ruleCount: pack.rules.length,
32
+ targetRoles: pack.target_roles
33
+ }))
34
+ });
35
+ } catch (error: any) {
36
+ logger.error('Failed to list packs', error);
37
+ res.status(500).json({ success: false, error: error.message });
38
+ }
39
+ });
40
+
41
+ /**
42
+ * GET /api/rule-packs/:packId
43
+ * Get details of a specific pack
44
+ */
45
+ router.get('/:packId', async (req: Request, res: Response) => {
46
+ try {
47
+ const packId = req.params.packId as string;
48
+ const pack = getPackById(packId);
49
+
50
+ if (!pack) {
51
+ return res.status(404).json({ success: false, error: 'Pack not found' });
52
+ }
53
+
54
+ res.json({ success: true, pack });
55
+ } catch (error: any) {
56
+ logger.error('Failed to get pack', error);
57
+ res.status(500).json({ success: false, error: error.message });
58
+ }
59
+ });
60
+
61
+ /**
62
+ * POST /api/rule-packs/install
63
+ * Install a rule pack for the authenticated user
64
+ * Body: { packId: string }
65
+ */
66
+ router.post('/install', async (req: Request, res: Response) => {
67
+ try {
68
+ const { packId } = req.body;
69
+ const supabase = (req as any).supabase;
70
+ const userId = (req as any).userId;
71
+
72
+ if (!packId) {
73
+ return res.status(400).json({ success: false, error: 'packId is required' });
74
+ }
75
+
76
+ if (!userId) {
77
+ return res.status(401).json({ success: false, error: 'Unauthorized' });
78
+ }
79
+
80
+ const service = new RulePackService(supabase);
81
+ const result = await service.installPack(userId, packId);
82
+
83
+ res.json(result);
84
+ } catch (error: any) {
85
+ logger.error('Failed to install pack', error);
86
+ res.status(500).json({ success: false, error: error.message });
87
+ }
88
+ });
89
+
90
+ /**
91
+ * POST /api/rule-packs/uninstall
92
+ * Uninstall a rule pack
93
+ * Body: { packId: string }
94
+ */
95
+ router.post('/uninstall', async (req: Request, res: Response) => {
96
+ try {
97
+ const { packId } = req.body;
98
+ const supabase = (req as any).supabase;
99
+ const userId = (req as any).userId;
100
+
101
+ if (!packId) {
102
+ return res.status(400).json({ success: false, error: 'packId is required' });
103
+ }
104
+
105
+ if (!userId) {
106
+ return res.status(401).json({ success: false, error: 'Unauthorized' });
107
+ }
108
+
109
+ const service = new RulePackService(supabase);
110
+ const result = await service.uninstallPack(userId, packId);
111
+
112
+ res.json(result);
113
+ } catch (error: any) {
114
+ logger.error('Failed to uninstall pack', error);
115
+ res.status(500).json({ success: false, error: error.message });
116
+ }
117
+ });
118
+
119
+ /**
120
+ * GET /api/rule-packs/installed
121
+ * Get user's installed packs
122
+ */
123
+ router.get('/installed', async (req: Request, res: Response) => {
124
+ try {
125
+ const supabase = (req as any).supabase;
126
+ const userId = (req as any).userId;
127
+
128
+ if (!userId) {
129
+ return res.status(401).json({ success: false, error: 'Unauthorized' });
130
+ }
131
+
132
+ const service = new RulePackService(supabase);
133
+ const installations = await service.getInstalledPacks(userId);
134
+
135
+ res.json({ success: true, installations });
136
+ } catch (error: any) {
137
+ logger.error('Failed to get installed packs', error);
138
+ res.status(500).json({ success: false, error: error.message });
139
+ }
140
+ });
141
+
142
+ /**
143
+ * POST /api/rule-packs/set-role
144
+ * Set user role and install appropriate packs
145
+ * Body: { role: UserRole }
146
+ */
147
+ router.post('/set-role', async (req: Request, res: Response) => {
148
+ try {
149
+ const { role } = req.body;
150
+ const supabase = (req as any).supabase;
151
+ const userId = (req as any).userId;
152
+
153
+ if (!role) {
154
+ return res.status(400).json({ success: false, error: 'role is required' });
155
+ }
156
+
157
+ if (!userId) {
158
+ return res.status(401).json({ success: false, error: 'Unauthorized' });
159
+ }
160
+
161
+ const validRoles: UserRole[] = ['executive', 'developer', 'sales', 'operations', 'marketing', 'other'];
162
+ if (!validRoles.includes(role as UserRole)) {
163
+ return res.status(400).json({ success: false, error: 'Invalid role' });
164
+ }
165
+
166
+ const service = new RulePackService(supabase);
167
+ const result = await service.updateUserRole(userId, role as UserRole, 'onboarding');
168
+
169
+ logger.info(`User ${userId} set role to ${role}, installed ${result.packsInstalled.length} packs`);
170
+
171
+ res.json(result);
172
+ } catch (error: any) {
173
+ logger.error('Failed to set user role', error);
174
+ res.status(500).json({ success: false, error: error.message });
175
+ }
176
+ });
177
+
178
+ /**
179
+ * GET /api/rule-packs/for-role/:role
180
+ * Get recommended packs for a specific role
181
+ */
182
+ router.get('/for-role/:role', async (req: Request, res: Response) => {
183
+ try {
184
+ const { role } = req.params;
185
+ const packs = getPacksForRole(role as UserRole);
186
+
187
+ res.json({
188
+ success: true,
189
+ role,
190
+ packs: packs.map(pack => ({
191
+ id: pack.id,
192
+ name: pack.name,
193
+ description: pack.description,
194
+ icon: pack.icon,
195
+ ruleCount: pack.rules.length
196
+ }))
197
+ });
198
+ } catch (error: any) {
199
+ logger.error('Failed to get packs for role', error);
200
+ res.status(500).json({ success: false, error: error.message });
201
+ }
202
+ });
203
+
204
+ export default router;
@@ -0,0 +1,354 @@
1
+ /**
2
+ * Rule Pack Service - Zero-Configuration Email Automation
3
+ *
4
+ * Handles installation, management, and tracking of rule packs for users.
5
+ */
6
+
7
+ import { SupabaseClient } from '@supabase/supabase-js';
8
+ import { createLogger } from '../utils/logger.js';
9
+ import { getPackById, getPacksForRole, ALL_PACKS } from './rulePacks/index.js';
10
+ import type { RulePack, RuleTemplate, UserRole } from './rulePacks/types.js';
11
+ import type { Rule, PackInstallation } from './supabase.js';
12
+
13
+ const logger = createLogger('RulePackService');
14
+
15
+ export class RulePackService {
16
+ constructor(private supabase: SupabaseClient) {}
17
+
18
+ /**
19
+ * Install a rule pack for a user
20
+ * Creates all rules from the pack and tracks installation
21
+ */
22
+ async installPack(userId: string, packId: string): Promise<{ success: boolean; rulesCreated: number; error?: string }> {
23
+ try {
24
+ const pack = getPackById(packId);
25
+ if (!pack) {
26
+ return { success: false, rulesCreated: 0, error: `Pack '${packId}' not found` };
27
+ }
28
+
29
+ logger.info(`Installing pack '${packId}' for user ${userId}`);
30
+
31
+ // Check if pack is already installed
32
+ const { data: existing } = await this.supabase
33
+ .from('pack_installations')
34
+ .select('*')
35
+ .eq('user_id', userId)
36
+ .eq('pack_id', packId)
37
+ .is('uninstalled_at', null)
38
+ .single();
39
+
40
+ if (existing) {
41
+ logger.info(`Pack '${packId}' already installed for user ${userId}`);
42
+ return { success: true, rulesCreated: 0 }; // Already installed, no-op
43
+ }
44
+
45
+ // Create all rules from the pack
46
+ let rulesCreated = 0;
47
+ for (const template of pack.rules) {
48
+ // Check if rule already exists (by rule_template_id)
49
+ const { data: existingRule } = await this.supabase
50
+ .from('rules')
51
+ .select('id')
52
+ .eq('user_id', userId)
53
+ .eq('rule_template_id', template.id)
54
+ .single();
55
+
56
+ if (existingRule) {
57
+ logger.debug(`Rule '${template.id}' already exists, skipping`);
58
+ continue;
59
+ }
60
+
61
+ // Create rule from template
62
+ const { error: ruleError } = await this.supabase
63
+ .from('rules')
64
+ .insert({
65
+ user_id: userId,
66
+ name: template.name,
67
+ description: template.description,
68
+ intent: template.intent,
69
+ priority: template.priority,
70
+ condition: template.condition as any,
71
+ actions: template.actions as any,
72
+ instructions: template.instructions,
73
+ is_enabled: template.is_enabled_by_default,
74
+ pack: packId,
75
+ rule_template_id: template.id,
76
+ is_system_managed: true
77
+ });
78
+
79
+ if (ruleError) {
80
+ logger.error(`Failed to create rule '${template.id}'`, ruleError);
81
+ continue; // Continue with other rules even if one fails
82
+ }
83
+
84
+ rulesCreated++;
85
+ }
86
+
87
+ // Track pack installation
88
+ const { error: installError } = await this.supabase
89
+ .from('pack_installations')
90
+ .insert({
91
+ user_id: userId,
92
+ pack_id: packId,
93
+ source: 'manual'
94
+ });
95
+
96
+ if (installError) {
97
+ logger.error(`Failed to track pack installation`, installError);
98
+ // Don't fail the whole operation if tracking fails
99
+ }
100
+
101
+ logger.info(`Successfully installed pack '${packId}' with ${rulesCreated} rules`);
102
+ return { success: true, rulesCreated };
103
+
104
+ } catch (error: any) {
105
+ logger.error(`Error installing pack '${packId}'`, error);
106
+ return { success: false, rulesCreated: 0, error: error.message };
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Uninstall a rule pack for a user
112
+ * Disables all rules from the pack and marks it as uninstalled
113
+ */
114
+ async uninstallPack(userId: string, packId: string): Promise<{ success: boolean; error?: string }> {
115
+ try {
116
+ logger.info(`Uninstalling pack '${packId}' for user ${userId}`);
117
+
118
+ // Disable all rules from this pack
119
+ const { error: disableError } = await this.supabase
120
+ .from('rules')
121
+ .update({ is_enabled: false })
122
+ .eq('user_id', userId)
123
+ .eq('pack', packId);
124
+
125
+ if (disableError) {
126
+ logger.error(`Failed to disable pack rules`, disableError);
127
+ return { success: false, error: disableError.message };
128
+ }
129
+
130
+ // Mark pack as uninstalled
131
+ const { error: uninstallError } = await this.supabase
132
+ .from('pack_installations')
133
+ .update({ uninstalled_at: new Date().toISOString() })
134
+ .eq('user_id', userId)
135
+ .eq('pack_id', packId)
136
+ .is('uninstalled_at', null);
137
+
138
+ if (uninstallError) {
139
+ logger.error(`Failed to mark pack as uninstalled`, uninstallError);
140
+ return { success: false, error: uninstallError.message };
141
+ }
142
+
143
+ logger.info(`Successfully uninstalled pack '${packId}'`);
144
+ return { success: true };
145
+
146
+ } catch (error: any) {
147
+ logger.error(`Error uninstalling pack '${packId}'`, error);
148
+ return { success: false, error: error.message };
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Get all installed packs for a user
154
+ */
155
+ async getInstalledPacks(userId: string): Promise<PackInstallation[]> {
156
+ const { data, error } = await this.supabase
157
+ .from('pack_installations')
158
+ .select('*')
159
+ .eq('user_id', userId)
160
+ .is('uninstalled_at', null)
161
+ .order('installed_at', { ascending: false });
162
+
163
+ if (error) {
164
+ logger.error('Failed to fetch installed packs', error);
165
+ return [];
166
+ }
167
+
168
+ return data || [];
169
+ }
170
+
171
+ /**
172
+ * Update user role and install appropriate packs
173
+ * This is the main entry point for onboarding
174
+ */
175
+ async updateUserRole(
176
+ userId: string,
177
+ role: UserRole,
178
+ source: 'onboarding' | 'manual' = 'onboarding'
179
+ ): Promise<{ success: boolean; packsInstalled: string[]; error?: string }> {
180
+ try {
181
+ logger.info(`Updating user role to '${role}' for user ${userId}`);
182
+
183
+ // 1. Update user settings
184
+ const { error: settingsError } = await this.supabase
185
+ .from('user_settings')
186
+ .update({
187
+ user_role: role,
188
+ onboarding_completed: true
189
+ })
190
+ .eq('user_id', userId);
191
+
192
+ if (settingsError) {
193
+ logger.error('Failed to update user settings', settingsError);
194
+ return { success: false, packsInstalled: [], error: settingsError.message };
195
+ }
196
+
197
+ // 2. Get packs for this role
198
+ const packs = getPacksForRole(role);
199
+ const packsInstalled: string[] = [];
200
+
201
+ // 3. Install each pack
202
+ for (const pack of packs) {
203
+ const result = await this.installPackWithSource(userId, pack.id, source);
204
+ if (result.success) {
205
+ packsInstalled.push(pack.id);
206
+ }
207
+ }
208
+
209
+ logger.info(`Successfully set up ${packsInstalled.length} packs for role '${role}'`);
210
+ return { success: true, packsInstalled };
211
+
212
+ } catch (error: any) {
213
+ logger.error(`Error updating user role`, error);
214
+ return { success: false, packsInstalled: [], error: error.message };
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Install a pack with a specific source tracking
220
+ */
221
+ private async installPackWithSource(
222
+ userId: string,
223
+ packId: string,
224
+ source: 'onboarding' | 'manual' | 'auto'
225
+ ): Promise<{ success: boolean; rulesCreated: number; error?: string }> {
226
+ const pack = getPackById(packId);
227
+ if (!pack) {
228
+ return { success: false, rulesCreated: 0, error: `Pack '${packId}' not found` };
229
+ }
230
+
231
+ // Check if already installed
232
+ const { data: existing } = await this.supabase
233
+ .from('pack_installations')
234
+ .select('*')
235
+ .eq('user_id', userId)
236
+ .eq('pack_id', packId)
237
+ .is('uninstalled_at', null)
238
+ .single();
239
+
240
+ if (existing) {
241
+ return { success: true, rulesCreated: 0 }; // Already installed
242
+ }
243
+
244
+ // Create rules
245
+ let rulesCreated = 0;
246
+ for (const template of pack.rules) {
247
+ const { data: existingRule } = await this.supabase
248
+ .from('rules')
249
+ .select('id')
250
+ .eq('user_id', userId)
251
+ .eq('rule_template_id', template.id)
252
+ .single();
253
+
254
+ if (existingRule) continue;
255
+
256
+ const { error } = await this.supabase
257
+ .from('rules')
258
+ .insert({
259
+ user_id: userId,
260
+ name: template.name,
261
+ description: template.description,
262
+ intent: template.intent,
263
+ priority: template.priority,
264
+ condition: template.condition as any,
265
+ actions: template.actions as any,
266
+ instructions: template.instructions,
267
+ is_enabled: template.is_enabled_by_default,
268
+ pack: packId,
269
+ rule_template_id: template.id,
270
+ is_system_managed: true
271
+ });
272
+
273
+ if (!error) rulesCreated++;
274
+ }
275
+
276
+ // Track installation
277
+ await this.supabase
278
+ .from('pack_installations')
279
+ .insert({
280
+ user_id: userId,
281
+ pack_id: packId,
282
+ source
283
+ });
284
+
285
+ return { success: true, rulesCreated };
286
+ }
287
+
288
+ /**
289
+ * Get pack metrics (how many users have it installed, etc.)
290
+ */
291
+ async getPackMetrics(packId: string): Promise<{
292
+ totalInstallations: number;
293
+ activeInstallations: number;
294
+ uninstallations: number;
295
+ }> {
296
+ const { data: all } = await this.supabase
297
+ .from('pack_installations')
298
+ .select('*')
299
+ .eq('pack_id', packId);
300
+
301
+ const total = all?.length || 0;
302
+ const active = all?.filter(p => !p.uninstalled_at).length || 0;
303
+ const uninstalled = all?.filter(p => p.uninstalled_at).length || 0;
304
+
305
+ return {
306
+ totalInstallations: total,
307
+ activeInstallations: active,
308
+ uninstallations: uninstalled
309
+ };
310
+ }
311
+
312
+ /**
313
+ * Auto-install universal pack for new users
314
+ * Called during signup or first sync
315
+ */
316
+ async ensureUniversalPack(userId: string): Promise<{ success: boolean; installed: boolean }> {
317
+ try {
318
+ // Check if user already has universal pack
319
+ const { data: existing } = await this.supabase
320
+ .from('pack_installations')
321
+ .select('*')
322
+ .eq('user_id', userId)
323
+ .eq('pack_id', 'universal')
324
+ .is('uninstalled_at', null)
325
+ .single();
326
+
327
+ if (existing) {
328
+ return { success: true, installed: false }; // Already has it
329
+ }
330
+
331
+ // Install universal pack
332
+ const result = await this.installPackWithSource(userId, 'universal', 'auto');
333
+
334
+ logger.info(`Auto-installed Universal Pack for new user ${userId}`);
335
+ return { success: result.success, installed: true };
336
+
337
+ } catch (error: any) {
338
+ logger.error('Failed to ensure universal pack', error);
339
+ return { success: false, installed: false };
340
+ }
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Get singleton instance (convenience)
346
+ */
347
+ let defaultInstance: RulePackService | null = null;
348
+
349
+ export function getRulePackService(supabase: SupabaseClient): RulePackService {
350
+ if (!defaultInstance) {
351
+ defaultInstance = new RulePackService(supabase);
352
+ }
353
+ return defaultInstance;
354
+ }
@@ -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')