surface-cli 0.1.1 → 0.3.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 (41) hide show
  1. package/README.md +42 -15
  2. package/dist/cli.js +236 -21
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config.js +2 -2
  5. package/dist/config.js.map +1 -1
  6. package/dist/contracts/account.js +8 -0
  7. package/dist/contracts/account.js.map +1 -1
  8. package/dist/e2e/gmail-v1.js +31 -0
  9. package/dist/e2e/gmail-v1.js.map +1 -1
  10. package/dist/e2e/outlook-v1.js +11 -0
  11. package/dist/e2e/outlook-v1.js.map +1 -1
  12. package/dist/lib/remote-auth.js +18 -2
  13. package/dist/lib/remote-auth.js.map +1 -1
  14. package/dist/lib/stored-mail.js +69 -0
  15. package/dist/lib/stored-mail.js.map +1 -0
  16. package/dist/paths.js +2 -0
  17. package/dist/paths.js.map +1 -1
  18. package/dist/providers/gmail/adapter.js +336 -53
  19. package/dist/providers/gmail/adapter.js.map +1 -1
  20. package/dist/providers/gmail/api.js +68 -0
  21. package/dist/providers/gmail/api.js.map +1 -1
  22. package/dist/providers/gmail/normalize.js.map +1 -1
  23. package/dist/providers/gmail/oauth.js +4 -0
  24. package/dist/providers/gmail/oauth.js.map +1 -1
  25. package/dist/providers/outlook/adapter.js +185 -97
  26. package/dist/providers/outlook/adapter.js.map +1 -1
  27. package/dist/providers/outlook/extract.js +7 -0
  28. package/dist/providers/outlook/extract.js.map +1 -1
  29. package/dist/providers/shared/inline-attachments.js +17 -0
  30. package/dist/providers/shared/inline-attachments.js.map +1 -0
  31. package/dist/refs.js +3 -0
  32. package/dist/refs.js.map +1 -1
  33. package/dist/session-daemon.js +218 -0
  34. package/dist/session-daemon.js.map +1 -0
  35. package/dist/session.js +283 -0
  36. package/dist/session.js.map +1 -0
  37. package/dist/state/database.js +542 -8
  38. package/dist/state/database.js.map +1 -1
  39. package/dist/summarizer.js +259 -76
  40. package/dist/summarizer.js.map +1 -1
  41. package/package.json +1 -1
@@ -24,6 +24,19 @@ export class SurfaceDatabase {
24
24
  updated_at TEXT NOT NULL
25
25
  );
26
26
 
