heathrow 0.7.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.
- checksums.yaml +7 -0
- data/.gitignore +58 -0
- data/README.md +205 -0
- data/bin/heathrow +42 -0
- data/bin/heathrowd +283 -0
- data/docs/ARCHITECTURE.md +1172 -0
- data/docs/DATABASE_SCHEMA.md +685 -0
- data/docs/DEVELOPMENT_WORKFLOW.md +867 -0
- data/docs/DISCORD_SETUP.md +142 -0
- data/docs/GMAIL_OAUTH_SETUP.md +120 -0
- data/docs/PLUGIN_SYSTEM.md +1370 -0
- data/docs/PROJECT_PLAN.md +1022 -0
- data/docs/README.md +417 -0
- data/docs/REDDIT_SETUP.md +174 -0
- data/docs/REPLY_FORWARD.md +182 -0
- data/docs/WHATSAPP_TELEGRAM_SETUP.md +306 -0
- data/heathrow.gemspec +34 -0
- data/heathrowd.service +21 -0
- data/img/heathrow.svg +95 -0
- data/img/rss_threaded.png +0 -0
- data/img/sources.png +0 -0
- data/lib/heathrow/address_book.rb +42 -0
- data/lib/heathrow/config.rb +332 -0
- data/lib/heathrow/database.rb +731 -0
- data/lib/heathrow/database_new.rb +392 -0
- data/lib/heathrow/event_bus.rb +175 -0
- data/lib/heathrow/logger.rb +122 -0
- data/lib/heathrow/message.rb +176 -0
- data/lib/heathrow/message_composer.rb +399 -0
- data/lib/heathrow/message_organizer.rb +774 -0
- data/lib/heathrow/migrations/001_initial_schema.rb +248 -0
- data/lib/heathrow/notmuch.rb +45 -0
- data/lib/heathrow/oauth2_smtp.rb +254 -0
- data/lib/heathrow/plugin/base.rb +212 -0
- data/lib/heathrow/plugin_manager.rb +141 -0
- data/lib/heathrow/poller.rb +93 -0
- data/lib/heathrow/smtp_sender.rb +204 -0
- data/lib/heathrow/source.rb +39 -0
- data/lib/heathrow/sources/base.rb +74 -0
- data/lib/heathrow/sources/discord.rb +357 -0
- data/lib/heathrow/sources/gmail.rb +294 -0
- data/lib/heathrow/sources/imap.rb +198 -0
- data/lib/heathrow/sources/instagram.rb +307 -0
- data/lib/heathrow/sources/instagram_fetch.py +101 -0
- data/lib/heathrow/sources/instagram_send.py +55 -0
- data/lib/heathrow/sources/instagram_send_marionette.py +104 -0
- data/lib/heathrow/sources/maildir.rb +606 -0
- data/lib/heathrow/sources/messenger.rb +212 -0
- data/lib/heathrow/sources/messenger_fetch.js +297 -0
- data/lib/heathrow/sources/messenger_fetch_marionette.py +138 -0
- data/lib/heathrow/sources/messenger_send.js +32 -0
- data/lib/heathrow/sources/messenger_send.py +100 -0
- data/lib/heathrow/sources/reddit.rb +461 -0
- data/lib/heathrow/sources/rss.rb +299 -0
- data/lib/heathrow/sources/slack.rb +375 -0
- data/lib/heathrow/sources/source_manager.rb +328 -0
- data/lib/heathrow/sources/telegram.rb +498 -0
- data/lib/heathrow/sources/webpage.rb +207 -0
- data/lib/heathrow/sources/weechat.rb +479 -0
- data/lib/heathrow/sources/whatsapp.rb +474 -0
- data/lib/heathrow/ui/application.rb +8098 -0
- data/lib/heathrow/ui/navigation.rb +8 -0
- data/lib/heathrow/ui/panes.rb +8 -0
- data/lib/heathrow/ui/source_wizard.rb +567 -0
- data/lib/heathrow/ui/threaded_view.rb +780 -0
- data/lib/heathrow/ui/views.rb +8 -0
- data/lib/heathrow/version.rb +3 -0
- data/lib/heathrow/wizards/discord_wizard.rb +193 -0
- data/lib/heathrow/wizards/slack_wizard.rb +140 -0
- data/lib/heathrow.rb +55 -0
- metadata +147 -0
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
# Heathrow Database Schema
|
|
2
|
+
|
|
3
|
+
**Database:** SQLite 3
|
|
4
|
+
**Location:** `~/.heathrow/heathrow.db`
|
|
5
|
+
**Version:** Schema version tracked in `schema_version` table
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Design Principles
|
|
10
|
+
|
|
11
|
+
1. **Normalization:** Avoid data duplication
|
|
12
|
+
2. **Flexibility:** JSON columns for plugin-specific data
|
|
13
|
+
3. **Performance:** Indices on frequently queried columns
|
|
14
|
+
4. **Migration:** Version-based schema upgrades
|
|
15
|
+
5. **Encryption:** Sensitive data encrypted at application layer
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Schema Version
|
|
20
|
+
|
|
21
|
+
```sql
|
|
22
|
+
CREATE TABLE schema_version (
|
|
23
|
+
version INTEGER PRIMARY KEY,
|
|
24
|
+
applied_at INTEGER NOT NULL -- Unix timestamp
|
|
25
|
+
);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Purpose:** Track database migrations.
|
|
29
|
+
|
|
30
|
+
**Usage:**
|
|
31
|
+
```ruby
|
|
32
|
+
current_version = db.query("SELECT MAX(version) FROM schema_version").first[0]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Messages Table
|
|
38
|
+
|
|
39
|
+
```sql
|
|
40
|
+
CREATE TABLE messages (
|
|
41
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
42
|
+
source_id INTEGER NOT NULL,
|
|
43
|
+
external_id TEXT NOT NULL, -- Source's message ID
|
|
44
|
+
thread_id TEXT, -- For threading support
|
|
45
|
+
parent_id INTEGER, -- Reply-to message (optional)
|
|
46
|
+
|
|
47
|
+
-- Sender information
|
|
48
|
+
sender TEXT NOT NULL, -- Email/username/phone
|
|
49
|
+
sender_name TEXT, -- Display name
|
|
50
|
+
|
|
51
|
+
-- Recipients (JSON array)
|
|
52
|
+
recipients TEXT NOT NULL, -- ["user1", "user2"]
|
|
53
|
+
cc TEXT, -- ["cc1", "cc2"]
|
|
54
|
+
bcc TEXT, -- ["bcc1", "bcc2"]
|
|
55
|
+
|
|
56
|
+
-- Content
|
|
57
|
+
subject TEXT,
|
|
58
|
+
content TEXT NOT NULL,
|
|
59
|
+
html_content TEXT, -- Original HTML (if applicable)
|
|
60
|
+
|
|
61
|
+
-- Metadata
|
|
62
|
+
timestamp INTEGER NOT NULL, -- Unix timestamp
|
|
63
|
+
received_at INTEGER NOT NULL, -- When we fetched it
|
|
64
|
+
read BOOLEAN DEFAULT 0,
|
|
65
|
+
starred BOOLEAN DEFAULT 0,
|
|
66
|
+
archived BOOLEAN DEFAULT 0,
|
|
67
|
+
|
|
68
|
+
-- Organization
|
|
69
|
+
labels TEXT, -- JSON array: ["work", "important"]
|
|
70
|
+
attachments TEXT, -- JSON array: [{"name": "file.pdf", "path": "/path"}]
|
|
71
|
+
|
|
72
|
+
-- Plugin-specific data (JSON)
|
|
73
|
+
metadata TEXT, -- {"slack_channel": "general", "discord_guild": "123"}
|
|
74
|
+
|
|
75
|
+
-- Constraints
|
|
76
|
+
UNIQUE(source_id, external_id),
|
|
77
|
+
FOREIGN KEY(source_id) REFERENCES sources(id) ON DELETE CASCADE,
|
|
78
|
+
FOREIGN KEY(parent_id) REFERENCES messages(id) ON DELETE SET NULL
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
-- Indices for performance
|
|
82
|
+
CREATE INDEX idx_messages_source ON messages(source_id);
|
|
83
|
+
CREATE INDEX idx_messages_timestamp ON messages(timestamp DESC);
|
|
84
|
+
CREATE INDEX idx_messages_thread ON messages(thread_id);
|
|
85
|
+
CREATE INDEX idx_messages_read ON messages(read);
|
|
86
|
+
CREATE INDEX idx_messages_sender ON messages(sender);
|
|
87
|
+
|
|
88
|
+
-- Full-text search
|
|
89
|
+
CREATE VIRTUAL TABLE messages_fts USING fts5(
|
|
90
|
+
subject,
|
|
91
|
+
content,
|
|
92
|
+
sender,
|
|
93
|
+
content=messages,
|
|
94
|
+
content_rowid=id
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
-- Triggers to keep FTS index updated
|
|
98
|
+
CREATE TRIGGER messages_ai AFTER INSERT ON messages BEGIN
|
|
99
|
+
INSERT INTO messages_fts(rowid, subject, content, sender)
|
|
100
|
+
VALUES (new.id, new.subject, new.content, new.sender);
|
|
101
|
+
END;
|
|
102
|
+
|
|
103
|
+
CREATE TRIGGER messages_ad AFTER DELETE ON messages BEGIN
|
|
104
|
+
DELETE FROM messages_fts WHERE rowid = old.id;
|
|
105
|
+
END;
|
|
106
|
+
|
|
107
|
+
CREATE TRIGGER messages_au AFTER UPDATE ON messages BEGIN
|
|
108
|
+
UPDATE messages_fts
|
|
109
|
+
SET subject = new.subject, content = new.content, sender = new.sender
|
|
110
|
+
WHERE rowid = new.id;
|
|
111
|
+
END;
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**JSON Column Examples:**
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
# recipients
|
|
118
|
+
["user@example.com", "another@example.com"]
|
|
119
|
+
|
|
120
|
+
# labels
|
|
121
|
+
["work", "important", "follow-up"]
|
|
122
|
+
|
|
123
|
+
# attachments
|
|
124
|
+
[
|
|
125
|
+
{"name": "report.pdf", "path": "~/.heathrow/attachments/abc123.pdf", "size": 102400},
|
|
126
|
+
{"name": "image.png", "path": "~/.heathrow/attachments/def456.png", "size": 51200}
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
# metadata (Gmail example)
|
|
130
|
+
{
|
|
131
|
+
"gmail_message_id": "<CABc123@mail.gmail.com>",
|
|
132
|
+
"gmail_thread_id": "18abc123def",
|
|
133
|
+
"gmail_labels": ["INBOX", "UNREAD"]
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# metadata (Slack example)
|
|
137
|
+
{
|
|
138
|
+
"slack_channel": "general",
|
|
139
|
+
"slack_team": "T0123ABC",
|
|
140
|
+
"slack_ts": "1234567890.123456",
|
|
141
|
+
"slack_reactions": [{"name": "thumbsup", "count": 3}]
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Sources Table
|
|
148
|
+
|
|
149
|
+
```sql
|
|
150
|
+
CREATE TABLE sources (
|
|
151
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
152
|
+
name TEXT NOT NULL UNIQUE, -- User-friendly name: "Work Email", "Personal Slack"
|
|
153
|
+
plugin_type TEXT NOT NULL, -- "gmail", "slack", "discord", etc.
|
|
154
|
+
enabled BOOLEAN DEFAULT 1,
|
|
155
|
+
|
|
156
|
+
-- Configuration (JSON, encrypted)
|
|
157
|
+
config TEXT NOT NULL, -- Plugin-specific settings
|
|
158
|
+
|
|
159
|
+
-- Capabilities (JSON array)
|
|
160
|
+
capabilities TEXT NOT NULL, -- ["read", "write", "attachments"]
|
|
161
|
+
|
|
162
|
+
-- Status
|
|
163
|
+
last_sync INTEGER, -- Unix timestamp of last successful fetch
|
|
164
|
+
last_error TEXT, -- Last error message (if any)
|
|
165
|
+
|
|
166
|
+
-- Statistics
|
|
167
|
+
message_count INTEGER DEFAULT 0,
|
|
168
|
+
created_at INTEGER NOT NULL,
|
|
169
|
+
updated_at INTEGER NOT NULL
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
CREATE INDEX idx_sources_enabled ON sources(enabled);
|
|
173
|
+
CREATE INDEX idx_sources_plugin_type ON sources(plugin_type);
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Config Column Examples:**
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
# Gmail
|
|
180
|
+
{
|
|
181
|
+
"email": "user@gmail.com",
|
|
182
|
+
"credentials": "ENCRYPTED:abc123...", # OAuth2 token
|
|
183
|
+
"sync_days": 30,
|
|
184
|
+
"sync_labels": ["INBOX", "SENT"]
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
# Slack
|
|
188
|
+
{
|
|
189
|
+
"workspace": "mycompany",
|
|
190
|
+
"token": "ENCRYPTED:xoxb-...",
|
|
191
|
+
"sync_channels": ["general", "random"],
|
|
192
|
+
"sync_dms": true
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# RSS
|
|
196
|
+
{
|
|
197
|
+
"feed_url": "https://example.com/feed.xml",
|
|
198
|
+
"update_interval": 3600
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Capabilities:**
|
|
203
|
+
- `read` - Can fetch messages
|
|
204
|
+
- `write` - Can send messages
|
|
205
|
+
- `real_time` - Supports live streaming
|
|
206
|
+
- `attachments` - Supports file attachments
|
|
207
|
+
- `threads` - Supports threaded conversations
|
|
208
|
+
- `search` - Supports server-side search
|
|
209
|
+
- `reactions` - Supports emoji reactions
|
|
210
|
+
- `typing` - Supports typing indicators
|
|
211
|
+
- `read_receipts` - Supports read receipts
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Views Table
|
|
216
|
+
|
|
217
|
+
```sql
|
|
218
|
+
CREATE TABLE views (
|
|
219
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
220
|
+
name TEXT NOT NULL UNIQUE, -- "All", "Unread", "Work", "Personal"
|
|
221
|
+
key_binding TEXT UNIQUE, -- "A", "N", "1", "2", etc.
|
|
222
|
+
|
|
223
|
+
-- Filter rules (JSON)
|
|
224
|
+
filters TEXT NOT NULL, -- Complex filter logic
|
|
225
|
+
|
|
226
|
+
-- Display options
|
|
227
|
+
sort_order TEXT DEFAULT 'timestamp DESC',
|
|
228
|
+
is_remainder BOOLEAN DEFAULT 0, -- Catch-all view
|
|
229
|
+
|
|
230
|
+
-- UI preferences
|
|
231
|
+
show_count BOOLEAN DEFAULT 1,
|
|
232
|
+
color INTEGER, -- Terminal color code
|
|
233
|
+
icon TEXT, -- Unicode icon
|
|
234
|
+
|
|
235
|
+
-- Metadata
|
|
236
|
+
created_at INTEGER NOT NULL,
|
|
237
|
+
updated_at INTEGER NOT NULL
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
CREATE INDEX idx_views_key_binding ON views(key_binding);
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**Filter Examples:**
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
# Show only unread messages from work email
|
|
247
|
+
{
|
|
248
|
+
"rules": [
|
|
249
|
+
{"field": "read", "op": "=", "value": false},
|
|
250
|
+
{"field": "source_id", "op": "=", "value": 1}
|
|
251
|
+
],
|
|
252
|
+
"logic": "AND"
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
# Show messages from specific senders OR with specific labels
|
|
256
|
+
{
|
|
257
|
+
"rules": [
|
|
258
|
+
{
|
|
259
|
+
"any": [
|
|
260
|
+
{"field": "sender", "op": "IN", "value": ["boss@work.com", "client@company.com"]},
|
|
261
|
+
{"field": "labels", "op": "CONTAINS", "value": "urgent"}
|
|
262
|
+
]
|
|
263
|
+
}
|
|
264
|
+
]
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
# Complex: (unread OR starred) AND (from work OR personal email)
|
|
268
|
+
{
|
|
269
|
+
"rules": [
|
|
270
|
+
{
|
|
271
|
+
"any": [
|
|
272
|
+
{"field": "read", "op": "=", "value": false},
|
|
273
|
+
{"field": "starred", "op": "=", "value": true}
|
|
274
|
+
]
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
"any": [
|
|
278
|
+
{"field": "source_id", "op": "=", "value": 1},
|
|
279
|
+
{"field": "source_id", "op": "=", "value": 2}
|
|
280
|
+
]
|
|
281
|
+
}
|
|
282
|
+
],
|
|
283
|
+
"logic": "AND"
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
# Remainder view (matches everything not matched by other views)
|
|
287
|
+
{
|
|
288
|
+
"rules": [],
|
|
289
|
+
"is_remainder": true
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
**Supported Filter Operators:**
|
|
294
|
+
- `=`, `!=` - Equality
|
|
295
|
+
- `>`, `<`, `>=`, `<=` - Comparison
|
|
296
|
+
- `CONTAINS` - Substring/array contains
|
|
297
|
+
- `IN` - Value in array
|
|
298
|
+
- `MATCHES` - Regex match
|
|
299
|
+
- `BEFORE`, `AFTER` - Date comparisons
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## Contacts Table
|
|
304
|
+
|
|
305
|
+
```sql
|
|
306
|
+
CREATE TABLE contacts (
|
|
307
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
308
|
+
name TEXT NOT NULL,
|
|
309
|
+
primary_email TEXT,
|
|
310
|
+
|
|
311
|
+
-- Platform identities (JSON)
|
|
312
|
+
identities TEXT, -- {"slack": "@john", "discord": "john#1234"}
|
|
313
|
+
|
|
314
|
+
-- Contact info
|
|
315
|
+
phone TEXT,
|
|
316
|
+
avatar_url TEXT,
|
|
317
|
+
|
|
318
|
+
-- Organization
|
|
319
|
+
tags TEXT, -- JSON array: ["work", "client"]
|
|
320
|
+
notes TEXT,
|
|
321
|
+
|
|
322
|
+
-- Statistics
|
|
323
|
+
message_count INTEGER DEFAULT 0,
|
|
324
|
+
last_contact INTEGER, -- Unix timestamp
|
|
325
|
+
|
|
326
|
+
-- Metadata
|
|
327
|
+
created_at INTEGER NOT NULL,
|
|
328
|
+
updated_at INTEGER NOT NULL
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
CREATE INDEX idx_contacts_email ON contacts(primary_email);
|
|
332
|
+
CREATE INDEX idx_contacts_name ON contacts(name);
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
**Identities Example:**
|
|
336
|
+
|
|
337
|
+
```ruby
|
|
338
|
+
{
|
|
339
|
+
"email": ["john@example.com", "john.doe@company.com"],
|
|
340
|
+
"slack": {"workspace": "T0123", "user_id": "U456", "handle": "@john"},
|
|
341
|
+
"discord": {"user_id": "123456789", "tag": "john#1234"},
|
|
342
|
+
"telegram": {"user_id": "987654", "username": "@johndoe"},
|
|
343
|
+
"phone": ["+1234567890"]
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Drafts Table
|
|
350
|
+
|
|
351
|
+
```sql
|
|
352
|
+
CREATE TABLE drafts (
|
|
353
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
354
|
+
source_id INTEGER, -- Which source to send from
|
|
355
|
+
reply_to_id INTEGER, -- If replying to a message
|
|
356
|
+
|
|
357
|
+
-- Content
|
|
358
|
+
recipients TEXT NOT NULL,
|
|
359
|
+
cc TEXT,
|
|
360
|
+
bcc TEXT,
|
|
361
|
+
subject TEXT,
|
|
362
|
+
content TEXT NOT NULL,
|
|
363
|
+
attachments TEXT,
|
|
364
|
+
|
|
365
|
+
-- Metadata
|
|
366
|
+
created_at INTEGER NOT NULL,
|
|
367
|
+
updated_at INTEGER NOT NULL,
|
|
368
|
+
|
|
369
|
+
FOREIGN KEY(source_id) REFERENCES sources(id) ON DELETE SET NULL,
|
|
370
|
+
FOREIGN KEY(reply_to_id) REFERENCES messages(id) ON DELETE SET NULL
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
CREATE INDEX idx_drafts_updated ON drafts(updated_at DESC);
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## Filters Table
|
|
379
|
+
|
|
380
|
+
```sql
|
|
381
|
+
CREATE TABLE filters (
|
|
382
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
383
|
+
name TEXT NOT NULL,
|
|
384
|
+
enabled BOOLEAN DEFAULT 1,
|
|
385
|
+
priority INTEGER DEFAULT 0, -- Higher priority = runs first
|
|
386
|
+
|
|
387
|
+
-- Conditions (JSON)
|
|
388
|
+
conditions TEXT NOT NULL,
|
|
389
|
+
|
|
390
|
+
-- Actions (JSON)
|
|
391
|
+
actions TEXT NOT NULL,
|
|
392
|
+
|
|
393
|
+
-- Metadata
|
|
394
|
+
created_at INTEGER NOT NULL,
|
|
395
|
+
updated_at INTEGER NOT NULL
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
CREATE INDEX idx_filters_enabled ON filters(enabled);
|
|
399
|
+
CREATE INDEX idx_filters_priority ON filters(priority DESC);
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
**Filter Examples:**
|
|
403
|
+
|
|
404
|
+
```ruby
|
|
405
|
+
# Auto-archive newsletters
|
|
406
|
+
{
|
|
407
|
+
"name": "Archive newsletters",
|
|
408
|
+
"conditions": {
|
|
409
|
+
"any": [
|
|
410
|
+
{"field": "sender", "op": "CONTAINS", "value": "newsletter"},
|
|
411
|
+
{"field": "subject", "op": "CONTAINS", "value": "unsubscribe"}
|
|
412
|
+
]
|
|
413
|
+
},
|
|
414
|
+
"actions": [
|
|
415
|
+
{"type": "set_field", "field": "archived", "value": true},
|
|
416
|
+
{"type": "set_field", "field": "read", "value": true}
|
|
417
|
+
]
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
# Auto-label messages from boss as important
|
|
421
|
+
{
|
|
422
|
+
"name": "Important from boss",
|
|
423
|
+
"conditions": {
|
|
424
|
+
"field": "sender",
|
|
425
|
+
"op": "=",
|
|
426
|
+
"value": "boss@company.com"
|
|
427
|
+
},
|
|
428
|
+
"actions": [
|
|
429
|
+
{"type": "add_label", "label": "important"},
|
|
430
|
+
{"type": "set_field", "field": "starred", "value": true}
|
|
431
|
+
]
|
|
432
|
+
}
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
## Settings Table
|
|
438
|
+
|
|
439
|
+
```sql
|
|
440
|
+
CREATE TABLE settings (
|
|
441
|
+
key TEXT PRIMARY KEY,
|
|
442
|
+
value TEXT NOT NULL,
|
|
443
|
+
updated_at INTEGER NOT NULL
|
|
444
|
+
);
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
**Common Settings:**
|
|
448
|
+
|
|
449
|
+
```ruby
|
|
450
|
+
# UI preferences
|
|
451
|
+
{"key": "ui.theme", "value": "dark"}
|
|
452
|
+
{"key": "ui.split_ratio", "value": "0.3"} # Left pane width
|
|
453
|
+
{"key": "ui.show_borders", "value": "true"}
|
|
454
|
+
|
|
455
|
+
# Behavior
|
|
456
|
+
{"key": "auto_mark_read", "value": "true"}
|
|
457
|
+
{"key": "notification_enabled", "value": "true"}
|
|
458
|
+
{"key": "sync_interval", "value": "300"} # 5 minutes
|
|
459
|
+
|
|
460
|
+
# Security
|
|
461
|
+
{"key": "encryption_key", "value": "ENCRYPTED:..."}
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## Migrations
|
|
467
|
+
|
|
468
|
+
**Migration Files:** `lib/heathrow/migrations/001_initial.rb`, `002_add_contacts.rb`, etc.
|
|
469
|
+
|
|
470
|
+
**Migration Template:**
|
|
471
|
+
|
|
472
|
+
```ruby
|
|
473
|
+
# lib/heathrow/migrations/001_initial.rb
|
|
474
|
+
module Heathrow
|
|
475
|
+
module Migrations
|
|
476
|
+
class Initial
|
|
477
|
+
VERSION = 1
|
|
478
|
+
|
|
479
|
+
def self.up(db)
|
|
480
|
+
db.exec <<-SQL
|
|
481
|
+
CREATE TABLE schema_version (
|
|
482
|
+
version INTEGER PRIMARY KEY,
|
|
483
|
+
applied_at INTEGER NOT NULL
|
|
484
|
+
);
|
|
485
|
+
SQL
|
|
486
|
+
|
|
487
|
+
db.exec <<-SQL
|
|
488
|
+
CREATE TABLE messages (
|
|
489
|
+
-- ... schema here
|
|
490
|
+
);
|
|
491
|
+
SQL
|
|
492
|
+
|
|
493
|
+
# Record migration
|
|
494
|
+
db.exec "INSERT INTO schema_version (version, applied_at) VALUES (?, ?)",
|
|
495
|
+
[VERSION, Time.now.to_i]
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def self.down(db)
|
|
499
|
+
db.exec "DROP TABLE messages"
|
|
500
|
+
db.exec "DELETE FROM schema_version WHERE version = ?", [VERSION]
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
**Running Migrations:**
|
|
508
|
+
|
|
509
|
+
```ruby
|
|
510
|
+
# lib/heathrow/database.rb
|
|
511
|
+
def migrate_to_latest
|
|
512
|
+
current = query("SELECT MAX(version) FROM schema_version").first&.first || 0
|
|
513
|
+
|
|
514
|
+
Dir["lib/heathrow/migrations/*.rb"].sort.each do |file|
|
|
515
|
+
require file
|
|
516
|
+
migration = # ... load migration class
|
|
517
|
+
next if migration::VERSION <= current
|
|
518
|
+
|
|
519
|
+
transaction do
|
|
520
|
+
migration.up(self)
|
|
521
|
+
log.info "Applied migration #{migration::VERSION}"
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
end
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
## Backup & Recovery
|
|
530
|
+
|
|
531
|
+
**Automatic Backups:**
|
|
532
|
+
- Daily backup to `~/.heathrow/backups/heathrow-YYYYMMDD.db`
|
|
533
|
+
- Keep last 7 days
|
|
534
|
+
- Weekly backup kept for 4 weeks
|
|
535
|
+
- Monthly backup kept for 12 months
|
|
536
|
+
|
|
537
|
+
**Recovery:**
|
|
538
|
+
```bash
|
|
539
|
+
cp ~/.heathrow/backups/heathrow-20240115.db ~/.heathrow/heathrow.db
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
**Export:**
|
|
543
|
+
```bash
|
|
544
|
+
sqlite3 ~/.heathrow/heathrow.db .dump > backup.sql
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
**Import:**
|
|
548
|
+
```bash
|
|
549
|
+
sqlite3 ~/.heathrow/heathrow-new.db < backup.sql
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
---
|
|
553
|
+
|
|
554
|
+
## Performance Considerations
|
|
555
|
+
|
|
556
|
+
### Query Optimization
|
|
557
|
+
|
|
558
|
+
**Slow Query Log:**
|
|
559
|
+
- Log queries > 100ms
|
|
560
|
+
- Review monthly for optimization
|
|
561
|
+
|
|
562
|
+
**Common Optimizations:**
|
|
563
|
+
1. Use indices on WHERE clauses
|
|
564
|
+
2. Limit result sets with LIMIT
|
|
565
|
+
3. Use prepared statements
|
|
566
|
+
4. Batch inserts in transactions
|
|
567
|
+
5. Periodic VACUUM to reclaim space
|
|
568
|
+
|
|
569
|
+
### Size Management
|
|
570
|
+
|
|
571
|
+
**Estimated Growth:**
|
|
572
|
+
- 100 messages/day = ~50KB/day = ~18MB/year
|
|
573
|
+
- 1000 messages/day = ~500KB/day = ~180MB/year
|
|
574
|
+
|
|
575
|
+
**Cleanup Strategies:**
|
|
576
|
+
1. Archive old messages (> 1 year) to separate DB
|
|
577
|
+
2. Delete archived messages after 5 years
|
|
578
|
+
3. Compress attachments
|
|
579
|
+
4. Purge deleted messages permanently
|
|
580
|
+
|
|
581
|
+
---
|
|
582
|
+
|
|
583
|
+
## Security
|
|
584
|
+
|
|
585
|
+
### Encryption
|
|
586
|
+
|
|
587
|
+
**Encrypted Fields:**
|
|
588
|
+
- `sources.config` (credentials)
|
|
589
|
+
- `settings.encryption_key` (master key)
|
|
590
|
+
|
|
591
|
+
**Encryption Method:**
|
|
592
|
+
- AES-256-GCM
|
|
593
|
+
- Key derived from user password via PBKDF2
|
|
594
|
+
- Stored in system keychain (macOS/Linux)
|
|
595
|
+
|
|
596
|
+
**Implementation:**
|
|
597
|
+
|
|
598
|
+
```ruby
|
|
599
|
+
require 'openssl'
|
|
600
|
+
|
|
601
|
+
class Crypto
|
|
602
|
+
def self.encrypt(plaintext, key)
|
|
603
|
+
cipher = OpenSSL::Cipher.new('aes-256-gcm')
|
|
604
|
+
cipher.encrypt
|
|
605
|
+
cipher.key = key
|
|
606
|
+
iv = cipher.random_iv
|
|
607
|
+
encrypted = cipher.update(plaintext) + cipher.final
|
|
608
|
+
auth_tag = cipher.auth_tag
|
|
609
|
+
|
|
610
|
+
# Return: iv + auth_tag + encrypted
|
|
611
|
+
[iv, auth_tag, encrypted].map { |d| Base64.strict_encode64(d) }.join(':')
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def self.decrypt(ciphertext, key)
|
|
615
|
+
iv, auth_tag, encrypted = ciphertext.split(':').map { |d| Base64.strict_decode64(d) }
|
|
616
|
+
|
|
617
|
+
decipher = OpenSSL::Cipher.new('aes-256-gcm')
|
|
618
|
+
decipher.decrypt
|
|
619
|
+
decipher.key = key
|
|
620
|
+
decipher.iv = iv
|
|
621
|
+
decipher.auth_tag = auth_tag
|
|
622
|
+
|
|
623
|
+
decipher.update(encrypted) + decipher.final
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
### Access Control
|
|
629
|
+
|
|
630
|
+
- Database file permissions: `0600` (user read/write only)
|
|
631
|
+
- Config file permissions: `0600`
|
|
632
|
+
- Attachment directory: `0700`
|
|
633
|
+
|
|
634
|
+
---
|
|
635
|
+
|
|
636
|
+
## Testing Data
|
|
637
|
+
|
|
638
|
+
**Test Database:** `~/.heathrow/heathrow-test.db`
|
|
639
|
+
|
|
640
|
+
**Seed Script:** `lib/heathrow/seeds.rb`
|
|
641
|
+
|
|
642
|
+
```ruby
|
|
643
|
+
# Create test sources
|
|
644
|
+
gmail = Source.create(
|
|
645
|
+
name: "Test Gmail",
|
|
646
|
+
plugin_type: "gmail",
|
|
647
|
+
config: {email: "test@example.com"}.to_json,
|
|
648
|
+
capabilities: ["read", "write"].to_json
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
# Create test messages
|
|
652
|
+
100.times do |i|
|
|
653
|
+
Message.create(
|
|
654
|
+
source_id: gmail.id,
|
|
655
|
+
external_id: "msg-#{i}",
|
|
656
|
+
sender: "sender#{i % 10}@example.com",
|
|
657
|
+
recipients: ["you@example.com"].to_json,
|
|
658
|
+
subject: "Test message #{i}",
|
|
659
|
+
content: "This is test message number #{i}.",
|
|
660
|
+
timestamp: Time.now.to_i - (i * 3600),
|
|
661
|
+
received_at: Time.now.to_i
|
|
662
|
+
)
|
|
663
|
+
end
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
---
|
|
667
|
+
|
|
668
|
+
## Future Enhancements
|
|
669
|
+
|
|
670
|
+
1. **Message Sync State Table**
|
|
671
|
+
- Track sync progress per source
|
|
672
|
+
- Resume interrupted syncs
|
|
673
|
+
|
|
674
|
+
2. **Attachment Metadata Table**
|
|
675
|
+
- Separate table for attachment details
|
|
676
|
+
- Deduplication by hash
|
|
677
|
+
|
|
678
|
+
3. **Search History Table**
|
|
679
|
+
- Save frequently used searches
|
|
680
|
+
- Autocomplete suggestions
|
|
681
|
+
|
|
682
|
+
4. **Statistics Table**
|
|
683
|
+
- Daily/weekly/monthly stats
|
|
684
|
+
- Message counts by source
|
|
685
|
+
- Response time tracking
|