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.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +58 -0
  3. data/README.md +205 -0
  4. data/bin/heathrow +42 -0
  5. data/bin/heathrowd +283 -0
  6. data/docs/ARCHITECTURE.md +1172 -0
  7. data/docs/DATABASE_SCHEMA.md +685 -0
  8. data/docs/DEVELOPMENT_WORKFLOW.md +867 -0
  9. data/docs/DISCORD_SETUP.md +142 -0
  10. data/docs/GMAIL_OAUTH_SETUP.md +120 -0
  11. data/docs/PLUGIN_SYSTEM.md +1370 -0
  12. data/docs/PROJECT_PLAN.md +1022 -0
  13. data/docs/README.md +417 -0
  14. data/docs/REDDIT_SETUP.md +174 -0
  15. data/docs/REPLY_FORWARD.md +182 -0
  16. data/docs/WHATSAPP_TELEGRAM_SETUP.md +306 -0
  17. data/heathrow.gemspec +34 -0
  18. data/heathrowd.service +21 -0
  19. data/img/heathrow.svg +95 -0
  20. data/img/rss_threaded.png +0 -0
  21. data/img/sources.png +0 -0
  22. data/lib/heathrow/address_book.rb +42 -0
  23. data/lib/heathrow/config.rb +332 -0
  24. data/lib/heathrow/database.rb +731 -0
  25. data/lib/heathrow/database_new.rb +392 -0
  26. data/lib/heathrow/event_bus.rb +175 -0
  27. data/lib/heathrow/logger.rb +122 -0
  28. data/lib/heathrow/message.rb +176 -0
  29. data/lib/heathrow/message_composer.rb +399 -0
  30. data/lib/heathrow/message_organizer.rb +774 -0
  31. data/lib/heathrow/migrations/001_initial_schema.rb +248 -0
  32. data/lib/heathrow/notmuch.rb +45 -0
  33. data/lib/heathrow/oauth2_smtp.rb +254 -0
  34. data/lib/heathrow/plugin/base.rb +212 -0
  35. data/lib/heathrow/plugin_manager.rb +141 -0
  36. data/lib/heathrow/poller.rb +93 -0
  37. data/lib/heathrow/smtp_sender.rb +204 -0
  38. data/lib/heathrow/source.rb +39 -0
  39. data/lib/heathrow/sources/base.rb +74 -0
  40. data/lib/heathrow/sources/discord.rb +357 -0
  41. data/lib/heathrow/sources/gmail.rb +294 -0
  42. data/lib/heathrow/sources/imap.rb +198 -0
  43. data/lib/heathrow/sources/instagram.rb +307 -0
  44. data/lib/heathrow/sources/instagram_fetch.py +101 -0
  45. data/lib/heathrow/sources/instagram_send.py +55 -0
  46. data/lib/heathrow/sources/instagram_send_marionette.py +104 -0
  47. data/lib/heathrow/sources/maildir.rb +606 -0
  48. data/lib/heathrow/sources/messenger.rb +212 -0
  49. data/lib/heathrow/sources/messenger_fetch.js +297 -0
  50. data/lib/heathrow/sources/messenger_fetch_marionette.py +138 -0
  51. data/lib/heathrow/sources/messenger_send.js +32 -0
  52. data/lib/heathrow/sources/messenger_send.py +100 -0
  53. data/lib/heathrow/sources/reddit.rb +461 -0
  54. data/lib/heathrow/sources/rss.rb +299 -0
  55. data/lib/heathrow/sources/slack.rb +375 -0
  56. data/lib/heathrow/sources/source_manager.rb +328 -0
  57. data/lib/heathrow/sources/telegram.rb +498 -0
  58. data/lib/heathrow/sources/webpage.rb +207 -0
  59. data/lib/heathrow/sources/weechat.rb +479 -0
  60. data/lib/heathrow/sources/whatsapp.rb +474 -0
  61. data/lib/heathrow/ui/application.rb +8098 -0
  62. data/lib/heathrow/ui/navigation.rb +8 -0
  63. data/lib/heathrow/ui/panes.rb +8 -0
  64. data/lib/heathrow/ui/source_wizard.rb +567 -0
  65. data/lib/heathrow/ui/threaded_view.rb +780 -0
  66. data/lib/heathrow/ui/views.rb +8 -0
  67. data/lib/heathrow/version.rb +3 -0
  68. data/lib/heathrow/wizards/discord_wizard.rb +193 -0
  69. data/lib/heathrow/wizards/slack_wizard.rb +140 -0
  70. data/lib/heathrow.rb +55 -0
  71. 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