27
+ CREATE TABLE IF NOT EXISTS account_identities (
28
+ account_id TEXT PRIMARY KEY,
29
+ primary_email TEXT NOT NULL,
30
+ display_name TEXT,
31
+ email_aliases_json TEXT NOT NULL DEFAULT '[]',
32
+ name_aliases_json TEXT NOT NULL DEFAULT '[]',
33
+ primary_email_source TEXT NOT NULL DEFAULT 'configured',
34
+ display_name_source TEXT,
35
+ verified_at TEXT,
36
+ updated_at TEXT NOT NULL,
37
+ FOREIGN KEY(account_id) REFERENCES accounts(account_id) ON DELETE CASCADE
38
+ );
39
+
27
40
  CREATE TABLE IF NOT EXISTS threads (
28
41
  thread_ref TEXT PRIMARY KEY,
29
42
  account_id TEXT NOT NULL,
@@ -101,13 +114,34 @@ export class SurfaceDatabase {
101
114
  brief TEXT NOT NULL,
102
115
  needs_action INTEGER NOT NULL DEFAULT 0,
103
116
  importance TEXT NOT NULL,
117
+ fingerprint TEXT,
104
118
  generated_at TEXT NOT NULL,
105
119
  FOREIGN KEY(thread_ref) REFERENCES threads(thread_ref) ON DELETE CASCADE
106
120
  );
121
+
122
+ CREATE TABLE IF NOT EXISTS sessions (
123
+ session_id TEXT PRIMARY KEY,
124
+ account_id TEXT NOT NULL,
125
+ provider TEXT NOT NULL,
126
+ transport TEXT NOT NULL,
127
+ socket_path TEXT NOT NULL,
128
+ auth_token TEXT NOT NULL,
129
+ status TEXT NOT NULL,
130
+ pid INTEGER,
131
+ idle_timeout_seconds INTEGER NOT NULL,
132
+ max_age_seconds INTEGER NOT NULL,
133
+ error_detail TEXT,
134
+ created_at TEXT NOT NULL,
135
+ last_used_at TEXT NOT NULL,
136
+ closed_at TEXT,
137
+ FOREIGN KEY(account_id) REFERENCES accounts(account_id) ON DELETE CASCADE
138
+ );
107
139
  `);
108
140
  this.ensureColumn("threads", "participants_json", "TEXT NOT NULL DEFAULT '[]'");
109
141
  this.ensureColumn("messages", "invite_json", "TEXT");
142
+ this.ensureColumn("summaries", "fingerprint", "TEXT");
110
143
  this.ensureProviderLocatorSchema();
144
+ this.seedMissingAccountIdentities();
111
145
  }
112
146
  tableColumns(tableName) {
113
147
  return this.connection
@@ -158,6 +192,153 @@ export class SurfaceDatabase {
158
192
  }
159
193
  this.connection.exec("DROP TABLE IF EXISTS provider_locators_legacy;");
160
194
  }
195
+ parseStringArray(rawValue) {
196
+ try {
197
+ const parsed = JSON.parse(rawValue);
198
+ return Array.isArray(parsed)
199
+ ? parsed.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
200
+ : [];
201
+ }
202
+ catch {
203
+ return [];
204
+ }
205
+ }
206
+ normalizeEmailList(values) {
207
+ const seen = new Set();
208
+ const normalized = [];
209
+ for (const value of values) {
210
+ const candidate = value.trim().toLowerCase();
211
+ if (!candidate || seen.has(candidate)) {
212
+ continue;
213
+ }
214
+ seen.add(candidate);
215
+ normalized.push(candidate);
216
+ }
217
+ return normalized;
218
+ }
219
+ isPlaceholderEmail(value) {
220
+ return value.trim().toLowerCase().endsWith("@placeholder.local");
221
+ }
222
+ normalizeNameList(values) {
223
+ const seen = new Set();
224
+ const normalized = [];
225
+ for (const value of values) {
226
+ const candidate = value.trim().replace(/\s+/g, " ");
227
+ const key = candidate.toLowerCase();
228
+ if (!candidate || seen.has(key)) {
229
+ continue;
230
+ }
231
+ seen.add(key);
232
+ normalized.push(candidate);
233
+ }
234
+ return normalized;
235
+ }
236
+ rowToAccountIdentity(row) {
237
+ return {
238
+ account_id: row.account_id,
239
+ primary_email: row.primary_email,
240
+ display_name: row.display_name,
241
+ email_aliases: this.parseStringArray(row.email_aliases_json),
242
+ name_aliases: this.parseStringArray(row.name_aliases_json),
243
+ primary_email_source: row.primary_email_source,
244
+ display_name_source: row.display_name_source,
245
+ verified_at: row.verified_at,
246
+ updated_at: row.updated_at,
247
+ };
248
+ }
249
+ seedMissingAccountIdentities() {
250
+ const timestamp = nowIsoUtc();
251
+ this.connection
252
+ .prepare(`
253
+ INSERT OR IGNORE INTO account_identities (
254
+ account_id,
255
+ primary_email,
256
+ email_aliases_json,
257
+ name_aliases_json,
258
+ primary_email_source,
259
+ display_name_source,
260
+ verified_at,
261
+ updated_at
262
+ )
263
+ SELECT
264
+ account_id,
265
+ lower(email),
266
+ '[]',
267
+ '[]',
268
+ 'configured',
269
+ NULL,
270
+ NULL,
271
+ @updated_at
272
+ FROM accounts
273
+ `)
274
+ .run({ updated_at: timestamp });
275
+ }
276
+ seedOrUpdateConfiguredIdentity(account) {
277
+ const existing = this.findAccountIdentity(account);
278
+ const timestamp = nowIsoUtc();
279
+ if (!existing) {
280
+ this.connection
281
+ .prepare(`
282
+ INSERT INTO account_identities (
283
+ account_id,
284
+ primary_email,
285
+ email_aliases_json,
286
+ name_aliases_json,
287
+ primary_email_source,
288
+ display_name_source,
289
+ verified_at,
290
+ updated_at
291
+ ) VALUES (
292
+ @account_id,
293
+ @primary_email,
294
+ '[]',
295
+ '[]',
296
+ 'configured',
297
+ NULL,
298
+ NULL,
299
+ @updated_at
300
+ )
301
+ `)
302
+ .run({
303
+ account_id: account.account_id,
304
+ primary_email: account.email.trim().toLowerCase(),
305
+ updated_at: timestamp,
306
+ });
307
+ return;
308
+ }
309
+ if (existing.primary_email_source !== "configured") {
310
+ return;
311
+ }
312
+ const primaryEmail = account.email.trim().toLowerCase();
313
+ if (existing.primary_email === primaryEmail) {
314
+ return;
315
+ }
316
+ this.connection
317
+ .prepare(`
318
+ UPDATE account_identities
319
+ SET primary_email = @primary_email,
320
+ updated_at = @updated_at
321
+ WHERE account_id = @account_id
322
+ `)
323
+ .run({
324
+ account_id: account.account_id,
325
+ primary_email: primaryEmail,
326
+ updated_at: timestamp,
327
+ });
328
+ this.clearSummariesForAccount(account.account_id);
329
+ }
330
+ clearSummariesForAccount(accountId) {
331
+ this.connection
332
+ .prepare(`
333
+ DELETE FROM summaries
334
+ WHERE thread_ref IN (
335
+ SELECT thread_ref
336
+ FROM threads
337
+ WHERE account_id = ?
338
+ )
339
+ `)
340
+ .run(accountId);
341
+ }
161
342
  upsertAccount(input) {
162
343
  const existing = this.findAccountByName(input.name);
163
344
  const timestamp = nowIsoUtc();
@@ -178,7 +359,9 @@ export class SurfaceDatabase {
178
359
  email: input.email,
179
360
  updated_at: timestamp,
180
361
  });
181
- return this.findAccountByName(input.name);
362
+ const account = this.findAccountByName(input.name);
363
+ this.seedOrUpdateConfiguredIdentity(account);
364
+ return account;
182
365
  }
183
366
  const account = {
184
367
  account_id: makeAccountId(),
@@ -210,8 +393,144 @@ export class SurfaceDatabase {
210
393
  )
211
394
  `)
