surface-cli 0.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/README.md +307 -0
- package/dist/cli.js +521 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.js +156 -0
- package/dist/config.js.map +1 -0
- package/dist/contracts/account.js +9 -0
- package/dist/contracts/account.js.map +1 -0
- package/dist/contracts/mail.js +2 -0
- package/dist/contracts/mail.js.map +1 -0
- package/dist/e2e/gmail-v1.js +247 -0
- package/dist/e2e/gmail-v1.js.map +1 -0
- package/dist/e2e/outlook-v1.js +179 -0
- package/dist/e2e/outlook-v1.js.map +1 -0
- package/dist/lib/errors.js +47 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/json.js +4 -0
- package/dist/lib/json.js.map +1 -0
- package/dist/lib/public-mail.js +29 -0
- package/dist/lib/public-mail.js.map +1 -0
- package/dist/lib/remote-auth.js +407 -0
- package/dist/lib/remote-auth.js.map +1 -0
- package/dist/lib/time.js +4 -0
- package/dist/lib/time.js.map +1 -0
- package/dist/lib/write-safety.js +34 -0
- package/dist/lib/write-safety.js.map +1 -0
- package/dist/paths.js +29 -0
- package/dist/paths.js.map +1 -0
- package/dist/providers/gmail/adapter.js +1102 -0
- package/dist/providers/gmail/adapter.js.map +1 -0
- package/dist/providers/gmail/api.js +99 -0
- package/dist/providers/gmail/api.js.map +1 -0
- package/dist/providers/gmail/normalize.js +336 -0
- package/dist/providers/gmail/normalize.js.map +1 -0
- package/dist/providers/gmail/oauth.js +328 -0
- package/dist/providers/gmail/oauth.js.map +1 -0
- package/dist/providers/index.js +12 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/outlook/adapter.js +1443 -0
- package/dist/providers/outlook/adapter.js.map +1 -0
- package/dist/providers/outlook/extract.js +416 -0
- package/dist/providers/outlook/extract.js.map +1 -0
- package/dist/providers/outlook/normalize.js +126 -0
- package/dist/providers/outlook/normalize.js.map +1 -0
- package/dist/providers/outlook/session.js +178 -0
- package/dist/providers/outlook/session.js.map +1 -0
- package/dist/providers/shared/html.js +88 -0
- package/dist/providers/shared/html.js.map +1 -0
- package/dist/providers/shared/types.js +2 -0
- package/dist/providers/shared/types.js.map +1 -0
- package/dist/providers/types.js +2 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/refs.js +18 -0
- package/dist/refs.js.map +1 -0
- package/dist/runtime.js +23 -0
- package/dist/runtime.js.map +1 -0
- package/dist/state/database.js +731 -0
- package/dist/state/database.js.map +1 -0
- package/dist/summarizer.js +217 -0
- package/dist/summarizer.js.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { makeAccountId } from "../refs.js";
|
|
3
|
+
import { nowIsoUtc } from "../lib/time.js";
|
|
4
|
+
export class SurfaceDatabase {
|
|
5
|
+
connection;
|
|
6
|
+
constructor(path) {
|
|
7
|
+
this.connection = new Database(path);
|
|
8
|
+
this.connection.pragma("journal_mode = WAL");
|
|
9
|
+
this.connection.pragma("foreign_keys = ON");
|
|
10
|
+
this.migrate();
|
|
11
|
+
}
|
|
12
|
+
close() {
|
|
13
|
+
this.connection.close();
|
|
14
|
+
}
|
|
15
|
+
migrate() {
|
|
16
|
+
this.connection.exec(`
|
|
17
|
+
CREATE TABLE IF NOT EXISTS accounts (
|
|
18
|
+
account_id TEXT PRIMARY KEY,
|
|
19
|
+
name TEXT NOT NULL UNIQUE,
|
|
20
|
+
provider TEXT NOT NULL,
|
|
21
|
+
transport TEXT NOT NULL,
|
|
22
|
+
email TEXT NOT NULL,
|
|
23
|
+
created_at TEXT NOT NULL,
|
|
24
|
+
updated_at TEXT NOT NULL
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
CREATE TABLE IF NOT EXISTS threads (
|
|
28
|
+
thread_ref TEXT PRIMARY KEY,
|
|
29
|
+
account_id TEXT NOT NULL,
|
|
30
|
+
subject TEXT,
|
|
31
|
+
mailbox TEXT,
|
|
32
|
+
participants_json TEXT NOT NULL DEFAULT '[]',
|
|
33
|
+
labels_json TEXT NOT NULL DEFAULT '[]',
|
|
34
|
+
received_at TEXT,
|
|
35
|
+
message_count INTEGER NOT NULL DEFAULT 0,
|
|
36
|
+
unread_count INTEGER NOT NULL DEFAULT 0,
|
|
37
|
+
has_attachments INTEGER NOT NULL DEFAULT 0,
|
|
38
|
+
first_seen_at TEXT NOT NULL,
|
|
39
|
+
last_synced_at TEXT NOT NULL,
|
|
40
|
+
FOREIGN KEY(account_id) REFERENCES accounts(account_id) ON DELETE CASCADE
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
44
|
+
message_ref TEXT PRIMARY KEY,
|
|
45
|
+
account_id TEXT NOT NULL,
|
|
46
|
+
thread_ref TEXT NOT NULL,
|
|
47
|
+
subject TEXT,
|
|
48
|
+
from_name TEXT,
|
|
49
|
+
from_email TEXT,
|
|
50
|
+
to_json TEXT NOT NULL DEFAULT '[]',
|
|
51
|
+
cc_json TEXT NOT NULL DEFAULT '[]',
|
|
52
|
+
sent_at TEXT,
|
|
53
|
+
received_at TEXT,
|
|
54
|
+
unread INTEGER NOT NULL DEFAULT 0,
|
|
55
|
+
snippet TEXT NOT NULL DEFAULT '',
|
|
56
|
+
body_cache_path TEXT,
|
|
57
|
+
body_cached INTEGER NOT NULL DEFAULT 0,
|
|
58
|
+
body_truncated INTEGER NOT NULL DEFAULT 0,
|
|
59
|
+
body_cached_bytes INTEGER NOT NULL DEFAULT 0,
|
|
60
|
+
invite_json TEXT,
|
|
61
|
+
first_seen_at TEXT NOT NULL,
|
|
62
|
+
last_synced_at TEXT NOT NULL,
|
|
63
|
+
FOREIGN KEY(account_id) REFERENCES accounts(account_id) ON DELETE CASCADE,
|
|
64
|
+
FOREIGN KEY(thread_ref) REFERENCES threads(thread_ref) ON DELETE CASCADE
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
CREATE TABLE IF NOT EXISTS thread_messages (
|
|
68
|
+
thread_ref TEXT NOT NULL,
|
|
69
|
+
message_ref TEXT NOT NULL,
|
|
70
|
+
position INTEGER NOT NULL,
|
|
71
|
+
PRIMARY KEY(thread_ref, message_ref),
|
|
72
|
+
FOREIGN KEY(thread_ref) REFERENCES threads(thread_ref) ON DELETE CASCADE,
|
|
73
|
+
FOREIGN KEY(message_ref) REFERENCES messages(message_ref) ON DELETE CASCADE
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
CREATE TABLE IF NOT EXISTS attachments (
|
|
77
|
+
attachment_id TEXT PRIMARY KEY,
|
|
78
|
+
message_ref TEXT NOT NULL,
|
|
79
|
+
filename TEXT NOT NULL,
|
|
80
|
+
mime_type TEXT NOT NULL,
|
|
81
|
+
size_bytes INTEGER,
|
|
82
|
+
inline INTEGER NOT NULL DEFAULT 0,
|
|
83
|
+
saved_to TEXT,
|
|
84
|
+
FOREIGN KEY(message_ref) REFERENCES messages(message_ref) ON DELETE CASCADE
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
CREATE TABLE IF NOT EXISTS provider_locators (
|
|
88
|
+
entity_kind TEXT NOT NULL,
|
|
89
|
+
entity_ref TEXT NOT NULL,
|
|
90
|
+
account_id TEXT NOT NULL,
|
|
91
|
+
provider_key TEXT NOT NULL,
|
|
92
|
+
locator_json TEXT NOT NULL,
|
|
93
|
+
PRIMARY KEY(entity_kind, entity_ref),
|
|
94
|
+
UNIQUE(entity_kind, account_id, provider_key)
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
CREATE TABLE IF NOT EXISTS summaries (
|
|
98
|
+
thread_ref TEXT PRIMARY KEY,
|
|
99
|
+
backend TEXT NOT NULL,
|
|
100
|
+
model TEXT NOT NULL,
|
|
101
|
+
brief TEXT NOT NULL,
|
|
102
|
+
needs_action INTEGER NOT NULL DEFAULT 0,
|
|
103
|
+
importance TEXT NOT NULL,
|
|
104
|
+
generated_at TEXT NOT NULL,
|
|
105
|
+
FOREIGN KEY(thread_ref) REFERENCES threads(thread_ref) ON DELETE CASCADE
|
|
106
|
+
);
|
|
107
|
+
`);
|
|
108
|
+
this.ensureColumn("threads", "participants_json", "TEXT NOT NULL DEFAULT '[]'");
|
|
109
|
+
this.ensureColumn("messages", "invite_json", "TEXT");
|
|
110
|
+
this.ensureProviderLocatorSchema();
|
|
111
|
+
}
|
|
112
|
+
tableColumns(tableName) {
|
|
113
|
+
return this.connection
|
|
114
|
+
.prepare(`PRAGMA table_info(${tableName})`)
|
|
115
|
+
.all().map((column) => column.name);
|
|
116
|
+
}
|
|
117
|
+
ensureColumn(tableName, columnName, definition) {
|
|
118
|
+
if (this.tableColumns(tableName).includes(columnName)) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
this.connection.exec(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${definition}`);
|
|
122
|
+
}
|
|
123
|
+
ensureProviderLocatorSchema() {
|
|
124
|
+
const columns = this.tableColumns("provider_locators");
|
|
125
|
+
if (columns.includes("account_id") && columns.includes("provider_key")) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
this.connection.exec(`
|
|
129
|
+
ALTER TABLE provider_locators RENAME TO provider_locators_legacy;
|
|
130
|
+
|
|
131
|
+
CREATE TABLE provider_locators (
|
|
132
|
+
entity_kind TEXT NOT NULL,
|
|
133
|
+
entity_ref TEXT NOT NULL,
|
|
134
|
+
account_id TEXT NOT NULL,
|
|
135
|
+
provider_key TEXT NOT NULL,
|
|
136
|
+
locator_json TEXT NOT NULL,
|
|
137
|
+
PRIMARY KEY(entity_kind, entity_ref),
|
|
138
|
+
UNIQUE(entity_kind, account_id, provider_key)
|
|
139
|
+
);
|
|
140
|
+
`);
|
|
141
|
+
if (columns.length > 0) {
|
|
142
|
+
this.connection.exec(`
|
|
143
|
+
INSERT OR IGNORE INTO provider_locators (
|
|
144
|
+
entity_kind,
|
|
145
|
+
entity_ref,
|
|
146
|
+
account_id,
|
|
147
|
+
provider_key,
|
|
148
|
+
locator_json
|
|
149
|
+
)
|
|
150
|
+
SELECT
|
|
151
|
+
entity_kind,
|
|
152
|
+
entity_ref,
|
|
153
|
+
'',
|
|
154
|
+
entity_ref,
|
|
155
|
+
locator_json
|
|
156
|
+
FROM provider_locators_legacy;
|
|
157
|
+
`);
|
|
158
|
+
}
|
|
159
|
+
this.connection.exec("DROP TABLE IF EXISTS provider_locators_legacy;");
|
|
160
|
+
}
|
|
161
|
+
upsertAccount(input) {
|
|
162
|
+
const existing = this.findAccountByName(input.name);
|
|
163
|
+
const timestamp = nowIsoUtc();
|
|
164
|
+
if (existing) {
|
|
165
|
+
this.connection
|
|
166
|
+
.prepare(`
|
|
167
|
+
UPDATE accounts
|
|
168
|
+
SET provider = @provider,
|
|
169
|
+
transport = @transport,
|
|
170
|
+
email = @email,
|
|
171
|
+
updated_at = @updated_at
|
|
172
|
+
WHERE name = @name
|
|
173
|
+
`)
|
|
174
|
+
.run({
|
|
175
|
+
name: input.name,
|
|
176
|
+
provider: input.provider,
|
|
177
|
+
transport: input.transport,
|
|
178
|
+
email: input.email,
|
|
179
|
+
updated_at: timestamp,
|
|
180
|
+
});
|
|
181
|
+
return this.findAccountByName(input.name);
|
|
182
|
+
}
|
|
183
|
+
const account = {
|
|
184
|
+
account_id: makeAccountId(),
|
|
185
|
+
name: input.name,
|
|
186
|
+
provider: input.provider,
|
|
187
|
+
transport: input.transport,
|
|
188
|
+
email: input.email,
|
|
189
|
+
created_at: timestamp,
|
|
190
|
+
updated_at: timestamp,
|
|
191
|
+
};
|
|
192
|
+
this.connection
|
|
193
|
+
.prepare(`
|
|
194
|
+
INSERT INTO accounts (
|
|
195
|
+
account_id,
|
|
196
|
+
name,
|
|
197
|
+
provider,
|
|
198
|
+
transport,
|
|
199
|
+
email,
|
|
200
|
+
created_at,
|
|
201
|
+
updated_at
|
|
202
|
+
) VALUES (
|
|
203
|
+
@account_id,
|
|
204
|
+
@name,
|
|
205
|
+
@provider,
|
|
206
|
+
@transport,
|
|
207
|
+
@email,
|
|
208
|
+
@created_at,
|
|
209
|
+
@updated_at
|
|
210
|
+
)
|
|
211
|
+
`)
|
|
212
|
+
.run(account);
|
|
213
|
+
return account;
|
|
214
|
+
}
|
|
215
|
+
listAccounts() {
|
|
216
|
+
return this.connection
|
|
217
|
+
.prepare(`
|
|
218
|
+
SELECT account_id, name, provider, transport, email, created_at, updated_at
|
|
219
|
+
FROM accounts
|
|
220
|
+
ORDER BY name ASC
|
|
221
|
+
`)
|
|
222
|
+
.all();
|
|
223
|
+
}
|
|
224
|
+
findAccountByName(name) {
|
|
225
|
+
return this.connection
|
|
226
|
+
.prepare(`
|
|
227
|
+
SELECT account_id, name, provider, transport, email, created_at, updated_at
|
|
228
|
+
FROM accounts
|
|
229
|
+
WHERE name = ?
|
|
230
|
+
LIMIT 1
|
|
231
|
+
`)
|
|
232
|
+
.get(name);
|
|
233
|
+
}
|
|
234
|
+
findAccountById(accountId) {
|
|
235
|
+
return this.connection
|
|
236
|
+
.prepare(`
|
|
237
|
+
SELECT account_id, name, provider, transport, email, created_at, updated_at
|
|
238
|
+
FROM accounts
|
|
239
|
+
WHERE account_id = ?
|
|
240
|
+
LIMIT 1
|
|
241
|
+
`)
|
|
242
|
+
.get(accountId);
|
|
243
|
+
}
|
|
244
|
+
removeAccountByName(name) {
|
|
245
|
+
const existing = this.findAccountByName(name);
|
|
246
|
+
if (!existing) {
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
this.connection.prepare("DELETE FROM accounts WHERE name = ?").run(name);
|
|
250
|
+
return existing;
|
|
251
|
+
}
|
|
252
|
+
transaction(work) {
|
|
253
|
+
return this.connection.transaction(work)();
|
|
254
|
+
}
|
|
255
|
+
findEntityRefByProviderKey(entityKind, accountId, providerKey) {
|
|
256
|
+
const row = this.connection
|
|
257
|
+
.prepare(`
|
|
258
|
+
SELECT entity_ref
|
|
259
|
+
FROM provider_locators
|
|
260
|
+
WHERE entity_kind = ?
|
|
261
|
+
AND account_id = ?
|
|
262
|
+
AND provider_key = ?
|
|
263
|
+
LIMIT 1
|
|
264
|
+
`)
|
|
265
|
+
.get(entityKind, accountId, providerKey);
|
|
266
|
+
return row?.entity_ref;
|
|
267
|
+
}
|
|
268
|
+
upsertProviderLocator(input) {
|
|
269
|
+
this.connection
|
|
270
|
+
.prepare(`
|
|
271
|
+
INSERT INTO provider_locators (
|
|
272
|
+
entity_kind,
|
|
273
|
+
entity_ref,
|
|
274
|
+
account_id,
|
|
275
|
+
provider_key,
|
|
276
|
+
locator_json
|
|
277
|
+
) VALUES (
|
|
278
|
+
@entity_kind,
|
|
279
|
+
@entity_ref,
|
|
280
|
+
@account_id,
|
|
281
|
+
@provider_key,
|
|
282
|
+
@locator_json
|
|
283
|
+
)
|
|
284
|
+
ON CONFLICT(entity_kind, entity_ref) DO UPDATE SET
|
|
285
|
+
account_id = excluded.account_id,
|
|
286
|
+
provider_key = excluded.provider_key,
|
|
287
|
+
locator_json = excluded.locator_json
|
|
288
|
+
`)
|
|
289
|
+
.run(input);
|
|
290
|
+
}
|
|
291
|
+
findProviderLocator(entityKind, entityRef) {
|
|
292
|
+
return this.connection
|
|
293
|
+
.prepare(`
|
|
294
|
+
SELECT entity_kind, entity_ref, account_id, provider_key, locator_json
|
|
295
|
+
FROM provider_locators
|
|
296
|
+
WHERE entity_kind = ?
|
|
297
|
+
AND entity_ref = ?
|
|
298
|
+
LIMIT 1
|
|
299
|
+
`)
|
|
300
|
+
.get(entityKind, entityRef);
|
|
301
|
+
}
|
|
302
|
+
upsertThread(input) {
|
|
303
|
+
const timestamp = nowIsoUtc();
|
|
304
|
+
const existing = this.connection
|
|
305
|
+
.prepare("SELECT thread_ref FROM threads WHERE thread_ref = ? LIMIT 1")
|
|
306
|
+
.get(input.thread_ref);
|
|
307
|
+
if (!existing) {
|
|
308
|
+
this.connection
|
|
309
|
+
.prepare(`
|
|
310
|
+
INSERT INTO threads (
|
|
311
|
+
thread_ref,
|
|
312
|
+
account_id,
|
|
313
|
+
subject,
|
|
314
|
+
mailbox,
|
|
315
|
+
participants_json,
|
|
316
|
+
labels_json,
|
|
317
|
+
received_at,
|
|
318
|
+
message_count,
|
|
319
|
+
unread_count,
|
|
320
|
+
has_attachments,
|
|
321
|
+
first_seen_at,
|
|
322
|
+
last_synced_at
|
|
323
|
+
) VALUES (
|
|
324
|
+
@thread_ref,
|
|
325
|
+
@account_id,
|
|
326
|
+
@subject,
|
|
327
|
+
@mailbox,
|
|
328
|
+
@participants_json,
|
|
329
|
+
@labels_json,
|
|
330
|
+
@received_at,
|
|
331
|
+
@message_count,
|
|
332
|
+
@unread_count,
|
|
333
|
+
@has_attachments,
|
|
334
|
+
@first_seen_at,
|
|
335
|
+
@last_synced_at
|
|
336
|
+
)
|
|
337
|
+
`)
|
|
338
|
+
.run({
|
|
339
|
+
...input,
|
|
340
|
+
participants_json: JSON.stringify(input.participants),
|
|
341
|
+
labels_json: JSON.stringify(input.labels),
|
|
342
|
+
has_attachments: input.has_attachments ? 1 : 0,
|
|
343
|
+
first_seen_at: timestamp,
|
|
344
|
+
last_synced_at: timestamp,
|
|
345
|
+
});
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
this.connection
|
|
349
|
+
.prepare(`
|
|
350
|
+
UPDATE threads
|
|
351
|
+
SET subject = @subject,
|
|
352
|
+
mailbox = @mailbox,
|
|
353
|
+
participants_json = @participants_json,
|
|
354
|
+
labels_json = @labels_json,
|
|
355
|
+
received_at = @received_at,
|
|
356
|
+
message_count = @message_count,
|
|
357
|
+
unread_count = @unread_count,
|
|
358
|
+
has_attachments = @has_attachments,
|
|
359
|
+
last_synced_at = @last_synced_at
|
|
360
|
+
WHERE thread_ref = @thread_ref
|
|
361
|
+
`)
|
|
362
|
+
.run({
|
|
363
|
+
...input,
|
|
364
|
+
participants_json: JSON.stringify(input.participants),
|
|
365
|
+
labels_json: JSON.stringify(input.labels),
|
|
366
|
+
has_attachments: input.has_attachments ? 1 : 0,
|
|
367
|
+
last_synced_at: timestamp,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
upsertMessage(input) {
|
|
371
|
+
const timestamp = nowIsoUtc();
|
|
372
|
+
const existing = this.connection
|
|
373
|
+
.prepare("SELECT message_ref FROM messages WHERE message_ref = ? LIMIT 1")
|
|
374
|
+
.get(input.message_ref);
|
|
375
|
+
const payload = {
|
|
376
|
+
...input,
|
|
377
|
+
unread: input.unread ? 1 : 0,
|
|
378
|
+
body_cached: input.body_cached ? 1 : 0,
|
|
379
|
+
body_truncated: input.body_truncated ? 1 : 0,
|
|
380
|
+
};
|
|
381
|
+
if (!existing) {
|
|
382
|
+
this.connection
|
|
383
|
+
.prepare(`
|
|
384
|
+
INSERT INTO messages (
|
|
385
|
+
message_ref,
|
|
386
|
+
account_id,
|
|
387
|
+
thread_ref,
|
|
388
|
+
subject,
|
|
389
|
+
from_name,
|
|
390
|
+
from_email,
|
|
391
|
+
to_json,
|
|
392
|
+
cc_json,
|
|
393
|
+
sent_at,
|
|
394
|
+
received_at,
|
|
395
|
+
unread,
|
|
396
|
+
snippet,
|
|
397
|
+
body_cache_path,
|
|
398
|
+
body_cached,
|
|
399
|
+
body_truncated,
|
|
400
|
+
body_cached_bytes,
|
|
401
|
+
invite_json,
|
|
402
|
+
first_seen_at,
|
|
403
|
+
last_synced_at
|
|
404
|
+
) VALUES (
|
|
405
|
+
@message_ref,
|
|
406
|
+
@account_id,
|
|
407
|
+
@thread_ref,
|
|
408
|
+
@subject,
|
|
409
|
+
@from_name,
|
|
410
|
+
@from_email,
|
|
411
|
+
@to_json,
|
|
412
|
+
@cc_json,
|
|
413
|
+
@sent_at,
|
|
414
|
+
@received_at,
|
|
415
|
+
@unread,
|
|
416
|
+
@snippet,
|
|
417
|
+
@body_cache_path,
|
|
418
|
+
@body_cached,
|
|
419
|
+
@body_truncated,
|
|
420
|
+
@body_cached_bytes,
|
|
421
|
+
@invite_json,
|
|
422
|
+
@first_seen_at,
|
|
423
|
+
@last_synced_at
|
|
424
|
+
)
|
|
425
|
+
`)
|
|
426
|
+
.run({
|
|
427
|
+
...payload,
|
|
428
|
+
first_seen_at: timestamp,
|
|
429
|
+
last_synced_at: timestamp,
|
|
430
|
+
});
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
this.connection
|
|
434
|
+
.prepare(`
|
|
435
|
+
UPDATE messages
|
|
436
|
+
SET thread_ref = @thread_ref,
|
|
437
|
+
subject = @subject,
|
|
438
|
+
from_name = @from_name,
|
|
439
|
+
from_email = @from_email,
|
|
440
|
+
to_json = @to_json,
|
|
441
|
+
cc_json = @cc_json,
|
|
442
|
+
sent_at = @sent_at,
|
|
443
|
+
received_at = @received_at,
|
|
444
|
+
unread = @unread,
|
|
445
|
+
snippet = @snippet,
|
|
446
|
+
body_cache_path = @body_cache_path,
|
|
447
|
+
body_cached = @body_cached,
|
|
448
|
+
body_truncated = @body_truncated,
|
|
449
|
+
body_cached_bytes = @body_cached_bytes,
|
|
450
|
+
invite_json = @invite_json,
|
|
451
|
+
last_synced_at = @last_synced_at
|
|
452
|
+
WHERE message_ref = @message_ref
|
|
453
|
+
`)
|
|
454
|
+
.run({
|
|
455
|
+
...payload,
|
|
456
|
+
last_synced_at: timestamp,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
replaceThreadMessages(threadRef, messageRefs) {
|
|
460
|
+
this.connection.prepare("DELETE FROM thread_messages WHERE thread_ref = ?").run(threadRef);
|
|
461
|
+
const insert = this.connection.prepare(`
|
|
462
|
+
INSERT INTO thread_messages (
|
|
463
|
+
thread_ref,
|
|
464
|
+
message_ref,
|
|
465
|
+
position
|
|
466
|
+
) VALUES (?, ?, ?)
|
|
467
|
+
`);
|
|
468
|
+
for (const [index, messageRef] of messageRefs.entries()) {
|
|
469
|
+
insert.run(threadRef, messageRef, index);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
replaceAttachments(messageRef, attachments) {
|
|
473
|
+
const existingSavedTo = new Map(this.listAttachmentsForMessage(messageRef).map((attachment) => [attachment.attachment_id, attachment.saved_to]));
|
|
474
|
+
this.connection.prepare("DELETE FROM attachments WHERE message_ref = ?").run(messageRef);
|
|
475
|
+
const insert = this.connection.prepare(`
|
|
476
|
+
INSERT INTO attachments (
|
|
477
|
+
attachment_id,
|
|
478
|
+
message_ref,
|
|
479
|
+
filename,
|
|
480
|
+
mime_type,
|
|
481
|
+
size_bytes,
|
|
482
|
+
inline,
|
|
483
|
+
saved_to
|
|
484
|
+
) VALUES (
|
|
485
|
+
@attachment_id,
|
|
486
|
+
@message_ref,
|
|
487
|
+
@filename,
|
|
488
|
+
@mime_type,
|
|
489
|
+
@size_bytes,
|
|
490
|
+
@inline,
|
|
491
|
+
@saved_to
|
|
492
|
+
)
|
|
493
|
+
`);
|
|
494
|
+
for (const attachment of attachments) {
|
|
495
|
+
insert.run({
|
|
496
|
+
...attachment,
|
|
497
|
+
message_ref: messageRef,
|
|
498
|
+
inline: attachment.inline ? 1 : 0,
|
|
499
|
+
saved_to: attachment.saved_to ?? existingSavedTo.get(attachment.attachment_id) ?? null,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
updateAttachmentSavedTo(attachmentId, savedTo) {
|
|
504
|
+
this.connection
|
|
505
|
+
.prepare(`
|
|
506
|
+
UPDATE attachments
|
|
507
|
+
SET saved_to = ?
|
|
508
|
+
WHERE attachment_id = ?
|
|
509
|
+
`)
|
|
510
|
+
.run(savedTo, attachmentId);
|
|
511
|
+
}
|
|
512
|
+
upsertSummary(threadRef, summary) {
|
|
513
|
+
this.connection
|
|
514
|
+
.prepare(`
|
|
515
|
+
INSERT INTO summaries (
|
|
516
|
+
thread_ref,
|
|
517
|
+
backend,
|
|
518
|
+
model,
|
|
519
|
+
brief,
|
|
520
|
+
needs_action,
|
|
521
|
+
importance,
|
|
522
|
+
generated_at
|
|
523
|
+
) VALUES (
|
|
524
|
+
@thread_ref,
|
|
525
|
+
@backend,
|
|
526
|
+
@model,
|
|
527
|
+
@brief,
|
|
528
|
+
@needs_action,
|
|
529
|
+
@importance,
|
|
530
|
+
@generated_at
|
|
531
|
+
)
|
|
532
|
+
ON CONFLICT(thread_ref) DO UPDATE SET
|
|
533
|
+
backend = excluded.backend,
|
|
534
|
+
model = excluded.model,
|
|
535
|
+
brief = excluded.brief,
|
|
536
|
+
needs_action = excluded.needs_action,
|
|
537
|
+
importance = excluded.importance,
|
|
538
|
+
generated_at = excluded.generated_at
|
|
539
|
+
`)
|
|
540
|
+
.run({
|
|
541
|
+
thread_ref: threadRef,
|
|
542
|
+
backend: summary.backend,
|
|
543
|
+
model: summary.model,
|
|
544
|
+
brief: summary.brief,
|
|
545
|
+
needs_action: summary.needs_action ? 1 : 0,
|
|
546
|
+
importance: summary.importance,
|
|
547
|
+
generated_at: nowIsoUtc(),
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
findSummary(threadRef) {
|
|
551
|
+
const row = this.connection
|
|
552
|
+
.prepare(`
|
|
553
|
+
SELECT backend, model, brief, needs_action, importance
|
|
554
|
+
FROM summaries
|
|
555
|
+
WHERE thread_ref = ?
|
|
556
|
+
LIMIT 1
|
|
557
|
+
`)
|
|
558
|
+
.get(threadRef);
|
|
559
|
+
if (!row) {
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
return {
|
|
563
|
+
backend: row.backend,
|
|
564
|
+
model: row.model,
|
|
565
|
+
brief: row.brief,
|
|
566
|
+
needs_action: Boolean(row.needs_action),
|
|
567
|
+
importance: row.importance,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
listAttachmentsForMessage(messageRef) {
|
|
571
|
+
return this.connection
|
|
572
|
+
.prepare(`
|
|
573
|
+
SELECT attachment_id, filename, mime_type, size_bytes, inline, saved_to
|
|
574
|
+
FROM attachments
|
|
575
|
+
WHERE message_ref = ?
|
|
576
|
+
ORDER BY filename ASC
|
|
577
|
+
`)
|
|
578
|
+
.all(messageRef);
|
|
579
|
+
}
|
|
580
|
+
getStoredMessage(messageRef) {
|
|
581
|
+
return this.connection
|
|
582
|
+
.prepare(`
|
|
583
|
+
SELECT
|
|
584
|
+
message_ref,
|
|
585
|
+
account_id,
|
|
586
|
+
thread_ref,
|
|
587
|
+
subject,
|
|
588
|
+
from_name,
|
|
589
|
+
from_email,
|
|
590
|
+
to_json,
|
|
591
|
+
cc_json,
|
|
592
|
+
sent_at,
|
|
593
|
+
received_at,
|
|
594
|
+
unread,
|
|
595
|
+
snippet,
|
|
596
|
+
body_cache_path,
|
|
597
|
+
body_cached,
|
|
598
|
+
body_truncated,
|
|
599
|
+
body_cached_bytes,
|
|
600
|
+
invite_json
|
|
601
|
+
FROM messages
|
|
602
|
+
WHERE message_ref = ?
|
|
603
|
+
LIMIT 1
|
|
604
|
+
`)
|
|
605
|
+
.get(messageRef);
|
|
606
|
+
}
|
|
607
|
+
updateInviteStatusForThread(threadRef, responseStatus) {
|
|
608
|
+
const rows = this.connection
|
|
609
|
+
.prepare(`
|
|
610
|
+
SELECT message_ref, invite_json
|
|
611
|
+
FROM messages
|
|
612
|
+
WHERE thread_ref = ?
|
|
613
|
+
AND invite_json IS NOT NULL
|
|
614
|
+
`)
|
|
615
|
+
.all(threadRef);
|
|
616
|
+
if (rows.length === 0) {
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
const update = this.connection.prepare(`
|
|
620
|
+
UPDATE messages
|
|
621
|
+
SET invite_json = @invite_json,
|
|
622
|
+
last_synced_at = @last_synced_at
|
|
623
|
+
WHERE message_ref = @message_ref
|
|
624
|
+
`);
|
|
625
|
+
const lastSyncedAt = nowIsoUtc();
|
|
626
|
+
for (const row of rows) {
|
|
627
|
+
const invite = JSON.parse(row.invite_json);
|
|
628
|
+
update.run({
|
|
629
|
+
message_ref: row.message_ref,
|
|
630
|
+
invite_json: JSON.stringify({
|
|
631
|
+
...invite,
|
|
632
|
+
response_status: responseStatus,
|
|
633
|
+
}),
|
|
634
|
+
last_synced_at: lastSyncedAt,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
listMessageRefsForThread(threadRef) {
|
|
639
|
+
return this.connection
|
|
640
|
+
.prepare(`
|
|
641
|
+
SELECT message_ref
|
|
642
|
+
FROM thread_messages
|
|
643
|
+
WHERE thread_ref = ?
|
|
644
|
+
ORDER BY position ASC
|
|
645
|
+
`)
|
|
646
|
+
.all(threadRef).map((row) => row.message_ref);
|
|
647
|
+
}
|
|
648
|
+
markThreadArchived(threadRef) {
|
|
649
|
+
this.connection
|
|
650
|
+
.prepare(`
|
|
651
|
+
UPDATE threads
|
|
652
|
+
SET mailbox = 'archive',
|
|
653
|
+
labels_json = '["archive"]',
|
|
654
|
+
last_synced_at = @last_synced_at
|
|
655
|
+
WHERE thread_ref = @thread_ref
|
|
656
|
+
`)
|
|
657
|
+
.run({
|
|
658
|
+
thread_ref: threadRef,
|
|
659
|
+
last_synced_at: nowIsoUtc(),
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
updateMessagesUnreadState(messageRefs, unread) {
|
|
663
|
+
if (messageRefs.length === 0) {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
const update = this.connection.prepare(`
|
|
667
|
+
UPDATE messages
|
|
668
|
+
SET unread = @unread,
|
|
669
|
+
last_synced_at = @last_synced_at
|
|
670
|
+
WHERE message_ref = @message_ref
|
|
671
|
+
`);
|
|
672
|
+
const lastSyncedAt = nowIsoUtc();
|
|
673
|
+
for (const messageRef of messageRefs) {
|
|
674
|
+
update.run({
|
|
675
|
+
message_ref: messageRef,
|
|
676
|
+
unread: unread ? 1 : 0,
|
|
677
|
+
last_synced_at: lastSyncedAt,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
recomputeThreadUnreadCounts(threadRefs) {
|
|
682
|
+
if (threadRefs.length === 0) {
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
const selectUnreadCount = this.connection.prepare(`
|
|
686
|
+
SELECT COUNT(*) AS unread_count
|
|
687
|
+
FROM messages
|
|
688
|
+
WHERE thread_ref = ?
|
|
689
|
+
AND unread = 1
|
|
690
|
+
`);
|
|
691
|
+
const updateThread = this.connection.prepare(`
|
|
692
|
+
UPDATE threads
|
|
693
|
+
SET unread_count = @unread_count,
|
|
694
|
+
labels_json = @labels_json,
|
|
695
|
+
last_synced_at = @last_synced_at
|
|
696
|
+
WHERE thread_ref = @thread_ref
|
|
697
|
+
`);
|
|
698
|
+
const lastSyncedAt = nowIsoUtc();
|
|
699
|
+
for (const threadRef of new Set(threadRefs)) {
|
|
700
|
+
const row = selectUnreadCount.get(threadRef);
|
|
701
|
+
const unreadCount = row?.unread_count ?? 0;
|
|
702
|
+
updateThread.run({
|
|
703
|
+
thread_ref: threadRef,
|
|
704
|
+
unread_count: unreadCount,
|
|
705
|
+
labels_json: JSON.stringify(unreadCount > 0 ? ["inbox", "unread"] : ["inbox"]),
|
|
706
|
+
last_synced_at: lastSyncedAt,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
findMessageByRef(messageRef) {
|
|
711
|
+
return this.connection
|
|
712
|
+
.prepare(`
|
|
713
|
+
SELECT message_ref, thread_ref, account_id
|
|
714
|
+
FROM messages
|
|
715
|
+
WHERE message_ref = ?
|
|
716
|
+
LIMIT 1
|
|
717
|
+
`)
|
|
718
|
+
.get(messageRef);
|
|
719
|
+
}
|
|
720
|
+
findAttachmentById(attachmentId) {
|
|
721
|
+
return this.connection
|
|
722
|
+
.prepare(`
|
|
723
|
+
SELECT attachment_id, message_ref
|
|
724
|
+
FROM attachments
|
|
725
|
+
WHERE attachment_id = ?
|
|
726
|
+
LIMIT 1
|
|
727
|
+
`)
|
|
728
|
+
.get(attachmentId);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
//# sourceMappingURL=database.js.map
|