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.
- package/README.md +42 -15
- package/dist/cli.js +236 -21
- package/dist/cli.js.map +1 -1
- package/dist/config.js +2 -2
- package/dist/config.js.map +1 -1
- package/dist/contracts/account.js +8 -0
- package/dist/contracts/account.js.map +1 -1
- package/dist/e2e/gmail-v1.js +31 -0
- package/dist/e2e/gmail-v1.js.map +1 -1
- package/dist/e2e/outlook-v1.js +11 -0
- package/dist/e2e/outlook-v1.js.map +1 -1
- package/dist/lib/remote-auth.js +18 -2
- package/dist/lib/remote-auth.js.map +1 -1
- package/dist/lib/stored-mail.js +69 -0
- package/dist/lib/stored-mail.js.map +1 -0
- package/dist/paths.js +2 -0
- package/dist/paths.js.map +1 -1
- package/dist/providers/gmail/adapter.js +336 -53
- package/dist/providers/gmail/adapter.js.map +1 -1
- package/dist/providers/gmail/api.js +68 -0
- package/dist/providers/gmail/api.js.map +1 -1
- package/dist/providers/gmail/normalize.js.map +1 -1
- package/dist/providers/gmail/oauth.js +4 -0
- package/dist/providers/gmail/oauth.js.map +1 -1
- package/dist/providers/outlook/adapter.js +185 -97
- package/dist/providers/outlook/adapter.js.map +1 -1
- package/dist/providers/outlook/extract.js +7 -0
- package/dist/providers/outlook/extract.js.map +1 -1
- package/dist/providers/shared/inline-attachments.js +17 -0
- package/dist/providers/shared/inline-attachments.js.map +1 -0
- package/dist/refs.js +3 -0
- package/dist/refs.js.map +1 -1
- package/dist/session-daemon.js +218 -0
- package/dist/session-daemon.js.map +1 -0
- package/dist/session.js +283 -0
- package/dist/session.js.map +1 -0
- package/dist/state/database.js +542 -8
- package/dist/state/database.js.map +1 -1
- package/dist/summarizer.js +259 -76
- package/dist/summarizer.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { SurfaceError
|
|
4
|
+
import { SurfaceError } from "../../lib/errors.js";
|
|
5
5
|
import { assertWriteAllowed } from "../../lib/write-safety.js";
|
|
6
6
|
import { makeAttachmentId, makeMessageRef, makeThreadRef } from "../../refs.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { summarizeAndPersistThreads } from "../../summarizer.js";
|
|
8
|
+
import { annotateBodyWithInlineAttachments } from "../shared/inline-attachments.js";
|
|
9
|
+
import { createGmailDraft, downloadGmailAttachmentBytes, getGoogleCalendarEvent, getGmailThread, listGmailThreads, listGoogleCalendarEventsByIcalUid, modifyGmailMessage, modifyGmailThread, patchGoogleCalendarEvent, sendGmailRawMessage, } from "./api.js";
|
|
9
10
|
import { clearGmailAuthState, gmailAuthStatus, runGmailLogin } from "./oauth.js";
|
|
10
11
|
import { decodeBase64UrlBytes, decodePartData, headerDateToIso, headerIndex, internalDateToIso, isCalendarPart, iterParts, normalizeGmailBody, parseCalendarInvite, parseMailbox, parseMailboxes, } from "./normalize.js";
|
|
11
12
|
function sourceInfo(account) {
|
|
@@ -76,6 +77,88 @@ function gmailMailbox(labels) {
|
|
|
76
77
|
}
|
|
77
78
|
return "archive";
|
|
78
79
|
}
|
|
80
|
+
function quoteGmailSearchValue(value) {
|
|
81
|
+
const normalized = value.trim();
|
|
82
|
+
if (!normalized) {
|
|
83
|
+
return '""';
|
|
84
|
+
}
|
|
85
|
+
return /[\s"]/u.test(normalized) ? `"${normalized.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : normalized;
|
|
86
|
+
}
|
|
87
|
+
function gmailMailboxSearchOperator(mailbox) {
|
|
88
|
+
const normalized = mailbox.trim().toLowerCase();
|
|
89
|
+
switch (normalized) {
|
|
90
|
+
case "drafts":
|
|
91
|
+
return "in:drafts";
|
|
92
|
+
case "sent":
|
|
93
|
+
return "in:sent";
|
|
94
|
+
case "trash":
|
|
95
|
+
return "in:trash";
|
|
96
|
+
case "spam":
|
|
97
|
+
return "in:spam";
|
|
98
|
+
case "inbox":
|
|
99
|
+
return "in:inbox";
|
|
100
|
+
case "archive":
|
|
101
|
+
return "in:archive";
|
|
102
|
+
default:
|
|
103
|
+
return `in:${quoteGmailSearchValue(normalized)}`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function gmailLabelSearchOperator(label) {
|
|
107
|
+
const normalized = normalizeLabel(label);
|
|
108
|
+
switch (normalized) {
|
|
109
|
+
case "unread":
|
|
110
|
+
return "is:unread";
|
|
111
|
+
case "read":
|
|
112
|
+
return "is:read";
|
|
113
|
+
case "starred":
|
|
114
|
+
return "is:starred";
|
|
115
|
+
case "important":
|
|
116
|
+
return "label:important";
|
|
117
|
+
case "inbox":
|
|
118
|
+
case "sent":
|
|
119
|
+
case "drafts":
|
|
120
|
+
case "trash":
|
|
121
|
+
case "spam":
|
|
122
|
+
case "archive":
|
|
123
|
+
return gmailMailboxSearchOperator(normalized);
|
|
124
|
+
default:
|
|
125
|
+
return `label:${quoteGmailSearchValue(normalized)}`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function buildGmailSearchQuery(query) {
|
|
129
|
+
const parts = [];
|
|
130
|
+
if (query.text?.trim()) {
|
|
131
|
+
parts.push(query.text.trim());
|
|
132
|
+
}
|
|
133
|
+
if (query.from?.trim()) {
|
|
134
|
+
parts.push(`from:${quoteGmailSearchValue(query.from)}`);
|
|
135
|
+
}
|
|
136
|
+
if (query.subject?.trim()) {
|
|
137
|
+
parts.push(`subject:${quoteGmailSearchValue(query.subject)}`);
|
|
138
|
+
}
|
|
139
|
+
if (query.mailbox?.trim()) {
|
|
140
|
+
parts.push(gmailMailboxSearchOperator(query.mailbox));
|
|
141
|
+
}
|
|
142
|
+
for (const label of query.labels ?? []) {
|
|
143
|
+
if (!label.trim()) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
parts.push(gmailLabelSearchOperator(label));
|
|
147
|
+
}
|
|
148
|
+
return parts.length > 0 ? parts.join(" ").trim() : undefined;
|
|
149
|
+
}
|
|
150
|
+
function threadMatchesStructuredFilters(thread, query) {
|
|
151
|
+
if (query.mailbox?.trim() && thread.envelope.mailbox.trim().toLowerCase() !== query.mailbox.trim().toLowerCase()) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
if ((query.labels?.length ?? 0) > 0) {
|
|
155
|
+
const available = new Set(thread.envelope.labels.map((label) => label.trim().toLowerCase()));
|
|
156
|
+
if ((query.labels ?? []).some((label) => !available.has(label.trim().toLowerCase()))) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
79
162
|
function partSizeBytes(part) {
|
|
80
163
|
if (typeof part.body?.size === "number") {
|
|
81
164
|
return part.body.size;
|
|
@@ -152,6 +235,126 @@ function mapCalendarPartstat(value) {
|
|
|
152
235
|
return null;
|
|
153
236
|
}
|
|
154
237
|
}
|
|
238
|
+
function mapGoogleCalendarResponseStatus(value) {
|
|
239
|
+
const normalized = (value ?? "").trim().toLowerCase();
|
|
240
|
+
switch (normalized) {
|
|
241
|
+
case "accepted":
|
|
242
|
+
return "accept";
|
|
243
|
+
case "declined":
|
|
244
|
+
return "decline";
|
|
245
|
+
case "tentative":
|
|
246
|
+
return "tentative";
|
|
247
|
+
case "needsaction":
|
|
248
|
+
return "needs_response";
|
|
249
|
+
default:
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function googleCalendarResponseStatusForRsvp(response) {
|
|
254
|
+
switch (response) {
|
|
255
|
+
case "accept":
|
|
256
|
+
return "accepted";
|
|
257
|
+
case "decline":
|
|
258
|
+
return "declined";
|
|
259
|
+
case "tentative":
|
|
260
|
+
return "tentative";
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function googleCalendarEventStart(event) {
|
|
264
|
+
return event.start?.dateTime ?? event.start?.date ?? null;
|
|
265
|
+
}
|
|
266
|
+
function normalizeComparableDatePrefix(value) {
|
|
267
|
+
if (!value) {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
const match = value.match(/^(\d{4}-\d{2}-\d{2})/);
|
|
271
|
+
return match?.[1] ?? null;
|
|
272
|
+
}
|
|
273
|
+
function chooseGoogleCalendarEvent(events, meetingStart) {
|
|
274
|
+
if (events.length === 0) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
const targetDate = normalizeComparableDatePrefix(meetingStart);
|
|
278
|
+
if (targetDate) {
|
|
279
|
+
const match = events.find((event) => normalizeComparableDatePrefix(googleCalendarEventStart(event)) === targetDate);
|
|
280
|
+
if (match) {
|
|
281
|
+
return match;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return events[0] ?? null;
|
|
285
|
+
}
|
|
286
|
+
function pickCalendarAttendee(event, attendeeEmail, fallbackEmail) {
|
|
287
|
+
const normalizedTarget = (attendeeEmail ?? fallbackEmail).trim().toLowerCase();
|
|
288
|
+
const normalizedFallback = fallbackEmail.trim().toLowerCase();
|
|
289
|
+
const attendees = event.attendees ?? [];
|
|
290
|
+
const chosen = attendees.find((attendee) => attendee.self === true)
|
|
291
|
+
?? attendees.find((attendee) => (attendee.email ?? "").trim().toLowerCase() === normalizedTarget)
|
|
292
|
+
?? attendees.find((attendee) => (attendee.email ?? "").trim().toLowerCase() === normalizedFallback);
|
|
293
|
+
if (!chosen?.email) {
|
|
294
|
+
return normalizedTarget
|
|
295
|
+
? { email: normalizedTarget, response_status: null }
|
|
296
|
+
: null;
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
email: chosen.email,
|
|
300
|
+
response_status: mapGoogleCalendarResponseStatus(chosen.responseStatus),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
async function hydrateGmailInviteMetadataFromCalendar(account, context, metadata) {
|
|
304
|
+
if (!metadata.calendar_uid) {
|
|
305
|
+
return metadata;
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
const events = await listGoogleCalendarEventsByIcalUid(account, context, "primary", metadata.calendar_uid);
|
|
309
|
+
const event = chooseGoogleCalendarEvent(events, metadata.meeting_start);
|
|
310
|
+
if (!event) {
|
|
311
|
+
return metadata;
|
|
312
|
+
}
|
|
313
|
+
const attendee = pickCalendarAttendee(event, metadata.attendee_email, account.email);
|
|
314
|
+
return {
|
|
315
|
+
...metadata,
|
|
316
|
+
attendee_email: attendee?.email ?? metadata.attendee_email,
|
|
317
|
+
invite: {
|
|
318
|
+
...metadata.invite,
|
|
319
|
+
rsvp_supported: true,
|
|
320
|
+
response_status: attendee?.response_status ?? metadata.invite.response_status,
|
|
321
|
+
available_rsvp_responses: ["accept", "decline", "tentative"],
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
if (error instanceof SurfaceError && (error.code === "reauth_required" || error.code === "transport_error")) {
|
|
327
|
+
return metadata;
|
|
328
|
+
}
|
|
329
|
+
throw error;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
async function extractGmailInviteMetadata(account, context, message, participants) {
|
|
333
|
+
const calendarText = await extractCalendarText(account, context, message);
|
|
334
|
+
if (!calendarText) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
const parsedInvite = parseCalendarInvite(calendarText, {
|
|
338
|
+
mailboxEmail: account.email,
|
|
339
|
+
recipientEmails: [...participants.to, ...participants.cc].map((mailbox) => mailbox.email),
|
|
340
|
+
});
|
|
341
|
+
const meeting = parsedInvite.meeting;
|
|
342
|
+
if (!meeting) {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
const metadata = {
|
|
346
|
+
invite: {
|
|
347
|
+
is_invite: true,
|
|
348
|
+
rsvp_supported: Boolean(meeting.uid),
|
|
349
|
+
response_status: mapCalendarPartstat(meeting.response_type),
|
|
350
|
+
available_rsvp_responses: meeting.uid ? ["accept", "decline", "tentative"] : [],
|
|
351
|
+
},
|
|
352
|
+
calendar_uid: meeting.uid ?? null,
|
|
353
|
+
attendee_email: meeting.attendee?.email ?? null,
|
|
354
|
+
meeting_start: meeting.start ?? null,
|
|
355
|
+
};
|
|
356
|
+
return hydrateGmailInviteMetadataFromCalendar(account, context, metadata);
|
|
357
|
+
}
|
|
155
358
|
async function normalizeGmailMessage(account, context, message) {
|
|
156
359
|
const indexedHeaders = headerIndex(message.payload?.headers);
|
|
157
360
|
const subject = indexedHeaders.subject ?? "";
|
|
@@ -163,22 +366,9 @@ async function normalizeGmailMessage(account, context, message) {
|
|
|
163
366
|
const unread = (message.labelIds ?? []).includes("UNREAD");
|
|
164
367
|
const body = normalizeGmailBody(message.payload, message.snippet ?? "");
|
|
165
368
|
const attachments = extractAttachmentRecords(message);
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
const parsedInvite = parseCalendarInvite(calendarText, {
|
|
170
|
-
mailboxEmail: account.email,
|
|
171
|
-
recipientEmails: [...to, ...cc].map((mailbox) => mailbox.email),
|
|
172
|
-
});
|
|
173
|
-
if (parsedInvite.meeting) {
|
|
174
|
-
invite = {
|
|
175
|
-
is_invite: true,
|
|
176
|
-
rsvp_supported: false,
|
|
177
|
-
response_status: mapCalendarPartstat(parsedInvite.meeting.response_type),
|
|
178
|
-
available_rsvp_responses: [],
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
}
|
|
369
|
+
const bodyText = annotateBodyWithInlineAttachments(body.text, attachments);
|
|
370
|
+
const inviteMetadata = await extractGmailInviteMetadata(account, context, message, { to, cc });
|
|
371
|
+
const invite = inviteMetadata?.invite;
|
|
182
372
|
return {
|
|
183
373
|
message_ref: "",
|
|
184
374
|
envelope: {
|
|
@@ -190,12 +380,12 @@ async function normalizeGmailMessage(account, context, message) {
|
|
|
190
380
|
unread,
|
|
191
381
|
...(subject ? { subject } : {}),
|
|
192
382
|
},
|
|
193
|
-
snippet: message.snippet ??
|
|
383
|
+
snippet: message.snippet ?? bodyText.slice(0, 240),
|
|
194
384
|
body: {
|
|
195
|
-
text:
|
|
385
|
+
text: bodyText,
|
|
196
386
|
truncated: false,
|
|
197
387
|
cached: true,
|
|
198
|
-
cached_bytes: Buffer.byteLength(
|
|
388
|
+
cached_bytes: Buffer.byteLength(bodyText, "utf8"),
|
|
199
389
|
},
|
|
200
390
|
attachments,
|
|
201
391
|
...(invite ? { invite } : {}),
|
|
@@ -208,6 +398,9 @@ async function normalizeGmailMessage(account, context, message) {
|
|
|
208
398
|
locator: {
|
|
209
399
|
thread_id: message.threadId ?? null,
|
|
210
400
|
message_id: message.id ?? null,
|
|
401
|
+
calendar_uid: inviteMetadata?.calendar_uid ?? null,
|
|
402
|
+
attendee_email: inviteMetadata?.attendee_email ?? null,
|
|
403
|
+
meeting_start: inviteMetadata?.meeting_start ?? null,
|
|
211
404
|
},
|
|
212
405
|
},
|
|
213
406
|
};
|
|
@@ -368,6 +561,9 @@ function parseMessageLocator(locatorJson) {
|
|
|
368
561
|
return {
|
|
369
562
|
thread_id: typeof parsed.thread_id === "string" && parsed.thread_id ? parsed.thread_id : null,
|
|
370
563
|
message_id: typeof parsed.message_id === "string" && parsed.message_id ? parsed.message_id : null,
|
|
564
|
+
calendar_uid: typeof parsed.calendar_uid === "string" && parsed.calendar_uid ? parsed.calendar_uid : null,
|
|
565
|
+
attendee_email: typeof parsed.attendee_email === "string" && parsed.attendee_email ? parsed.attendee_email : null,
|
|
566
|
+
meeting_start: typeof parsed.meeting_start === "string" && parsed.meeting_start ? parsed.meeting_start : null,
|
|
371
567
|
};
|
|
372
568
|
}
|
|
373
569
|
function latestStoredThreadMessage(threadRef, context) {
|
|
@@ -411,6 +607,18 @@ function buildMarkMessagesEnvelope(account, command, updated) {
|
|
|
411
607
|
updated,
|
|
412
608
|
};
|
|
413
609
|
}
|
|
610
|
+
function buildRsvpEnvelope(account, messageRef, threadRef, response, invite) {
|
|
611
|
+
return {
|
|
612
|
+
schema_version: "1",
|
|
613
|
+
command: "rsvp",
|
|
614
|
+
account: account.name,
|
|
615
|
+
message_ref: messageRef,
|
|
616
|
+
thread_ref: threadRef,
|
|
617
|
+
source: sourceInfo(account),
|
|
618
|
+
response,
|
|
619
|
+
invite: invite ?? null,
|
|
620
|
+
};
|
|
621
|
+
}
|
|
414
622
|
function buildReadEnvelope(account, messageRef, threadRef, parsed, attachments, cacheStatus) {
|
|
415
623
|
return {
|
|
416
624
|
schema_version: "1",
|
|
@@ -431,19 +639,6 @@ function buildReadEnvelope(account, messageRef, threadRef, parsed, attachments,
|
|
|
431
639
|
},
|
|
432
640
|
};
|
|
433
641
|
}
|
|
434
|
-
async function maybeSummarizeThreads(threads, context) {
|
|
435
|
-
if (context.config.summarizerBackend === "none") {
|
|
436
|
-
return threads;
|
|
437
|
-
}
|
|
438
|
-
const summarized = [];
|
|
439
|
-
for (const thread of threads) {
|
|
440
|
-
summarized.push({
|
|
441
|
-
...thread,
|
|
442
|
-
summary: await summarizeThread(thread, context.config),
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
return summarized;
|
|
446
|
-
}
|
|
447
642
|
async function persistThreads(account, context, threads) {
|
|
448
643
|
return context.db.transaction(() => {
|
|
449
644
|
const persistedThreads = [];
|
|
@@ -557,9 +752,6 @@ async function persistThreads(account, context, threads) {
|
|
|
557
752
|
messageRefs.push(resolvedMessageRef);
|
|
558
753
|
}
|
|
559
754
|
context.db.replaceThreadMessages(resolvedThreadRef, messageRefs);
|
|
560
|
-
if (thread.summary) {
|
|
561
|
-
context.db.upsertSummary(resolvedThreadRef, thread.summary);
|
|
562
|
-
}
|
|
563
755
|
persistedThreads.push({
|
|
564
756
|
...thread,
|
|
565
757
|
thread_ref: resolvedThreadRef,
|
|
@@ -572,8 +764,8 @@ async function persistThreads(account, context, threads) {
|
|
|
572
764
|
async function fetchAndPersistGmailThread(account, context, threadId) {
|
|
573
765
|
const thread = await getGmailThread(account, context, threadId);
|
|
574
766
|
const normalized = await normalizeGmailThread(account, context, thread);
|
|
575
|
-
const
|
|
576
|
-
await
|
|
767
|
+
const persisted = await persistThreads(account, context, [normalized]);
|
|
768
|
+
await summarizeAndPersistThreads(persisted, context.config, context.db, context.db.getAccountIdentity(account));
|
|
577
769
|
}
|
|
578
770
|
async function refreshStoredMessage(account, messageRef, context) {
|
|
579
771
|
const locatorRow = context.db.findProviderLocator("message", messageRef);
|
|
@@ -654,6 +846,72 @@ async function resolveGmailMessageContext(account, messageRef, context) {
|
|
|
654
846
|
headers: headerIndex(message.payload?.headers),
|
|
655
847
|
};
|
|
656
848
|
}
|
|
849
|
+
async function resolveGmailRsvpTarget(account, messageRef, context) {
|
|
850
|
+
const target = await resolveGmailMessageContext(account, messageRef, context);
|
|
851
|
+
const inviteMetadata = await extractGmailInviteMetadata(account, context, target.message, {
|
|
852
|
+
to: parseMailboxes(target.headers.to),
|
|
853
|
+
cc: parseMailboxes(target.headers.cc),
|
|
854
|
+
});
|
|
855
|
+
if (!inviteMetadata?.calendar_uid) {
|
|
856
|
+
throw new SurfaceError("unsupported", `Message '${messageRef}' is not a Gmail calendar invite with a resolvable UID.`, {
|
|
857
|
+
account: account.name,
|
|
858
|
+
messageRef,
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
if (!inviteMetadata.invite.rsvp_supported) {
|
|
862
|
+
throw new SurfaceError("unsupported", `Message '${messageRef}' is not a Gmail invite Surface can RSVP to.`, {
|
|
863
|
+
account: account.name,
|
|
864
|
+
messageRef,
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
const attendeeEmail = inviteMetadata.attendee_email?.trim().toLowerCase()
|
|
868
|
+
|| account.email.trim().toLowerCase();
|
|
869
|
+
if (!attendeeEmail) {
|
|
870
|
+
throw new SurfaceError("unsupported", `Message '${messageRef}' does not expose an attendee email for RSVP.`, {
|
|
871
|
+
account: account.name,
|
|
872
|
+
messageRef,
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
return {
|
|
876
|
+
stored: target.stored,
|
|
877
|
+
message: target.message,
|
|
878
|
+
threadRef: target.stored.thread_ref,
|
|
879
|
+
calendarUid: inviteMetadata.calendar_uid,
|
|
880
|
+
attendeeEmail,
|
|
881
|
+
meetingStart: inviteMetadata.meeting_start,
|
|
882
|
+
invite: inviteMetadata.invite,
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
async function performGmailRsvp(account, context, target, response) {
|
|
886
|
+
const events = await listGoogleCalendarEventsByIcalUid(account, context, "primary", target.calendarUid);
|
|
887
|
+
const event = chooseGoogleCalendarEvent(events, target.meetingStart);
|
|
888
|
+
if (!event?.id) {
|
|
889
|
+
throw new SurfaceError("not_found", `Google Calendar could not find an event matching invite UID '${target.calendarUid}'.`, { account: account.name });
|
|
890
|
+
}
|
|
891
|
+
const attendee = pickCalendarAttendee(event, target.attendeeEmail, account.email);
|
|
892
|
+
if (!attendee?.email) {
|
|
893
|
+
throw new SurfaceError("unsupported", `Google Calendar event '${event.id}' does not expose an attendee that Surface can RSVP as.`, { account: account.name });
|
|
894
|
+
}
|
|
895
|
+
const patched = await patchGoogleCalendarEvent(account, context, "primary", event.id, {
|
|
896
|
+
attendeesOmitted: true,
|
|
897
|
+
attendees: [
|
|
898
|
+
{
|
|
899
|
+
email: attendee.email,
|
|
900
|
+
responseStatus: googleCalendarResponseStatusForRsvp(response),
|
|
901
|
+
},
|
|
902
|
+
],
|
|
903
|
+
});
|
|
904
|
+
const refreshed = patched.id
|
|
905
|
+
? await getGoogleCalendarEvent(account, context, "primary", patched.id)
|
|
906
|
+
: patched;
|
|
907
|
+
const refreshedAttendee = pickCalendarAttendee(refreshed, attendee.email, account.email);
|
|
908
|
+
return {
|
|
909
|
+
is_invite: true,
|
|
910
|
+
rsvp_supported: true,
|
|
911
|
+
response_status: refreshedAttendee?.response_status ?? mapGoogleCalendarResponseStatus(googleCalendarResponseStatusForRsvp(response)),
|
|
912
|
+
available_rsvp_responses: ["accept", "decline", "tentative"],
|
|
913
|
+
};
|
|
914
|
+
}
|
|
657
915
|
async function refreshRefsFromGmailResponse(account, context, response) {
|
|
658
916
|
const threadId = response.threadId ?? null;
|
|
659
917
|
if (threadId) {
|
|
@@ -697,8 +955,8 @@ async function fetchGmailThreads(account, context, options) {
|
|
|
697
955
|
.filter((threadId) => Boolean(threadId))
|
|
698
956
|
.map((threadId) => getGmailThread(account, context, threadId)));
|
|
699
957
|
const normalized = await Promise.all(hydrated.map((thread) => normalizeGmailThread(account, context, thread)));
|
|
700
|
-
const
|
|
701
|
-
return
|
|
958
|
+
const persisted = await persistThreads(account, context, normalized);
|
|
959
|
+
return summarizeAndPersistThreads(persisted, context.config, context.db, context.db.getAccountIdentity(account));
|
|
702
960
|
}
|
|
703
961
|
async function sendOrDraftGmailMessage(account, context, payload) {
|
|
704
962
|
if (payload.draft) {
|
|
@@ -725,13 +983,8 @@ export class GmailApiAdapter {
|
|
|
725
983
|
transport = "gmail-api";
|
|
726
984
|
async login(account, context) {
|
|
727
985
|
const result = await runGmailLogin(account, context);
|
|
728
|
-
if (result.authenticatedEmail
|
|
729
|
-
context.db.
|
|
730
|
-
name: account.name,
|
|
731
|
-
provider: account.provider,
|
|
732
|
-
transport: account.transport,
|
|
733
|
-
email: result.authenticatedEmail,
|
|
734
|
-
});
|
|
986
|
+
if (result.authenticatedEmail) {
|
|
987
|
+
context.db.updateAccountIdentityFromProvider(account, result.authenticatedEmail);
|
|
735
988
|
}
|
|
736
989
|
return {
|
|
737
990
|
status: "authenticated",
|
|
@@ -749,16 +1002,21 @@ export class GmailApiAdapter {
|
|
|
749
1002
|
}
|
|
750
1003
|
async authStatus(account, context) {
|
|
751
1004
|
const status = await gmailAuthStatus(account, context);
|
|
1005
|
+
if (status.authenticatedEmail) {
|
|
1006
|
+
context.db.updateAccountIdentityFromProvider(account, status.authenticatedEmail);
|
|
1007
|
+
}
|
|
752
1008
|
return status.authenticated
|
|
753
1009
|
? { status: "authenticated", detail: status.detail }
|
|
754
1010
|
: { status: "unauthenticated", detail: status.detail };
|
|
755
1011
|
}
|
|
756
1012
|
async search(account, query, context) {
|
|
757
|
-
|
|
1013
|
+
const queryText = buildGmailSearchQuery(query);
|
|
1014
|
+
const threads = await fetchGmailThreads(account, context, {
|
|
758
1015
|
kind: "search",
|
|
759
|
-
queryText:
|
|
1016
|
+
...(queryText ? { queryText } : {}),
|
|
760
1017
|
limit: query.limit,
|
|
761
1018
|
});
|
|
1019
|
+
return threads.filter((thread) => threadMatchesStructuredFilters(thread, query)).slice(0, query.limit);
|
|
762
1020
|
}
|
|
763
1021
|
async fetchUnread(account, query, context) {
|
|
764
1022
|
return fetchGmailThreads(account, context, {
|
|
@@ -766,6 +1024,23 @@ export class GmailApiAdapter {
|
|
|
766
1024
|
limit: query.limit,
|
|
767
1025
|
});
|
|
768
1026
|
}
|
|
1027
|
+
async refreshThread(account, threadRef, context) {
|
|
1028
|
+
const locatorRow = context.db.findProviderLocator("thread", threadRef);
|
|
1029
|
+
if (!locatorRow) {
|
|
1030
|
+
throw new SurfaceError("cache_miss", `No provider locator exists for thread '${threadRef}'.`, {
|
|
1031
|
+
account: account.name,
|
|
1032
|
+
threadRef,
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
const locator = JSON.parse(locatorRow.locator_json);
|
|
1036
|
+
if (!locator.thread_id) {
|
|
1037
|
+
throw new SurfaceError("transport_error", `Thread '${threadRef}' is missing a Gmail thread id.`, {
|
|
1038
|
+
account: account.name,
|
|
1039
|
+
threadRef,
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
await fetchAndPersistGmailThread(account, context, locator.thread_id);
|
|
1043
|
+
}
|
|
769
1044
|
async readMessage(account, messageRef, refresh, context) {
|
|
770
1045
|
const stored = context.db.getStoredMessage(messageRef);
|
|
771
1046
|
if (!stored) {
|
|
@@ -884,8 +1159,16 @@ export class GmailApiAdapter {
|
|
|
884
1159
|
},
|
|
885
1160
|
};
|
|
886
1161
|
}
|
|
887
|
-
async rsvp(account,
|
|
888
|
-
|
|
1162
|
+
async rsvp(account, messageRef, response, context) {
|
|
1163
|
+
const target = await resolveGmailRsvpTarget(account, messageRef, context);
|
|
1164
|
+
const invite = await performGmailRsvp(account, context, target, response);
|
|
1165
|
+
context.db.updateInviteForThread(target.threadRef, {
|
|
1166
|
+
is_invite: true,
|
|
1167
|
+
rsvp_supported: invite.rsvp_supported,
|
|
1168
|
+
response_status: invite.response_status,
|
|
1169
|
+
available_rsvp_responses: invite.available_rsvp_responses,
|
|
1170
|
+
});
|
|
1171
|
+
return buildRsvpEnvelope(account, messageRef, target.threadRef, response, invite);
|
|
889
1172
|
}
|
|
890
1173
|
async sendMessage(account, input, context) {
|
|
891
1174
|
const recipients = {
|