212
395
  .run(account);
396
+ this.seedOrUpdateConfiguredIdentity(account);
213
397
  return account;
214
398
  }
399
+ findAccountIdentity(account) {
400
+ const row = this.connection
401
+ .prepare(`
402
+ SELECT
403
+ account_id,
404
+ primary_email,
405
+ display_name,
406
+ email_aliases_json,
407
+ name_aliases_json,
408
+ primary_email_source,
409
+ display_name_source,
410
+ verified_at,
411
+ updated_at
412
+ FROM account_identities
413
+ WHERE account_id = ?
414
+ LIMIT 1
415
+ `)
416
+ .get(account.account_id);
417
+ return row ? this.rowToAccountIdentity(row) : undefined;
418
+ }
419
+ getAccountIdentity(account) {
420
+ const existing = this.findAccountIdentity(account);
421
+ if (existing) {
422
+ return existing;
423
+ }
424
+ this.seedOrUpdateConfiguredIdentity(account);
425
+ const seeded = this.findAccountIdentity(account);
426
+ if (!seeded) {
427
+ throw new Error(`Account identity could not be created for account '${account.name}'.`);
428
+ }
429
+ return seeded;
430
+ }
431
+ updateAccountIdentityFromUser(account, input) {
432
+ const existing = this.getAccountIdentity(account);
433
+ const timestamp = nowIsoUtc();
434
+ const primaryEmail = input.primary_email?.trim().toLowerCase() ?? existing.primary_email;
435
+ const displayName = input.display_name?.trim().replace(/\s+/g, " ") ?? existing.display_name;
436
+ const emailAliases = this.normalizeEmailList([
437
+ ...(input.clear_email_aliases ? [] : existing.email_aliases),
438
+ ...(input.email_aliases ?? []),
439
+ ].filter((email) => email.trim().toLowerCase() !== primaryEmail));
440
+ const nameAliases = this.normalizeNameList([
441
+ ...(input.clear_name_aliases ? [] : existing.name_aliases),
442
+ ...(input.name_aliases ?? []),
443
+ ].filter((name) => name.trim().replace(/\s+/g, " ") !== displayName));
444
+ this.connection
445
+ .prepare(`
446
+ UPDATE account_identities
447
+ SET primary_email = @primary_email,
448
+ display_name = @display_name,
449
+ email_aliases_json = @email_aliases_json,
450
+ name_aliases_json = @name_aliases_json,
451
+ primary_email_source = @primary_email_source,
452
+ display_name_source = @display_name_source,
453
+ verified_at = @verified_at,
454
+ updated_at = @updated_at
455
+ WHERE account_id = @account_id
456
+ `)
457
+ .run({
458
+ account_id: account.account_id,
459
+ primary_email: primaryEmail,
460
+ display_name: displayName,
461
+ email_aliases_json: JSON.stringify(emailAliases),
462
+ name_aliases_json: JSON.stringify(nameAliases),
463
+ primary_email_source: input.primary_email ? "user_confirmed" : existing.primary_email_source,
464
+ display_name_source: input.display_name ? "user_confirmed" : existing.display_name_source,
465
+ verified_at: input.primary_email ? timestamp : existing.verified_at,
466
+ updated_at: timestamp,
467
+ });
468
+ if (input.primary_email) {
469
+ this.connection
470
+ .prepare(`
471
+ UPDATE accounts
472
+ SET email = @email,
473
+ updated_at = @updated_at
474
+ WHERE account_id = @account_id
475
+ `)
476
+ .run({
477
+ account_id: account.account_id,
478
+ email: primaryEmail,
479
+ updated_at: timestamp,
480
+ });
481
+ }
482
+ this.clearSummariesForAccount(account.account_id);
483
+ return this.getAccountIdentity(account);
484
+ }
485
+ updateAccountIdentityFromProvider(account, authenticatedEmail) {
486
+ const existing = this.getAccountIdentity(account);
487
+ const timestamp = nowIsoUtc();
488
+ const primaryEmail = authenticatedEmail.trim().toLowerCase();
489
+ const emailAliases = this.normalizeEmailList([
490
+ ...existing.email_aliases,
491
+ ...(existing.primary_email
492
+ && existing.primary_email !== primaryEmail
493
+ && !this.isPlaceholderEmail(existing.primary_email)
494
+ ? [existing.primary_email]
495
+ : []),
496
+ ].filter((email) => email.trim().toLowerCase() !== primaryEmail));
497
+ const identityChanged = primaryEmail !== existing.primary_email
498
+ || existing.primary_email_source !== "provider_verified"
499
+ || JSON.stringify(emailAliases) !== JSON.stringify(existing.email_aliases);
500
+ this.connection
501
+ .prepare(`
502
+ UPDATE account_identities
503
+ SET primary_email = @primary_email,
504
+ email_aliases_json = @email_aliases_json,
505
+ primary_email_source = 'provider_verified',
506
+ verified_at = @verified_at,
507
+ updated_at = @updated_at
508
+ WHERE account_id = @account_id
509
+ `)
510
+ .run({
511
+ account_id: account.account_id,
512
+ primary_email: primaryEmail,
513
+ email_aliases_json: JSON.stringify(emailAliases),
514
+ verified_at: timestamp,
515
+ updated_at: timestamp,
516
+ });
517
+ this.connection
518
+ .prepare(`
519
+ UPDATE accounts
520
+ SET email = @email,
521
+ updated_at = @updated_at
522
+ WHERE account_id = @account_id
523
+ `)
524
+ .run({
525
+ account_id: account.account_id,
526
+ email: primaryEmail,
527
+ updated_at: timestamp,
528
+ });
529
+ if (identityChanged) {
530
+ this.clearSummariesForAccount(account.account_id);
531
+ }
532
+ return this.getAccountIdentity({ ...account, email: primaryEmail });
533
+ }
215
534
  listAccounts() {
216
535
  return this.connection
217
536
  .prepare(`
@@ -509,7 +828,7 @@ export class SurfaceDatabase {
509
828
  `)
510
829
  .run(savedTo, attachmentId);
511
830
  }
