@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
package/.env.example
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Supabase Configuration
|
|
2
|
+
VITE_SUPABASE_URL=your_supabase_url
|
|
3
|
+
VITE_SUPABASE_ANON_KEY=your_supabase_anon_key
|
|
4
|
+
|
|
5
|
+
# API Configuration
|
|
6
|
+
# Express API URL (default: http://localhost:3004)
|
|
7
|
+
# Note: RealTimeX Desktop uses ports 3001/3002
|
|
8
|
+
VITE_API_URL=http://localhost:3004
|
|
9
|
+
PORT=3004
|
|
10
|
+
|
|
11
|
+
# OpenAI / LLM Configuration
|
|
12
|
+
LLM_API_KEY=your_llm_api_key
|
|
13
|
+
LLM_BASE_URL=https://api.openai.com/v1
|
|
14
|
+
LLM_MODEL=gpt-4o-mini
|
|
15
|
+
|
|
16
|
+
# Security (required in production)
|
|
17
|
+
JWT_SECRET="your-secure-jwt-secret-min-32-chars"
|
|
18
|
+
TOKEN_ENCRYPTION_KEY="your-32-character-encryption-key"
|
|
19
|
+
CORS_ORIGINS="https://yourdomain.com"
|
|
20
|
+
|
|
21
|
+
# Development Only - Bypass authentication (DO NOT use in production)
|
|
22
|
+
DISABLE_AUTH=true
|
|
23
|
+
|
|
24
|
+
# Gmail OAuth
|
|
25
|
+
GMAIL_CLIENT_ID=your_gmail_client_id
|
|
26
|
+
GMAIL_CLIENT_SECRET=your_gmail_client_secret
|
|
27
|
+
|
|
28
|
+
# Microsoft Graph (Outlook)
|
|
29
|
+
MS_GRAPH_CLIENT_ID=your_ms_graph_client_id
|
|
30
|
+
MS_GRAPH_TENANT_ID=common
|
|
31
|
+
MS_GRAPH_CLIENT_SECRET=your_ms_graph_client_secret
|
|
32
|
+
|
|
33
|
+
# Processing
|
|
34
|
+
EMAIL_BATCH_SIZE=20
|
|
35
|
+
SYNC_INTERVAL_MS=300000
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 RealTimeX Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# AI Email Automator v2.0
|
|
2
|
+
|
|
3
|
+
An agentic, AI-powered email management platform that learns to handle your inbox. Categorize, archive, delete, and draft responses automatically using LLMs and Supabase.
|
|
4
|
+
|
|
5
|
+
## 🚀 Vision: "Own Your Inbox"
|
|
6
|
+
|
|
7
|
+
The AI Email Automator is designed as a standalone "Agent" for the RealTimeX ecosystem. It follows the **Own Your Data** philosophy:
|
|
8
|
+
|
|
9
|
+
- **Zero Cloud Costs**: Runs on your infrastructure using Supabase
|
|
10
|
+
- **Privacy First**: Your emails are processed on your private infrastructure
|
|
11
|
+
- **Agentic Intelligence**: An assistant that suggests "Winning Responses"
|
|
12
|
+
- **Production Ready**: Full security, testing, and DevOps infrastructure
|
|
13
|
+
|
|
14
|
+
## ✨ Features
|
|
15
|
+
|
|
16
|
+
### Core Capabilities
|
|
17
|
+
- **Smart Categorization**: AI classifies emails (Spam, Newsletter, Support, Client, etc.)
|
|
18
|
+
- **Inbox Zero Engine**: Auto-trash spam and archive newsletters
|
|
19
|
+
- **Winning Responses**: AI-generated draft replies for important emails
|
|
20
|
+
- **Multi-Provider**: Gmail (OAuth2) and Microsoft 365 (Device Flow)
|
|
21
|
+
- **Automation Rules**: Custom rules for auto-pilot email handling
|
|
22
|
+
- **Real-time Sync**: Live updates via Supabase subscriptions
|
|
23
|
+
- **Background Scheduler**: Automatic periodic email sync
|
|
24
|
+
|
|
25
|
+
### Production Features
|
|
26
|
+
- **Security**: JWT auth, token encryption, rate limiting, input validation
|
|
27
|
+
- **Hybrid Architecture**: Edge Functions (serverless) + Local App (privacy)
|
|
28
|
+
- **State Management**: React Context with centralized app state
|
|
29
|
+
- **Error Handling**: Error boundaries, toast notifications, logging
|
|
30
|
+
- **Analytics Dashboard**: Email stats, category breakdown, sync history
|
|
31
|
+
- **RealTimeX Integration**: Works with RealTimeX Desktop as Local App
|
|
32
|
+
|
|
33
|
+
## 🛠 Tech Stack
|
|
34
|
+
|
|
35
|
+
| Layer | Technologies |
|
|
36
|
+
|-------|-------------|
|
|
37
|
+
| **Frontend** | React 19, Vite 7, TailwindCSS 4, Lucide Icons |
|
|
38
|
+
| **Edge Functions** | Deno, Supabase Functions (OAuth, DB proxy) |
|
|
39
|
+
| **Local API** | Node.js, Express 5, TypeScript (Sync, AI) |
|
|
40
|
+
| **AI** | OpenAI / Instructor-JS (supports Ollama, LM Studio) |
|
|
41
|
+
| **Database** | Supabase (PostgreSQL) with RLS |
|
|
42
|
+
| **Testing** | Vitest, Testing Library |
|
|
43
|
+
|
|
44
|
+
## 🏁 Quick Start
|
|
45
|
+
|
|
46
|
+
For detailed instructions on installation, configuration, and usage, please see the [**Email Automator Documentation Hub**](docs/README.md).
|
|
47
|
+
|
|
48
|
+
### Prerequisites
|
|
49
|
+
- Node.js v20+
|
|
50
|
+
- Supabase project with CLI access
|
|
51
|
+
- LLM API key (OpenAI, Anthropic, or local)
|
|
52
|
+
|
|
53
|
+
### Option 1: Using npx (Recommended)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Interactive setup
|
|
57
|
+
npx @realtimex/email-automator-setup
|
|
58
|
+
|
|
59
|
+
# Deploy Edge Functions
|
|
60
|
+
npx @realtimex/email-automator-deploy
|
|
61
|
+
|
|
62
|
+
# Start Email Automator
|
|
63
|
+
npx @realtimex/email-automator
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Option 2: Clone and Install
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
git clone https://github.com/therealtimex/email-automator.git
|
|
70
|
+
cd email-automator
|
|
71
|
+
npm install
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Setup
|
|
75
|
+
|
|
76
|
+
1. **Deploy Edge Functions to Supabase:**
|
|
77
|
+
```bash
|
|
78
|
+
supabase login
|
|
79
|
+
./scripts/deploy-functions.sh
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
2. **Configure Edge Function Secrets in Supabase Dashboard:**
|
|
83
|
+
- Settings → Edge Functions → Add secrets
|
|
84
|
+
- Required: `TOKEN_ENCRYPTION_KEY`, `GMAIL_CLIENT_ID`, `GMAIL_CLIENT_SECRET`, etc.
|
|
85
|
+
|
|
86
|
+
3. **Configure Local Environment:**
|
|
87
|
+
```bash
|
|
88
|
+
cp .env.example .env
|
|
89
|
+
# Edit .env with your credentials
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Development
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# Terminal 1: Local API (Email Sync & AI Processing)
|
|
96
|
+
# Default port: 3004 (RealTimeX Desktop uses 3001/3002)
|
|
97
|
+
npm run dev:api
|
|
98
|
+
|
|
99
|
+
# Terminal 2: Frontend
|
|
100
|
+
npm run dev
|
|
101
|
+
|
|
102
|
+
# Optional: Specify custom ports
|
|
103
|
+
npm run dev:api -- --port 3005
|
|
104
|
+
npm run dev -- --port 5174
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Note:** Email Automator uses port **3004** by default to avoid conflicts with RealTimeX Desktop (ports 3001/3002). You can change ports via command line arguments or environment variables.
|
|
108
|
+
|
|
109
|
+
### Using npx
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# Start with default port (3004)
|
|
113
|
+
npx @realtimex/email-automator
|
|
114
|
+
|
|
115
|
+
# Start with custom port
|
|
116
|
+
npx @realtimex/email-automator --port 3005
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
See [NPX Usage Guide](docs-dev/NPX-USAGE.md) for complete documentation.
|
|
120
|
+
|
|
121
|
+
## 📂 Project Structure
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
├── api/ # Local App (Express)
|
|
125
|
+
│ ├── server.ts # Express entry point
|
|
126
|
+
│ └── src/
|
|
127
|
+
│ ├── routes/ # Sync & Actions endpoints
|
|
128
|
+
│ ├── services/ # Email sync, AI processing
|
|
129
|
+
│ └── utils/ # Logger, crypto, helpers
|
|
130
|
+
├── src/ # Frontend (React)
|
|
131
|
+
│ ├── components/ # React components
|
|
132
|
+
│ ├── context/ # App state management
|
|
133
|
+
│ ├── hooks/ # Custom hooks (realtime)
|
|
134
|
+
│ └── lib/ # Hybrid API client, types
|
|
135
|
+
├── supabase/ # Supabase Configuration
|
|
136
|
+
│ ├── functions/ # Edge Functions (OAuth, DB)
|
|
137
|
+
│ │ ├── _shared/ # Shared utilities
|
|
138
|
+
│ │ ├── auth-gmail/ # Gmail OAuth
|
|
139
|
+
│ │ ├── auth-microsoft/ # Microsoft OAuth
|
|
140
|
+
│ │ ├── api-v1-accounts/ # Account management
|
|
141
|
+
│ │ ├── api-v1-emails/ # Email operations
|
|
142
|
+
│ │ ├── api-v1-rules/ # Rules CRUD
|
|
143
|
+
│ │ └── api-v1-settings/ # Settings & stats
|
|
144
|
+
│ └── migrations/ # Database schema
|
|
145
|
+
├── scripts/
|
|
146
|
+
│ └── deploy-functions.sh # Deploy Edge Functions
|
|
147
|
+
└── tests/ # Unit & integration tests
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## 🔐 Environment Variables
|
|
151
|
+
|
|
152
|
+
### Edge Functions (Supabase Dashboard)
|
|
153
|
+
```bash
|
|
154
|
+
TOKEN_ENCRYPTION_KEY=32-char-key
|
|
155
|
+
GMAIL_CLIENT_ID=xxx
|
|
156
|
+
GMAIL_CLIENT_SECRET=xxx
|
|
157
|
+
MS_GRAPH_CLIENT_ID=xxx
|
|
158
|
+
MS_GRAPH_CLIENT_SECRET=xxx
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Local App (.env file)
|
|
162
|
+
```bash
|
|
163
|
+
# Supabase
|
|
164
|
+
VITE_SUPABASE_URL=https://xxx.supabase.co
|
|
165
|
+
VITE_SUPABASE_ANON_KEY=your-anon-key
|
|
166
|
+
|
|
167
|
+
# API Configuration (default port: 3004)
|
|
168
|
+
VITE_API_URL=http://localhost:3004
|
|
169
|
+
PORT=3004
|
|
170
|
+
|
|
171
|
+
# LLM
|
|
172
|
+
LLM_API_KEY=your-llm-key
|
|
173
|
+
LLM_BASE_URL=https://api.openai.com/v1
|
|
174
|
+
LLM_MODEL=gpt-4o-mini
|
|
175
|
+
|
|
176
|
+
# Development
|
|
177
|
+
DISABLE_AUTH=true
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## 🧪 Testing
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
npm run test # Watch mode
|
|
184
|
+
npm run test:run # Single run
|
|
185
|
+
npm run test:coverage # With coverage
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## 📡 API Architecture
|
|
189
|
+
|
|
190
|
+
### Edge Functions (Supabase)
|
|
191
|
+
| Method | Endpoint | Description |
|
|
192
|
+
|--------|----------|-------------|
|
|
193
|
+
| GET | `/auth-gmail?action=url` | Get Gmail OAuth URL |
|
|
194
|
+
| POST | `/auth-gmail` | Complete Gmail OAuth |
|
|
195
|
+
| POST | `/auth-microsoft?action=device-flow` | Start Microsoft device flow |
|
|
196
|
+
| GET | `/api-v1-accounts` | List connected accounts |
|
|
197
|
+
| GET | `/api-v1-emails` | List processed emails |
|
|
198
|
+
| GET | `/api-v1-rules` | List automation rules |
|
|
199
|
+
| GET | `/api-v1-settings` | Get user settings |
|
|
200
|
+
|
|
201
|
+
### Local App (Express)
|
|
202
|
+
| Method | Endpoint | Description |
|
|
203
|
+
|--------|----------|-------------|
|
|
204
|
+
| POST | `/api/sync` | Trigger email sync |
|
|
205
|
+
| POST | `/api/actions/execute` | Execute email action |
|
|
206
|
+
| POST | `/api/actions/draft/:id` | Generate draft reply |
|
|
207
|
+
| GET | `/api/health` | Health check |
|
|
208
|
+
|
|
209
|
+
## 🤝 Contributing
|
|
210
|
+
|
|
211
|
+
1. Fork the repository
|
|
212
|
+
2. Create a feature branch
|
|
213
|
+
3. Make your changes
|
|
214
|
+
4. Run tests: `npm run test:run`
|
|
215
|
+
5. Submit a Pull Request
|
|
216
|
+
|
|
217
|
+
## 📄 License
|
|
218
|
+
|
|
219
|
+
MIT License - Copyright (c) 2026 RealTimeX Team
|
|
220
|
+
|
|
221
|
+
## 📦 NPX Commands
|
|
222
|
+
|
|
223
|
+
Email Automator is fully compatible with npx for easy installation and execution:
|
|
224
|
+
|
|
225
|
+
| Command | Description |
|
|
226
|
+
|---------|-------------|
|
|
227
|
+
| `npx @realtimex/email-automator` | Start the Email Automator API server |
|
|
228
|
+
| `npx @realtimex/email-automator-setup` | Interactive setup wizard |
|
|
229
|
+
| `npx @realtimex/email-automator-deploy` | Deploy Edge Functions to Supabase |
|
|
230
|
+
|
|
231
|
+
### Examples
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
# First time setup
|
|
235
|
+
npx @realtimex/email-automator-setup
|
|
236
|
+
npx @realtimex/email-automator-deploy
|
|
237
|
+
npx @realtimex/email-automator
|
|
238
|
+
|
|
239
|
+
# Daily usage
|
|
240
|
+
npx @realtimex/email-automator
|
|
241
|
+
|
|
242
|
+
# Custom port
|
|
243
|
+
npx @realtimex/email-automator --port 3005
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
See [NPX Usage Guide](docs-dev/NPX-USAGE.md) for complete documentation.
|
|
247
|
+
|
package/api/server.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import cors from 'cors';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { spawn } from 'child_process';
|
|
6
|
+
import { config, validateConfig } from './src/config/index.js';
|
|
7
|
+
import { errorHandler } from './src/middleware/errorHandler.js';
|
|
8
|
+
import { apiRateLimit } from './src/middleware/rateLimit.js';
|
|
9
|
+
import routes from './src/routes/index.js';
|
|
10
|
+
import { logger } from './src/utils/logger.js';
|
|
11
|
+
import { getServerSupabase } from './src/services/supabase.js';
|
|
12
|
+
import { startScheduler, stopScheduler } from './src/services/scheduler.js';
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
|
|
17
|
+
// Validate configuration
|
|
18
|
+
const configValidation = validateConfig();
|
|
19
|
+
if (!configValidation.valid) {
|
|
20
|
+
logger.warn('Configuration warnings', { errors: configValidation.errors });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const app = express();
|
|
24
|
+
|
|
25
|
+
// Security headers
|
|
26
|
+
app.use((req, res, next) => {
|
|
27
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
28
|
+
res.setHeader('X-Frame-Options', 'DENY');
|
|
29
|
+
res.setHeader('X-XSS-Protection', '1; mode=block');
|
|
30
|
+
if (config.isProduction) {
|
|
31
|
+
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
|
32
|
+
}
|
|
33
|
+
next();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// CORS configuration
|
|
37
|
+
app.use(cors({
|
|
38
|
+
origin: config.isProduction
|
|
39
|
+
? config.security.corsOrigins
|
|
40
|
+
: true, // Allow all in development
|
|
41
|
+
credentials: true,
|
|
42
|
+
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
43
|
+
allowedHeaders: ['Content-Type', 'Authorization', 'X-Supabase-Url', 'X-Supabase-Anon-Key'],
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
// Body parsing
|
|
47
|
+
app.use(express.json({ limit: '10mb' }));
|
|
48
|
+
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
|
49
|
+
|
|
50
|
+
// Request logging
|
|
51
|
+
app.use((req, res, next) => {
|
|
52
|
+
const start = Date.now();
|
|
53
|
+
res.on('finish', () => {
|
|
54
|
+
const duration = Date.now() - start;
|
|
55
|
+
logger.debug(`${req.method} ${req.path}`, {
|
|
56
|
+
status: res.statusCode,
|
|
57
|
+
duration: `${duration}ms`,
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
next();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Rate limiting (global)
|
|
64
|
+
app.use('/api', apiRateLimit);
|
|
65
|
+
|
|
66
|
+
// API routes
|
|
67
|
+
app.use('/api', routes);
|
|
68
|
+
|
|
69
|
+
// Serve static files in production or if dist exists
|
|
70
|
+
const distPath = path.join(__dirname, '..', 'dist');
|
|
71
|
+
app.use(express.static(distPath));
|
|
72
|
+
|
|
73
|
+
// Handle client-side routing
|
|
74
|
+
app.get(/.*/, (req, res, next) => {
|
|
75
|
+
if (req.path.startsWith('/api')) return next();
|
|
76
|
+
res.sendFile(path.join(distPath, 'index.html'), (err) => {
|
|
77
|
+
if (err) {
|
|
78
|
+
// If dist doesn't exist, return 404 for non-API routes
|
|
79
|
+
res.status(404).json({
|
|
80
|
+
success: false,
|
|
81
|
+
error: { code: 'NOT_FOUND', message: 'Frontend not built or endpoint not found' }
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Error handler (must be last)
|
|
88
|
+
app.use(errorHandler);
|
|
89
|
+
|
|
90
|
+
// Graceful shutdown
|
|
91
|
+
const shutdown = () => {
|
|
92
|
+
logger.info('Shutting down gracefully...');
|
|
93
|
+
stopScheduler();
|
|
94
|
+
process.exit(0);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
process.on('SIGTERM', shutdown);
|
|
98
|
+
process.on('SIGINT', shutdown);
|
|
99
|
+
|
|
100
|
+
// Start server
|
|
101
|
+
const server = app.listen(config.port, () => {
|
|
102
|
+
const url = `http://localhost:${config.port}`;
|
|
103
|
+
logger.info(`Server running at ${url}`, {
|
|
104
|
+
environment: config.nodeEnv,
|
|
105
|
+
supabase: getServerSupabase() ? 'connected' : 'not configured',
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Start background scheduler
|
|
109
|
+
if (getServerSupabase()) {
|
|
110
|
+
startScheduler();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Automatically open browser unless -n flag is provided
|
|
114
|
+
if (!config.noUi) {
|
|
115
|
+
const startCommand = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
116
|
+
spawn(startCommand, [url], { detached: true }).unref();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Handle server errors
|
|
121
|
+
server.on('error', (error: NodeJS.ErrnoException) => {
|
|
122
|
+
if (error.code === 'EADDRINUSE') {
|
|
123
|
+
logger.error(`Port ${config.port} is already in use`);
|
|
124
|
+
} else {
|
|
125
|
+
logger.error('Server error', error);
|
|
126
|
+
}
|
|
127
|
+
process.exit(1);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
export default app;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import dotenv from 'dotenv';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
|
|
5
|
+
dotenv.config();
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
|
|
10
|
+
function parseArgs(args: string[]): { port: number | null, noUi: boolean } {
|
|
11
|
+
const portIndex = args.indexOf('--port');
|
|
12
|
+
let port = null;
|
|
13
|
+
if (portIndex !== -1 && args[portIndex + 1]) {
|
|
14
|
+
const p = parseInt(args[portIndex + 1], 10);
|
|
15
|
+
if (!isNaN(p) && p > 0 && p < 65536) {
|
|
16
|
+
port = p;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const noUi = args.includes('--no-ui');
|
|
21
|
+
|
|
22
|
+
return { port, noUi };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const cliArgs = parseArgs(process.argv.slice(2));
|
|
26
|
+
|
|
27
|
+
export const config = {
|
|
28
|
+
// Server
|
|
29
|
+
// Default port 3004 (RealTimeX Desktop uses 3001/3002)
|
|
30
|
+
port: cliArgs.port || (process.env.PORT ? parseInt(process.env.PORT, 10) : 3004),
|
|
31
|
+
noUi: cliArgs.noUi,
|
|
32
|
+
nodeEnv: process.env.NODE_ENV || 'development',
|
|
33
|
+
isProduction: process.env.NODE_ENV === 'production',
|
|
34
|
+
|
|
35
|
+
// Paths
|
|
36
|
+
rootDir: join(__dirname, '..', '..', '..'),
|
|
37
|
+
scriptsDir: join(__dirname, '..', '..', '..', 'scripts'),
|
|
38
|
+
|
|
39
|
+
// Supabase
|
|
40
|
+
supabase: {
|
|
41
|
+
url: process.env.SUPABASE_URL || process.env.VITE_SUPABASE_URL || '',
|
|
42
|
+
anonKey: process.env.SUPABASE_ANON_KEY || process.env.VITE_SUPABASE_ANON_KEY || '',
|
|
43
|
+
serviceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY || '',
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
// LLM
|
|
47
|
+
llm: {
|
|
48
|
+
apiKey: process.env.LLM_API_KEY || '',
|
|
49
|
+
baseUrl: process.env.LLM_BASE_URL,
|
|
50
|
+
model: process.env.LLM_MODEL || 'gpt-4o-mini',
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// OAuth - Gmail
|
|
54
|
+
gmail: {
|
|
55
|
+
clientId: process.env.GMAIL_CLIENT_ID || '',
|
|
56
|
+
clientSecret: process.env.GMAIL_CLIENT_SECRET || '',
|
|
57
|
+
redirectUri: process.env.GMAIL_REDIRECT_URI || 'urn:ietf:wg:oauth:2.0:oob',
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
// OAuth - Microsoft
|
|
61
|
+
microsoft: {
|
|
62
|
+
clientId: process.env.MS_GRAPH_CLIENT_ID || '',
|
|
63
|
+
tenantId: process.env.MS_GRAPH_TENANT_ID || 'common',
|
|
64
|
+
clientSecret: process.env.MS_GRAPH_CLIENT_SECRET || '',
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
// Security
|
|
68
|
+
security: {
|
|
69
|
+
encryptionKey: process.env.TOKEN_ENCRYPTION_KEY || '',
|
|
70
|
+
jwtSecret: process.env.JWT_SECRET || 'dev-secret-change-in-production',
|
|
71
|
+
corsOrigins: process.env.CORS_ORIGINS?.split(',') || ['http://localhost:3003', 'http://localhost:5173'],
|
|
72
|
+
rateLimitWindowMs: 15 * 60 * 1000, // 15 minutes
|
|
73
|
+
rateLimitMax: 100,
|
|
74
|
+
disableAuth: process.env.DISABLE_AUTH === 'true',
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
// Processing
|
|
78
|
+
processing: {
|
|
79
|
+
batchSize: parseInt(process.env.EMAIL_BATCH_SIZE || '20', 10),
|
|
80
|
+
syncIntervalMs: parseInt(process.env.SYNC_INTERVAL_MS || '60000', 10), // 1 minute
|
|
81
|
+
maxRetries: 3,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export function validateConfig(): { valid: boolean; errors: string[] } {
|
|
86
|
+
const errors: string[] = [];
|
|
87
|
+
|
|
88
|
+
if (!config.supabase.url) {
|
|
89
|
+
errors.push('SUPABASE_URL is required');
|
|
90
|
+
}
|
|
91
|
+
if (!config.supabase.anonKey) {
|
|
92
|
+
errors.push('SUPABASE_ANON_KEY is required');
|
|
93
|
+
}
|
|
94
|
+
if (config.isProduction && config.security.jwtSecret === 'dev-secret-change-in-production') {
|
|
95
|
+
errors.push('JWT_SECRET must be set in production');
|
|
96
|
+
}
|
|
97
|
+
if (config.isProduction && !config.security.encryptionKey) {
|
|
98
|
+
errors.push('TOKEN_ENCRYPTION_KEY must be set in production');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { valid: errors.length === 0, errors };
|
|
102
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { createClient, SupabaseClient, User } from '@supabase/supabase-js';
|
|
3
|
+
import { config } from '../config/index.js';
|
|
4
|
+
import { AuthenticationError, AuthorizationError } from './errorHandler.js';
|
|
5
|
+
import { createLogger, Logger } from '../utils/logger.js';
|
|
6
|
+
|
|
7
|
+
const logger = createLogger('AuthMiddleware');
|
|
8
|
+
|
|
9
|
+
import { getServerSupabase, isValidUrl } from '../services/supabase.js';
|
|
10
|
+
|
|
11
|
+
// Extend Express Request to include user
|
|
12
|
+
declare global {
|
|
13
|
+
namespace Express {
|
|
14
|
+
interface Request {
|
|
15
|
+
user?: User;
|
|
16
|
+
supabase?: SupabaseClient;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Check if anon key looks valid (JWT or publishable key format)
|
|
22
|
+
function isValidAnonKey(key: string): boolean {
|
|
23
|
+
if (!key) return false;
|
|
24
|
+
// JWT anon keys start with eyJ, publishable keys start with sb_publishable_
|
|
25
|
+
return key.startsWith('eyJ') || key.startsWith('sb_publishable_');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Helper to get Supabase config from request headers (frontend passes these)
|
|
29
|
+
function getSupabaseConfigFromRequest(req: Request): { url: string; anonKey: string } | null {
|
|
30
|
+
const url = req.headers['x-supabase-url'] as string;
|
|
31
|
+
const anonKey = req.headers['x-supabase-anon-key'] as string;
|
|
32
|
+
|
|
33
|
+
if (url && anonKey && isValidUrl(url) && isValidAnonKey(anonKey)) {
|
|
34
|
+
return { url, anonKey };
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function authMiddleware(
|
|
40
|
+
req: Request,
|
|
41
|
+
_res: Response,
|
|
42
|
+
next: NextFunction
|
|
43
|
+
): Promise<void> {
|
|
44
|
+
try {
|
|
45
|
+
// Get Supabase config: prefer env vars, fallback to request headers
|
|
46
|
+
const headerConfig = getSupabaseConfigFromRequest(req);
|
|
47
|
+
|
|
48
|
+
const envUrl = config.supabase.url;
|
|
49
|
+
const envKey = config.supabase.anonKey;
|
|
50
|
+
|
|
51
|
+
// Basic validation: URL must start with http(s)
|
|
52
|
+
// This prevents using placeholders like "CHANGE_ME" or empty strings
|
|
53
|
+
const isEnvUrlValid = envUrl && (envUrl.startsWith('http://') || envUrl.startsWith('https://'));
|
|
54
|
+
const isEnvKeyValid = !!envKey && envKey.length > 0;
|
|
55
|
+
|
|
56
|
+
const supabaseUrl = isEnvUrlValid ? envUrl : (headerConfig?.url || '');
|
|
57
|
+
const supabaseAnonKey = isEnvKeyValid ? envKey : (headerConfig?.anonKey || '');
|
|
58
|
+
|
|
59
|
+
// Development bypass: skip auth if DISABLE_AUTH=true in non-production
|
|
60
|
+
if (config.security.disableAuth && !config.isProduction) {
|
|
61
|
+
logger.warn('Auth disabled for development - creating mock user');
|
|
62
|
+
|
|
63
|
+
// Create a mock user for development
|
|
64
|
+
req.user = {
|
|
65
|
+
id: '00000000-0000-0000-0000-000000000000',
|
|
66
|
+
email: 'dev@local.test',
|
|
67
|
+
user_metadata: {},
|
|
68
|
+
app_metadata: {},
|
|
69
|
+
aud: 'authenticated',
|
|
70
|
+
created_at: new Date().toISOString(),
|
|
71
|
+
} as User;
|
|
72
|
+
|
|
73
|
+
// Use the shared Supabase client, or create one from request headers
|
|
74
|
+
let supabase = getServerSupabase();
|
|
75
|
+
if (!supabase && supabaseUrl && supabaseAnonKey) {
|
|
76
|
+
supabase = createClient(supabaseUrl, supabaseAnonKey, {
|
|
77
|
+
auth: { autoRefreshToken: false, persistSession: false },
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (supabase) {
|
|
82
|
+
req.supabase = supabase;
|
|
83
|
+
// Initialize logger persistence for mock user
|
|
84
|
+
Logger.setPersistence(supabase, req.user.id);
|
|
85
|
+
} else {
|
|
86
|
+
throw new AuthenticationError('Supabase not configured. Please set up Supabase in the app or provide SUPABASE_URL/ANON_KEY in .env');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return next();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const authHeader = req.headers.authorization;
|
|
93
|
+
|
|
94
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
95
|
+
throw new AuthenticationError('Missing or invalid authorization header');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const token = authHeader.substring(7);
|
|
99
|
+
|
|
100
|
+
if (!supabaseUrl || !supabaseAnonKey) {
|
|
101
|
+
throw new AuthenticationError('Supabase not configured. Please set up Supabase in the app or provide SUPABASE_URL/ANON_KEY in .env');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Create a Supabase client with the user's token
|
|
105
|
+
const supabase = createClient(supabaseUrl, supabaseAnonKey, {
|
|
106
|
+
global: {
|
|
107
|
+
headers: {
|
|
108
|
+
Authorization: `Bearer ${token}`,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Verify the token by getting the user
|
|
114
|
+
const { data: { user }, error } = await supabase.auth.getUser(token);
|
|
115
|
+
|
|
116
|
+
if (error || !user) {
|
|
117
|
+
logger.debug('Auth failed', { error: error?.message });
|
|
118
|
+
throw new AuthenticationError('Invalid or expired token');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Initialize logger persistence for this request
|
|
122
|
+
Logger.setPersistence(supabase, user.id);
|
|
123
|
+
|
|
124
|
+
// Attach user and supabase client to request
|
|
125
|
+
req.user = user;
|
|
126
|
+
req.supabase = supabase;
|
|
127
|
+
|
|
128
|
+
next();
|
|
129
|
+
} catch (error) {
|
|
130
|
+
logger.error('Auth middleware error', error);
|
|
131
|
+
next(error);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function optionalAuth(
|
|
136
|
+
req: Request,
|
|
137
|
+
_res: Response,
|
|
138
|
+
next: NextFunction
|
|
139
|
+
): void {
|
|
140
|
+
const authHeader = req.headers.authorization;
|
|
141
|
+
|
|
142
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
143
|
+
// No auth provided, continue without user
|
|
144
|
+
return next();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// If auth is provided, validate it
|
|
148
|
+
authMiddleware(req, _res, next);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function requireRole(roles: string[]) {
|
|
152
|
+
return async (req: Request, _res: Response, next: NextFunction) => {
|
|
153
|
+
if (!req.user) {
|
|
154
|
+
return next(new AuthenticationError());
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Check user metadata for role (customize based on your auth setup)
|
|
158
|
+
const userRole = req.user.user_metadata?.role || 'user';
|
|
159
|
+
|
|
160
|
+
if (!roles.includes(userRole)) {
|
|
161
|
+
return next(new AuthorizationError(`Requires one of: ${roles.join(', ')}`));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
next();
|
|
165
|
+
};
|
|
166
|
+
}
|