@realtimex/email-automator 2.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.
- package/.env.example +35 -0
- package/LICENSE +21 -0
- package/README.md +247 -0
- package/api/server.ts +130 -0
- package/api/src/config/index.ts +102 -0
- package/api/src/middleware/auth.ts +166 -0
- package/api/src/middleware/errorHandler.ts +97 -0
- package/api/src/middleware/index.ts +4 -0
- package/api/src/middleware/rateLimit.ts +87 -0
- package/api/src/middleware/validation.ts +118 -0
- package/api/src/routes/actions.ts +214 -0
- package/api/src/routes/auth.ts +157 -0
- package/api/src/routes/emails.ts +144 -0
- package/api/src/routes/health.ts +36 -0
- package/api/src/routes/index.ts +22 -0
- package/api/src/routes/migrate.ts +76 -0
- package/api/src/routes/rules.ts +149 -0
- package/api/src/routes/settings.ts +229 -0
- package/api/src/routes/sync.ts +152 -0
- package/api/src/services/eventLogger.ts +52 -0
- package/api/src/services/gmail.ts +456 -0
- package/api/src/services/intelligence.ts +288 -0
- package/api/src/services/microsoft.ts +368 -0
- package/api/src/services/processor.ts +596 -0
- package/api/src/services/scheduler.ts +255 -0
- package/api/src/services/supabase.ts +144 -0
- package/api/src/utils/contentCleaner.ts +114 -0
- package/api/src/utils/crypto.ts +80 -0
- package/api/src/utils/logger.ts +142 -0
- package/bin/email-automator-deploy.js +79 -0
- package/bin/email-automator-setup.js +144 -0
- package/bin/email-automator.js +61 -0
- package/dist/assets/index-BQ1uMdFh.js +97 -0
- package/dist/assets/index-Dzi17fx5.css +1 -0
- package/dist/email-automator-logo.svg +51 -0
- package/dist/favicon.svg +45 -0
- package/dist/index.html +14 -0
- package/index.html +13 -0
- package/package.json +112 -0
- package/public/email-automator-logo.svg +51 -0
- package/public/favicon.svg +45 -0
- package/scripts/deploy-functions.sh +55 -0
- package/scripts/migrate.sh +177 -0
- package/src/App.tsx +622 -0
- package/src/components/AccountSettings.tsx +310 -0
- package/src/components/AccountSettingsPage.tsx +390 -0
- package/src/components/Configuration.tsx +1345 -0
- package/src/components/Dashboard.tsx +940 -0
- package/src/components/ErrorBoundary.tsx +71 -0
- package/src/components/LiveTerminal.tsx +308 -0
- package/src/components/LoadingSpinner.tsx +39 -0
- package/src/components/Login.tsx +371 -0
- package/src/components/Logo.tsx +57 -0
- package/src/components/SetupWizard.tsx +388 -0
- package/src/components/Toast.tsx +109 -0
- package/src/components/migration/MigrationBanner.tsx +97 -0
- package/src/components/migration/MigrationModal.tsx +458 -0
- package/src/components/migration/MigrationPulseIndicator.tsx +38 -0
- package/src/components/mode-toggle.tsx +24 -0
- package/src/components/theme-provider.tsx +72 -0
- package/src/components/ui/alert.tsx +66 -0
- package/src/components/ui/button.tsx +57 -0
- package/src/components/ui/card.tsx +75 -0
- package/src/components/ui/dialog.tsx +133 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/otp-input.tsx +184 -0
- package/src/context/AppContext.tsx +422 -0
- package/src/context/MigrationContext.tsx +53 -0
- package/src/context/TerminalContext.tsx +31 -0
- package/src/core/actions.ts +76 -0
- package/src/core/auth.ts +108 -0
- package/src/core/intelligence.ts +76 -0
- package/src/core/processor.ts +112 -0
- package/src/hooks/useRealtimeEmails.ts +111 -0
- package/src/index.css +140 -0
- package/src/lib/api-config.ts +42 -0
- package/src/lib/api-old.ts +228 -0
- package/src/lib/api.ts +421 -0
- package/src/lib/migration-check.ts +264 -0
- package/src/lib/sounds.ts +120 -0
- package/src/lib/supabase-config.ts +117 -0
- package/src/lib/supabase.ts +28 -0
- package/src/lib/types.ts +166 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +10 -0
- package/supabase/.env.example +15 -0
- package/supabase/.temp/cli-latest +1 -0
- package/supabase/.temp/gotrue-version +1 -0
- package/supabase/.temp/pooler-url +1 -0
- package/supabase/.temp/postgres-version +1 -0
- package/supabase/.temp/project-ref +1 -0
- package/supabase/.temp/rest-version +1 -0
- package/supabase/.temp/storage-migration +1 -0
- package/supabase/.temp/storage-version +1 -0
- package/supabase/config.toml +95 -0
- package/supabase/functions/_shared/auth-helper.ts +76 -0
- package/supabase/functions/_shared/auth.ts +33 -0
- package/supabase/functions/_shared/cors.ts +45 -0
- package/supabase/functions/_shared/encryption.ts +70 -0
- package/supabase/functions/_shared/supabaseAdmin.ts +14 -0
- package/supabase/functions/api-v1-accounts/index.ts +133 -0
- package/supabase/functions/api-v1-emails/index.ts +177 -0
- package/supabase/functions/api-v1-rules/index.ts +177 -0
- package/supabase/functions/api-v1-settings/index.ts +247 -0
- package/supabase/functions/auth-gmail/index.ts +197 -0
- package/supabase/functions/auth-microsoft/index.ts +215 -0
- package/supabase/functions/setup/index.ts +92 -0
- package/supabase/migrations/20260114000000_initial_schema.sql +81 -0
- package/supabase/migrations/20260115000000_add_user_settings.sql +49 -0
- package/supabase/migrations/20260115000001_add_auth_flow.sql +80 -0
- package/supabase/migrations/20260115000002_fix_permissions.sql +5 -0
- package/supabase/migrations/20260115000003_fix_init_state_permissions.sql +9 -0
- package/supabase/migrations/20260115000004_add_migration_rpc.sql +13 -0
- package/supabase/migrations/20260115000005_add_provider_creds.sql +7 -0
- package/supabase/migrations/20260115000006_backfill_profiles.sql +22 -0
- package/supabase/migrations/20260116000000_add_sync_scope.sql +15 -0
- package/supabase/migrations/20260116000001_per_account_sync_scope.sql +19 -0
- package/supabase/migrations/20260116000002_add_llm_api_key.sql +5 -0
- package/supabase/migrations/20260117000000_refactor_integrations.sql +36 -0
- package/supabase/migrations/20260117000001_add_processing_events.sql +30 -0
- package/supabase/migrations/20260117000002_multi_actions.sql +15 -0
- package/supabase/migrations/20260117000003_seed_default_rules.sql +77 -0
- package/supabase/migrations/20260117000004_rule_instructions.sql +5 -0
- package/supabase/migrations/20260117000005_rule_attachments.sql +7 -0
- package/supabase/migrations/20260117000006_setup_storage.sql +32 -0
- package/supabase/migrations/20260117000007_add_system_logs.sql +26 -0
- package/supabase/migrations/20260117000008_link_logs_to_accounts.sql +8 -0
- package/supabase/migrations/20260117000009_convert_toggles_to_rules.sql +28 -0
- package/supabase/migrations/20260117000010_add_atomic_action_append.sql +13 -0
- package/supabase/migrations/20260117000011_add_profile_avatar.sql +4 -0
- package/supabase/migrations/20260117000012_setup_avatars_storage.sql +26 -0
- package/supabase/templates/confirmation.html +76 -0
- package/supabase/templates/email-change.html +76 -0
- package/supabase/templates/invite.html +72 -0
- package/supabase/templates/magic-link.html +68 -0
- package/supabase/templates/recovery.html +82 -0
- package/tsconfig.json +36 -0
- package/vite.config.ts +162 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
+
import { config } from '../config/index.js';
|
|
3
|
+
import { createLogger } from '../utils/logger.js';
|
|
4
|
+
import { EmailProcessorService } from './processor.js';
|
|
5
|
+
import { getServerSupabase } from './supabase.js';
|
|
6
|
+
|
|
7
|
+
const logger = createLogger('Scheduler');
|
|
8
|
+
|
|
9
|
+
interface ScheduledJob {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
interval: number;
|
|
13
|
+
lastRun: Date | null;
|
|
14
|
+
isRunning: boolean;
|
|
15
|
+
timer: NodeJS.Timeout | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class SyncScheduler {
|
|
19
|
+
private jobs: Map<string, ScheduledJob> = new Map();
|
|
20
|
+
private supabase: SupabaseClient | null = null;
|
|
21
|
+
|
|
22
|
+
constructor() {
|
|
23
|
+
this.supabase = getServerSupabase();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async start(): Promise<void> {
|
|
27
|
+
if (!this.supabase) {
|
|
28
|
+
logger.warn('Supabase not configured, scheduler disabled');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
logger.info('Starting sync scheduler');
|
|
33
|
+
|
|
34
|
+
// Schedule periodic sync for all active accounts
|
|
35
|
+
this.scheduleGlobalSync();
|
|
36
|
+
|
|
37
|
+
// Schedule cleanup job
|
|
38
|
+
this.scheduleCleanup();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
stop(): void {
|
|
42
|
+
logger.info('Stopping sync scheduler');
|
|
43
|
+
for (const job of this.jobs.values()) {
|
|
44
|
+
if (job.timer) {
|
|
45
|
+
clearInterval(job.timer);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
this.jobs.clear();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private scheduleGlobalSync(): void {
|
|
52
|
+
const jobId = 'global-sync';
|
|
53
|
+
const interval = config.processing.syncIntervalMs;
|
|
54
|
+
|
|
55
|
+
const job: ScheduledJob = {
|
|
56
|
+
id: jobId,
|
|
57
|
+
name: 'Global Email Sync',
|
|
58
|
+
interval,
|
|
59
|
+
lastRun: null,
|
|
60
|
+
isRunning: false,
|
|
61
|
+
timer: null,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
job.timer = setInterval(async () => {
|
|
65
|
+
if (job.isRunning) {
|
|
66
|
+
logger.debug('Global sync already running, skipping');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
job.isRunning = true;
|
|
71
|
+
try {
|
|
72
|
+
await this.runGlobalSync();
|
|
73
|
+
job.lastRun = new Date();
|
|
74
|
+
} catch (error) {
|
|
75
|
+
logger.error('Global sync failed', error);
|
|
76
|
+
} finally {
|
|
77
|
+
job.isRunning = false;
|
|
78
|
+
}
|
|
79
|
+
}, interval);
|
|
80
|
+
|
|
81
|
+
this.jobs.set(jobId, job);
|
|
82
|
+
logger.info(`Scheduled global sync every ${interval / 1000}s`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private async runGlobalSync(): Promise<void> {
|
|
86
|
+
if (!this.supabase) return;
|
|
87
|
+
|
|
88
|
+
// Get all active accounts with their user settings
|
|
89
|
+
const { data: accounts, error } = await this.supabase
|
|
90
|
+
.from('email_accounts')
|
|
91
|
+
.select(`
|
|
92
|
+
id,
|
|
93
|
+
user_id,
|
|
94
|
+
provider,
|
|
95
|
+
is_active
|
|
96
|
+
`)
|
|
97
|
+
.eq('is_active', true);
|
|
98
|
+
|
|
99
|
+
if (error) {
|
|
100
|
+
logger.error('Failed to fetch accounts for sync', error);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!accounts || accounts.length === 0) {
|
|
105
|
+
logger.debug('No active accounts to sync');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
logger.info(`Running global sync for ${accounts.length} accounts`);
|
|
110
|
+
|
|
111
|
+
// Group by user to check their sync interval settings
|
|
112
|
+
const userAccounts = new Map<string, typeof accounts>();
|
|
113
|
+
for (const account of accounts) {
|
|
114
|
+
const existing = userAccounts.get(account.user_id) || [];
|
|
115
|
+
existing.push(account);
|
|
116
|
+
userAccounts.set(account.user_id, existing);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Process each user's accounts
|
|
120
|
+
for (const [userId, userAccountList] of userAccounts) {
|
|
121
|
+
// Check user's sync interval preference
|
|
122
|
+
const { data: settings } = await this.supabase
|
|
123
|
+
.from('user_settings')
|
|
124
|
+
.select('sync_interval_minutes')
|
|
125
|
+
.eq('user_id', userId)
|
|
126
|
+
.single();
|
|
127
|
+
|
|
128
|
+
const syncIntervalMs = (settings?.sync_interval_minutes || 5) * 60 * 1000;
|
|
129
|
+
|
|
130
|
+
// Check last sync time
|
|
131
|
+
const { data: lastLog } = await this.supabase
|
|
132
|
+
.from('processing_logs')
|
|
133
|
+
.select('started_at')
|
|
134
|
+
.eq('user_id', userId)
|
|
135
|
+
.eq('status', 'success')
|
|
136
|
+
.order('started_at', { ascending: false })
|
|
137
|
+
.limit(1)
|
|
138
|
+
.single();
|
|
139
|
+
|
|
140
|
+
if (lastLog) {
|
|
141
|
+
const lastSyncTime = new Date(lastLog.started_at).getTime();
|
|
142
|
+
const now = Date.now();
|
|
143
|
+
if (now - lastSyncTime < syncIntervalMs) {
|
|
144
|
+
logger.debug(`Skipping sync for user ${userId}, last sync was recent`);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Sync each account
|
|
150
|
+
const processor = new EmailProcessorService(this.supabase);
|
|
151
|
+
for (const account of userAccountList) {
|
|
152
|
+
try {
|
|
153
|
+
await processor.syncAccount(account.id, userId);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
logger.error('Account sync failed', error, { accountId: account.id });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private scheduleCleanup(): void {
|
|
162
|
+
const jobId = 'cleanup';
|
|
163
|
+
const interval = 24 * 60 * 60 * 1000; // Daily
|
|
164
|
+
|
|
165
|
+
const job: ScheduledJob = {
|
|
166
|
+
id: jobId,
|
|
167
|
+
name: 'Data Cleanup',
|
|
168
|
+
interval,
|
|
169
|
+
lastRun: null,
|
|
170
|
+
isRunning: false,
|
|
171
|
+
timer: null,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
job.timer = setInterval(async () => {
|
|
175
|
+
if (job.isRunning) return;
|
|
176
|
+
|
|
177
|
+
job.isRunning = true;
|
|
178
|
+
try {
|
|
179
|
+
await this.runCleanup();
|
|
180
|
+
job.lastRun = new Date();
|
|
181
|
+
} catch (error) {
|
|
182
|
+
logger.error('Cleanup failed', error);
|
|
183
|
+
} finally {
|
|
184
|
+
job.isRunning = false;
|
|
185
|
+
}
|
|
186
|
+
}, interval);
|
|
187
|
+
|
|
188
|
+
this.jobs.set(jobId, job);
|
|
189
|
+
logger.info('Scheduled daily cleanup');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private async runCleanup(): Promise<void> {
|
|
193
|
+
if (!this.supabase) return;
|
|
194
|
+
|
|
195
|
+
logger.info('Running cleanup job');
|
|
196
|
+
|
|
197
|
+
// Delete old processing logs (older than 30 days)
|
|
198
|
+
const thirtyDaysAgo = new Date();
|
|
199
|
+
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
200
|
+
|
|
201
|
+
const { error: logsError } = await this.supabase
|
|
202
|
+
.from('processing_logs')
|
|
203
|
+
.delete()
|
|
204
|
+
.lt('started_at', thirtyDaysAgo.toISOString());
|
|
205
|
+
|
|
206
|
+
if (logsError) {
|
|
207
|
+
logger.error('Failed to cleanup old logs', logsError);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Delete emails that were trashed more than 7 days ago
|
|
211
|
+
const sevenDaysAgo = new Date();
|
|
212
|
+
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
213
|
+
|
|
214
|
+
const { error: emailsError } = await this.supabase
|
|
215
|
+
.from('emails')
|
|
216
|
+
.delete()
|
|
217
|
+
.eq('action_taken', 'delete')
|
|
218
|
+
.lt('created_at', sevenDaysAgo.toISOString());
|
|
219
|
+
|
|
220
|
+
if (emailsError) {
|
|
221
|
+
logger.error('Failed to cleanup old emails', emailsError);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
logger.info('Cleanup completed');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
getJobStatus(): Array<{ id: string; name: string; lastRun: Date | null; isRunning: boolean }> {
|
|
228
|
+
return Array.from(this.jobs.values()).map(job => ({
|
|
229
|
+
id: job.id,
|
|
230
|
+
name: job.name,
|
|
231
|
+
lastRun: job.lastRun,
|
|
232
|
+
isRunning: job.isRunning,
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Singleton
|
|
238
|
+
let schedulerInstance: SyncScheduler | null = null;
|
|
239
|
+
|
|
240
|
+
export function getScheduler(): SyncScheduler {
|
|
241
|
+
if (!schedulerInstance) {
|
|
242
|
+
schedulerInstance = new SyncScheduler();
|
|
243
|
+
}
|
|
244
|
+
return schedulerInstance;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function startScheduler(): void {
|
|
248
|
+
getScheduler().start();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function stopScheduler(): void {
|
|
252
|
+
if (schedulerInstance) {
|
|
253
|
+
schedulerInstance.stop();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { createClient, SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
+
import { config } from '../config/index.js';
|
|
3
|
+
import { createLogger } from '../utils/logger.js';
|
|
4
|
+
|
|
5
|
+
const logger = createLogger('SupabaseService');
|
|
6
|
+
|
|
7
|
+
let serverClient: SupabaseClient | null = null;
|
|
8
|
+
|
|
9
|
+
export function isValidUrl(url: string): boolean {
|
|
10
|
+
try {
|
|
11
|
+
return url.startsWith('http://') || url.startsWith('https://');
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getServerSupabase(): SupabaseClient | null {
|
|
18
|
+
if (serverClient) return serverClient;
|
|
19
|
+
|
|
20
|
+
const url = config.supabase.url;
|
|
21
|
+
const key = config.supabase.anonKey;
|
|
22
|
+
|
|
23
|
+
if (!url || !key || !isValidUrl(url)) {
|
|
24
|
+
logger.warn('Supabase not configured or invalid URL - skipping client initialization', {
|
|
25
|
+
url: url || 'missing'
|
|
26
|
+
});
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
serverClient = createClient(url, key, {
|
|
32
|
+
auth: {
|
|
33
|
+
autoRefreshToken: false,
|
|
34
|
+
persistSession: false,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
logger.info('Server Supabase client initialized');
|
|
39
|
+
return serverClient;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
logger.error('Failed to initialize Supabase client', error);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getServiceRoleSupabase(): SupabaseClient | null {
|
|
47
|
+
const url = config.supabase.url;
|
|
48
|
+
const key = config.supabase.serviceRoleKey;
|
|
49
|
+
|
|
50
|
+
if (!url || !key || !isValidUrl(url)) {
|
|
51
|
+
logger.warn('Service role Supabase not configured or invalid URL');
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
return createClient(url, key, {
|
|
57
|
+
auth: {
|
|
58
|
+
autoRefreshToken: false,
|
|
59
|
+
persistSession: false,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
} catch (error) {
|
|
63
|
+
logger.error('Failed to initialize Service Role Supabase client', error);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Database types (expand as needed)
|
|
69
|
+
export interface EmailAccount {
|
|
70
|
+
id: string;
|
|
71
|
+
user_id: string;
|
|
72
|
+
provider: 'gmail' | 'outlook';
|
|
73
|
+
email_address: string;
|
|
74
|
+
access_token: string | null;
|
|
75
|
+
refresh_token: string | null;
|
|
76
|
+
token_expires_at: string | null;
|
|
77
|
+
scopes: string[];
|
|
78
|
+
is_active: boolean;
|
|
79
|
+
last_sync_checkpoint?: string | null;
|
|
80
|
+
sync_start_date?: string | null;
|
|
81
|
+
sync_max_emails_per_run?: number;
|
|
82
|
+
last_sync_at?: string | null;
|
|
83
|
+
last_sync_status?: 'idle' | 'syncing' | 'success' | 'error';
|
|
84
|
+
last_sync_error?: string | null;
|
|
85
|
+
created_at: string;
|
|
86
|
+
updated_at: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface Email {
|
|
90
|
+
id: string;
|
|
91
|
+
account_id: string;
|
|
92
|
+
external_id: string;
|
|
93
|
+
subject: string | null;
|
|
94
|
+
sender: string | null;
|
|
95
|
+
recipient: string | null;
|
|
96
|
+
date: string | null;
|
|
97
|
+
body_snippet: string | null;
|
|
98
|
+
category: string | null;
|
|
99
|
+
is_useless: boolean;
|
|
100
|
+
ai_analysis: Record<string, unknown> | null;
|
|
101
|
+
suggested_action: string | null; // Deprecated
|
|
102
|
+
suggested_actions?: string[];
|
|
103
|
+
action_taken: string | null; // Deprecated
|
|
104
|
+
actions_taken?: string[];
|
|
105
|
+
created_at: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface Rule {
|
|
109
|
+
id: string;
|
|
110
|
+
user_id: string;
|
|
111
|
+
name: string;
|
|
112
|
+
condition: Record<string, unknown>;
|
|
113
|
+
action: 'delete' | 'archive' | 'draft' | 'star' | 'read';
|
|
114
|
+
instructions?: string;
|
|
115
|
+
attachments?: any[];
|
|
116
|
+
is_enabled: boolean;
|
|
117
|
+
created_at: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface ProcessingLog {
|
|
121
|
+
id: string;
|
|
122
|
+
user_id: string;
|
|
123
|
+
account_id: string | null;
|
|
124
|
+
status: 'running' | 'success' | 'failed';
|
|
125
|
+
started_at: string;
|
|
126
|
+
completed_at: string | null;
|
|
127
|
+
emails_processed: number;
|
|
128
|
+
emails_deleted: number;
|
|
129
|
+
emails_drafted: number;
|
|
130
|
+
error_message: string | null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface UserSettings {
|
|
134
|
+
id: string;
|
|
135
|
+
user_id: string;
|
|
136
|
+
llm_model: string | null;
|
|
137
|
+
llm_base_url: string | null;
|
|
138
|
+
llm_api_key: string | null;
|
|
139
|
+
auto_trash_spam: boolean;
|
|
140
|
+
smart_drafts: boolean;
|
|
141
|
+
sync_interval_minutes: number;
|
|
142
|
+
created_at: string;
|
|
143
|
+
updated_at: string;
|
|
144
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export class ContentCleaner {
|
|
2
|
+
/**
|
|
3
|
+
* Cleans email body by removing noise, quoted replies, and footers.
|
|
4
|
+
* Ported from Python ContentCleaner.
|
|
5
|
+
*/
|
|
6
|
+
static cleanEmailBody(text: string): string {
|
|
7
|
+
if (!text) return "";
|
|
8
|
+
|
|
9
|
+
// 0. Lightweight HTML -> Markdown Conversion
|
|
10
|
+
|
|
11
|
+
// Structure: <br>, <p> -> Newlines
|
|
12
|
+
text = text.replace(/<br\s*\/?\?>/gi, '\n');
|
|
13
|
+
text = text.replace(/<\/p>/gi, '\n\n');
|
|
14
|
+
text = text.replace(/<p.*?>/gi, ''); // Open p tags just gone
|
|
15
|
+
|
|
16
|
+
// Structure: Headers <h1>-<h6> -> # Title
|
|
17
|
+
text = text.replace(/<h[1-6].*?>(.*?)<\/h[1-6]>/gsi, (match, p1) => `\n# ${p1}\n`);
|
|
18
|
+
|
|
19
|
+
// Structure: Lists <li> -> - Item
|
|
20
|
+
text = text.replace(/<li.*?>(.*?)<\/li>/gsi, (match, p1) => `\n- ${p1}`);
|
|
21
|
+
text = text.replace(/<ul.*?>/gi, '');
|
|
22
|
+
text = text.replace(/<\/ul>/gi, '\n');
|
|
23
|
+
|
|
24
|
+
// Links: <a href=\"...\">text</a> -> [text](href)
|
|
25
|
+
text = text.replace(/<a\s+(?:[^>]*?\s+)?href=\"([^\"]*)\"[^>]*>(.*?)<\/a>/gsi, (match, href, content) => `[${content}](${href})`);
|
|
26
|
+
|
|
27
|
+
// Images: <img src=\"...\" alt=\"...\"> -> 
|
|
28
|
+
text = text.replace(/<img\s+(?:[^>]*?\s+)?src=\"([^\"]*)\"(?:[^>]*?\s+)?alt=\"([^\"]*)\"[^>]*>/gsi, (match, src, alt) => ``);
|
|
29
|
+
|
|
30
|
+
// Style/Script removal (strictly remove content)
|
|
31
|
+
text = text.replace(/<script.*?>.*?<\/script>/gsi, '');
|
|
32
|
+
text = text.replace(/<style.*?>.*?<\/style>/gsi, '');
|
|
33
|
+
|
|
34
|
+
// Final Strip of remaining tags
|
|
35
|
+
text = text.replace(/<[^>]+>/g, ' ');
|
|
36
|
+
|
|
37
|
+
// Entity decoding (Basic)
|
|
38
|
+
text = text.replace(/ /gi, ' ');
|
|
39
|
+
text = text.replace(/&/gi, '&');
|
|
40
|
+
text = text.replace(/</gi, '<');
|
|
41
|
+
text = text.replace(/>/gi, '>');
|
|
42
|
+
text = text.replace(/"/gi, '"');
|
|
43
|
+
text = text.replace(/'/gi, "'");
|
|
44
|
+
|
|
45
|
+
const lines = text.split('\n');
|
|
46
|
+
const cleanedLines: string[] = [];
|
|
47
|
+
|
|
48
|
+
// Heuristics for reply headers
|
|
49
|
+
const replyHeaderPatterns = [
|
|
50
|
+
/^On .* wrote:$/i,
|
|
51
|
+
/^From: .*$/i,
|
|
52
|
+
/^Sent: .*$/i,
|
|
53
|
+
/^To: .*$/i,
|
|
54
|
+
/^Subject: .*$/i
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
// Heuristics for footers
|
|
58
|
+
const footerPatterns = [
|
|
59
|
+
/unsubscribe/i,
|
|
60
|
+
/privacy policy/i,
|
|
61
|
+
/terms of service/i,
|
|
62
|
+
/view in browser/i,
|
|
63
|
+
/copyright \d{4}/i
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
for (let line of lines) {
|
|
67
|
+
let lineStripped = line.trim();
|
|
68
|
+
|
|
69
|
+
// 2. Quoted text removal (lines starting with >)
|
|
70
|
+
if (lineStripped.startsWith('>')) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 3. Check for specific reply separators
|
|
75
|
+
// If we hit a reply header, we truncate the rest (Aggressive strategy per Python code)
|
|
76
|
+
if (/^On .* wrote:$/i.test(lineStripped)) {
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 4. Footer removal (simple check on short lines)
|
|
81
|
+
if (lineStripped.length < 100) {
|
|
82
|
+
let isFooter = false;
|
|
83
|
+
for (const pattern of footerPatterns) {
|
|
84
|
+
if (pattern.test(lineStripped)) {
|
|
85
|
+
isFooter = true;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (isFooter) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
cleanedLines.push(line);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Reassemble
|
|
98
|
+
text = cleanedLines.join('\n');
|
|
99
|
+
|
|
100
|
+
// Collapse multiple newlines
|
|
101
|
+
text = text.replace(/\n{3,}/g, '\n\n');
|
|
102
|
+
|
|
103
|
+
// Sanitize LLM Special Tokens (Prevent Prompt Injection/Confusion)
|
|
104
|
+
// Break sequences like <|channel|>, [INST], <s>
|
|
105
|
+
text = text.replace(/<\|/g, '< |');
|
|
106
|
+
text = text.replace(/\|>/g, '| >');
|
|
107
|
+
text = text.replace(/\[INST\]/gi, '[ INST ]');
|
|
108
|
+
text = text.replace(/\[\/INST\]/gi, '[ /INST ]');
|
|
109
|
+
text = text.replace(/<s>/gi, '<s>');
|
|
110
|
+
text = text.replace(/<\/s>/gi, '</s>');
|
|
111
|
+
|
|
112
|
+
return text.trim();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypto';
|
|
2
|
+
import { config } from '../config/index.js';
|
|
3
|
+
|
|
4
|
+
// Edge Functions compatible encryption (matching supabase/functions/_shared/encryption.ts)
|
|
5
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
6
|
+
const IV_LENGTH = 12; // Match Edge Functions
|
|
7
|
+
const KEY_LENGTH = 32;
|
|
8
|
+
|
|
9
|
+
function getKey(): Buffer {
|
|
10
|
+
const secret = config.security.encryptionKey || 'dev-key-not-secure';
|
|
11
|
+
// Match Edge Functions key derivation: pad/slice to 32 chars
|
|
12
|
+
return Buffer.from(secret.padEnd(32, '0').slice(0, 32), 'utf8');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function encryptToken(plaintext: string): string {
|
|
16
|
+
if (!plaintext) return '';
|
|
17
|
+
|
|
18
|
+
const iv = randomBytes(IV_LENGTH);
|
|
19
|
+
const key = getKey();
|
|
20
|
+
|
|
21
|
+
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
22
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
|
23
|
+
const tag = cipher.getAuthTag();
|
|
24
|
+
|
|
25
|
+
// Format: base64(iv + ciphertext + tag) - compatible with Edge Functions
|
|
26
|
+
const combined = Buffer.concat([iv, encrypted, tag]);
|
|
27
|
+
return combined.toString('base64');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function decryptToken(encrypted: string): string {
|
|
31
|
+
if (!encrypted) return '';
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// Try Edge Functions format first: base64(iv + ciphertext + tag)
|
|
35
|
+
const combined = Buffer.from(encrypted, 'base64');
|
|
36
|
+
|
|
37
|
+
if (combined.length < IV_LENGTH + 16) {
|
|
38
|
+
// Too short, might be plaintext
|
|
39
|
+
return encrypted;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const iv = combined.subarray(0, IV_LENGTH);
|
|
43
|
+
const tag = combined.subarray(combined.length - 16);
|
|
44
|
+
const data = combined.subarray(IV_LENGTH, combined.length - 16);
|
|
45
|
+
const key = getKey();
|
|
46
|
+
|
|
47
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
|
48
|
+
decipher.setAuthTag(tag);
|
|
49
|
+
|
|
50
|
+
return Buffer.concat([decipher.update(data), decipher.final()]).toString('utf8');
|
|
51
|
+
} catch (error) {
|
|
52
|
+
// Try legacy format: salt:iv:tag:encrypted
|
|
53
|
+
try {
|
|
54
|
+
const parts = encrypted.split(':');
|
|
55
|
+
if (parts.length === 4) {
|
|
56
|
+
const [saltB64, ivB64, tagB64, dataB64] = parts;
|
|
57
|
+
const salt = Buffer.from(saltB64, 'base64');
|
|
58
|
+
const iv = Buffer.from(ivB64, 'base64');
|
|
59
|
+
const tag = Buffer.from(tagB64, 'base64');
|
|
60
|
+
const data = Buffer.from(dataB64, 'base64');
|
|
61
|
+
const secret = config.security.encryptionKey || 'dev-key-not-secure';
|
|
62
|
+
const key = scryptSync(secret, salt, KEY_LENGTH);
|
|
63
|
+
|
|
64
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
|
65
|
+
decipher.setAuthTag(tag);
|
|
66
|
+
|
|
67
|
+
return Buffer.concat([decipher.update(data), decipher.final()]).toString('utf8');
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
// Fall through to plaintext
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// If all decryption fails, assume plaintext (for migration)
|
|
74
|
+
return encrypted;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function generateSecureToken(length: number = 32): string {
|
|
79
|
+
return randomBytes(length).toString('hex');
|
|
80
|
+
}
|