512
- upsertSummary(threadRef, summary) {
831
+ upsertSummary(threadRef, summary, fingerprint) {
513
832
  this.connection
514
833
  .prepare(`
515
834
  INSERT INTO summaries (
@@ -519,6 +838,7 @@ export class SurfaceDatabase {
519
838
  brief,
520
839
  needs_action,
521
840
  importance,
841
+ fingerprint,
522
842
  generated_at
523
843
  ) VALUES (
524
844
  @thread_ref,
@@ -527,6 +847,7 @@ export class SurfaceDatabase {
527
847
  @brief,
528
848
  @needs_action,
529
849
  @importance,
850
+ @fingerprint,
530
851
  @generated_at
531
852
  )
532
853
  ON CONFLICT(thread_ref) DO UPDATE SET
@@ -535,6 +856,7 @@ export class SurfaceDatabase {
535
856
  brief = excluded.brief,
536
857
  needs_action = excluded.needs_action,
537
858
  importance = excluded.importance,
859
+ fingerprint = excluded.fingerprint,
538
860
  generated_at = excluded.generated_at
539
861
  `)
540
862
  .run({
@@ -544,13 +866,22 @@ export class SurfaceDatabase {
544
866
  brief: summary.brief,
545
867
  needs_action: summary.needs_action ? 1 : 0,
546
868
  importance: summary.importance,
869
+ fingerprint,
547
870
  generated_at: nowIsoUtc(),
548
871
  });
549
872
  }
550
- findSummary(threadRef) {
873
+ clearSummary(threadRef) {
874
+ this.connection
875
+ .prepare(`
876
+ DELETE FROM summaries
877
+ WHERE thread_ref = ?
878
+ `)
879
+ .run(threadRef);
880
+ }
881
+ findStoredSummary(threadRef) {
551
882
  const row = this.connection
552
883
  .prepare(`
553
- SELECT backend, model, brief, needs_action, importance
884
+ SELECT thread_ref, backend, model, brief, needs_action, importance, fingerprint
554
885
  FROM summaries
555
886
  WHERE thread_ref = ?
556
887
  LIMIT 1
@@ -559,6 +890,151 @@ export class SurfaceDatabase {
559
890
  if (!row) {
560
891
  return null;
561
892
  }
893
+ return row;
894
+ }
895
+ createSession(input) {
896
+ const timestamp = nowIsoUtc();
897
+ this.connection
898
+ .prepare(`
899
+ INSERT INTO sessions (
900
+ session_id,
901
+ account_id,
902
+ provider,
903
+ transport,
904
+ socket_path,
905
+ auth_token,
906
+ status,
907
+ pid,
908
+ idle_timeout_seconds,
909
+ max_age_seconds,
910
+ error_detail,
911
+ created_at,
912
+ last_used_at,
913
+ closed_at
914
+ ) VALUES (
915
+ @session_id,
916
+ @account_id,
917
+ @provider,
918
+ @transport,
919
+ @socket_path,
920
+ @auth_token,
921
+ 'starting',
922
+ NULL,
923
+ @idle_timeout_seconds,
924
+ @max_age_seconds,
925
+ NULL,
926
+ @created_at,
927
+ @last_used_at,
928
+ NULL
929
+ )
930
+ `)
931
+ .run({
932
+ ...input,
933
+ created_at: timestamp,
934
+ last_used_at: timestamp,
935
+ });
936
+ return this.getSession(input.session_id);
937
+ }
938
+ getSession(sessionId) {
939
+ return this.connection
940
+ .prepare(`
941
+ SELECT
942
+ session_id,
943
+ account_id,
944
+ provider,
945
+ transport,
946
+ socket_path,
947
+ auth_token,
948
+ status,
949
+ pid,
950
+ idle_timeout_seconds,
951
+ max_age_seconds,
952
+ error_detail,
953
+ created_at,
954
+ last_used_at,
955
+ closed_at
956
+ FROM sessions
957
+ WHERE session_id = ?
958
+ LIMIT 1
959
+ `)
960
+ .get(sessionId);
961
+ }
962
+ listSessions() {
963
+ return this.connection
964
+ .prepare(`
965
+ SELECT
966
+ session_id,
967
+ account_id,
968
+ provider,
969
+ transport,
970
+ socket_path,
971
+ auth_token,
972
+ status,
973
+ pid,
974
+ idle_timeout_seconds,
975
+ max_age_seconds,
976
+ error_detail,
977
+ created_at,
978
+ last_used_at,
979
+ closed_at
980
+ FROM sessions
981
+ ORDER BY created_at DESC
982
+ `)
983
+ .all();
984
+ }
985
+ markSessionRunning(sessionId, pid) {
986
+ this.connection
987
+ .prepare(`
988
+ UPDATE sessions
989
+ SET status = 'running',
990
+ pid = ?,
991
+ error_detail = NULL,
992
+ closed_at = NULL
993
+ WHERE session_id = ?
994
+ `)
995
+ .run(pid, sessionId);
996
+ }
997
+ updateSessionProcessInfo(sessionId, pid) {
998
+ this.connection
999
+ .prepare(`
1000
+ UPDATE sessions
1001
+ SET pid = ?
1002
+ WHERE session_id = ?
1003
+ `)
1004
+ .run(pid, sessionId);
1005
+ }
1006
+ touchSession(sessionId) {
1007
+ this.connection
1008
+ .prepare(`
1009
+ UPDATE sessions
1010
+ SET last_used_at = ?
1011
+ WHERE session_id = ?
1012
+ `)
1013
+ .run(nowIsoUtc(), sessionId);
1014
+ }
1015
+ markSessionClosed(sessionId, status, options = {}) {
1016
+ this.connection
1017
+ .prepare(`
1018
+ UPDATE sessions
1019
+ SET status = @status,
1020
+ error_detail = @error_detail,
1021
+ pid = @pid,
1022
+ closed_at = @closed_at
1023
+ WHERE session_id = @session_id
1024
+ `)
1025
+ .run({
1026
+ session_id: sessionId,
1027
+ status,
1028
+ error_detail: options.errorDetail ?? null,
1029
+ pid: options.pid ?? null,
1030
+ closed_at: nowIsoUtc(),
1031
+ });
1032
+ }
1033
+ findSummary(threadRef) {
1034
+ const row = this.findStoredSummary(threadRef);
1035
+ if (!row) {
1036
+ return null;
1037
+ }
562
1038
  return {
563
1039
  backend: row.backend,
564
1040
  model: row.model,
@@ -604,7 +1080,30 @@ export class SurfaceDatabase {
604
1080
  `)
605
1081
  .get(messageRef);
606
1082
  }
1083
+ getStoredThread(threadRef) {
1084
+ return this.connection
1085
+ .prepare(`
1086
+ SELECT
1087
+ thread_ref,
1088
+ account_id,
1089
+ subject,
1090
+ mailbox,
1091
+ participants_json,
1092
+ labels_json,
1093
+ received_at,
1094
+ message_count,
1095
+ unread_count,
1096
+ has_attachments
1097
+ FROM threads
1098
+ WHERE thread_ref = ?
1099
+ LIMIT 1
1100
+ `)
1101
+ .get(threadRef);
1102
+ }
607
1103
  updateInviteStatusForThread(threadRef, responseStatus) {
1104
+ this.updateInviteForThread(threadRef, { response_status: responseStatus });
1105
+ }
1106
+ updateInviteForThread(threadRef, patch) {
608
1107
  const rows = this.connection
609
1108
  .prepare(`
610
1109
  SELECT message_ref, invite_json
@@ -627,10 +1126,7 @@ export class SurfaceDatabase {
627
1126
  const invite = JSON.parse(row.invite_json);
628
1127
  update.run({
629
1128
  message_ref: row.message_ref,
630
- invite_json: JSON.stringify({
631
- ...invite,
632
- response_status: responseStatus,
633
- }),
1129
+ invite_json: JSON.stringify({ ...invite, ...patch }),
634
1130
  last_synced_at: lastSyncedAt,
635
1131
  });
636
1132
  }
@@ -645,6 +1141,34 @@ export class SurfaceDatabase {
645
1141
  `)
646
1142
  .all(threadRef).map((row) => row.message_ref);
647
1143
  }
1144
+ listStoredMessagesForThread(threadRef) {
1145
+ return this.connection
1146
+ .prepare(`
1147
+ SELECT
1148
+ m.message_ref,
1149
+ m.account_id,
1150
+ m.thread_ref,
1151
+ m.subject,
1152
+ m.from_name,
1153
+ m.from_email,
1154
+ m.to_json,
1155
+ m.cc_json,
1156
+ m.sent_at,
1157
+ m.received_at,
1158
+ m.unread,
1159
+ m.snippet,
1160
+ m.body_cache_path,
1161
+ m.body_cached,
1162
+ m.body_truncated,
1163
+ m.body_cached_bytes,
1164
+ m.invite_json
1165
+ FROM thread_messages tm
1166
+ INNER JOIN messages m ON m.message_ref = tm.message_ref
1167
+ WHERE tm.thread_ref = ?
1168
+ ORDER BY tm.position ASC
1169
+ `)
1170
+ .all(threadRef);
1171
+ }
648
1172
  markThreadArchived(threadRef) {
649
1173
  this.connection
650
1174
  .prepare(`
@@ -717,6 +1241,16 @@ export class SurfaceDatabase {
717
1241
  `)
718
1242
  .get(messageRef);
719
1243
  }
1244
+ findThreadByRef(threadRef) {
1245
+ return this.connection
1246
+ .prepare(`
1247
+ SELECT thread_ref, account_id
1248
+ FROM threads
1249
+ WHERE thread_ref = ?
1250
+ LIMIT 1
1251
+ `)
1252
+ .get(threadRef);
1253
+ }
720
1254
  findAttachmentById(attachmentId) {
721
1255
  return this.connection
722
1256
  .prepare(`