surface-cli 0.1.1 → 0.2.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 +9 -4
- package/dist/cli.js +81 -10
- package/dist/cli.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/providers/gmail/adapter.js +326 -25
- 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 +1 -0
- package/dist/providers/gmail/oauth.js.map +1 -1
- package/dist/providers/outlook/adapter.js +88 -11
- 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/state/database.js +62 -4
- package/dist/state/database.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
function storedAttachments(db, messageRef) {
|
|
3
|
+
return db.listAttachmentsForMessage(messageRef).map((attachment) => ({
|
|
4
|
+
attachment_id: attachment.attachment_id,
|
|
5
|
+
filename: attachment.filename,
|
|
6
|
+
mime_type: attachment.mime_type,
|
|
7
|
+
size_bytes: attachment.size_bytes,
|
|
8
|
+
inline: Boolean(attachment.inline),
|
|
9
|
+
}));
|
|
10
|
+
}
|
|
11
|
+
function storedMessage(record, attachments) {
|
|
12
|
+
const hasReadableCache = Boolean(record.body_cache_path && existsSync(record.body_cache_path));
|
|
13
|
+
return {
|
|
14
|
+
message_ref: record.message_ref,
|
|
15
|
+
envelope: {
|
|
16
|
+
from: record.from_name || record.from_email
|
|
17
|
+
? {
|
|
18
|
+
name: record.from_name ?? "",
|
|
19
|
+
email: record.from_email ?? "",
|
|
20
|
+
}
|
|
21
|
+
: null,
|
|
22
|
+
to: JSON.parse(record.to_json),
|
|
23
|
+
cc: JSON.parse(record.cc_json),
|
|
24
|
+
sent_at: record.sent_at,
|
|
25
|
+
received_at: record.received_at,
|
|
26
|
+
unread: Boolean(record.unread),
|
|
27
|
+
...(record.subject ? { subject: record.subject } : {}),
|
|
28
|
+
},
|
|
29
|
+
snippet: record.snippet,
|
|
30
|
+
body: {
|
|
31
|
+
text: hasReadableCache && record.body_cache_path ? readFileSync(record.body_cache_path, "utf8") : "",
|
|
32
|
+
truncated: Boolean(record.body_truncated),
|
|
33
|
+
cached: hasReadableCache && Boolean(record.body_cached),
|
|
34
|
+
cached_bytes: record.body_cached_bytes,
|
|
35
|
+
},
|
|
36
|
+
attachments,
|
|
37
|
+
...(record.invite_json ? { invite: JSON.parse(record.invite_json) } : {}),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function threadHasReadableCache(db, threadRef) {
|
|
41
|
+
const messages = db.listStoredMessagesForThread(threadRef);
|
|
42
|
+
return messages.length > 0 && messages.every((message) => Boolean(message.body_cache_path && existsSync(message.body_cache_path)));
|
|
43
|
+
}
|
|
44
|
+
export function loadStoredThread(db, account, threadRef) {
|
|
45
|
+
const thread = db.getStoredThread(threadRef);
|
|
46
|
+
if (!thread) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
thread_ref: thread.thread_ref,
|
|
51
|
+
source: {
|
|
52
|
+
provider: account.provider,
|
|
53
|
+
transport: account.transport,
|
|
54
|
+
},
|
|
55
|
+
envelope: {
|
|
56
|
+
subject: thread.subject ?? "",
|
|
57
|
+
participants: JSON.parse(thread.participants_json),
|
|
58
|
+
mailbox: thread.mailbox ?? "inbox",
|
|
59
|
+
labels: JSON.parse(thread.labels_json),
|
|
60
|
+
received_at: thread.received_at,
|
|
61
|
+
message_count: thread.message_count,
|
|
62
|
+
unread_count: thread.unread_count,
|
|
63
|
+
has_attachments: Boolean(thread.has_attachments),
|
|
64
|
+
},
|
|
65
|
+
summary: db.findSummary(threadRef),
|
|
66
|
+
messages: db.listStoredMessagesForThread(threadRef).map((message) => storedMessage(message, storedAttachments(db, message.message_ref))),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=stored-mail.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stored-mail.js","sourceRoot":"","sources":["../../src/lib/stored-mail.ts"],"names":[],"mappings":"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"}
|
|
@@ -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
7
|
import { summarizeThread } from "../../summarizer.js";
|
|
8
|
-
import {
|
|
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",
|
|
@@ -654,6 +862,72 @@ async function resolveGmailMessageContext(account, messageRef, context) {
|
|
|
654
862
|
headers: headerIndex(message.payload?.headers),
|
|
655
863
|
};
|
|
656
864
|
}
|
|
865
|
+
async function resolveGmailRsvpTarget(account, messageRef, context) {
|
|
866
|
+
const target = await resolveGmailMessageContext(account, messageRef, context);
|
|
867
|
+
const inviteMetadata = await extractGmailInviteMetadata(account, context, target.message, {
|
|
868
|
+
to: parseMailboxes(target.headers.to),
|
|
869
|
+
cc: parseMailboxes(target.headers.cc),
|
|
870
|
+
});
|
|
871
|
+
if (!inviteMetadata?.calendar_uid) {
|
|
872
|
+
throw new SurfaceError("unsupported", `Message '${messageRef}' is not a Gmail calendar invite with a resolvable UID.`, {
|
|
873
|
+
account: account.name,
|
|
874
|
+
messageRef,
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
if (!inviteMetadata.invite.rsvp_supported) {
|
|
878
|
+
throw new SurfaceError("unsupported", `Message '${messageRef}' is not a Gmail invite Surface can RSVP to.`, {
|
|
879
|
+
account: account.name,
|
|
880
|
+
messageRef,
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
const attendeeEmail = inviteMetadata.attendee_email?.trim().toLowerCase()
|
|
884
|
+
|| account.email.trim().toLowerCase();
|
|
885
|
+
if (!attendeeEmail) {
|
|
886
|
+
throw new SurfaceError("unsupported", `Message '${messageRef}' does not expose an attendee email for RSVP.`, {
|
|
887
|
+
account: account.name,
|
|
888
|
+
messageRef,
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
return {
|
|
892
|
+
stored: target.stored,
|
|
893
|
+
message: target.message,
|
|
894
|
+
threadRef: target.stored.thread_ref,
|
|
895
|
+
calendarUid: inviteMetadata.calendar_uid,
|
|
896
|
+
attendeeEmail,
|
|
897
|
+
meetingStart: inviteMetadata.meeting_start,
|
|
898
|
+
invite: inviteMetadata.invite,
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
async function performGmailRsvp(account, context, target, response) {
|
|
902
|
+
const events = await listGoogleCalendarEventsByIcalUid(account, context, "primary", target.calendarUid);
|
|
903
|
+
const event = chooseGoogleCalendarEvent(events, target.meetingStart);
|
|
904
|
+
if (!event?.id) {
|
|
905
|
+
throw new SurfaceError("not_found", `Google Calendar could not find an event matching invite UID '${target.calendarUid}'.`, { account: account.name });
|
|
906
|
+
}
|
|
907
|
+
const attendee = pickCalendarAttendee(event, target.attendeeEmail, account.email);
|
|
908
|
+
if (!attendee?.email) {
|
|
909
|
+
throw new SurfaceError("unsupported", `Google Calendar event '${event.id}' does not expose an attendee that Surface can RSVP as.`, { account: account.name });
|
|
910
|
+
}
|
|
911
|
+
const patched = await patchGoogleCalendarEvent(account, context, "primary", event.id, {
|
|
912
|
+
attendeesOmitted: true,
|
|
913
|
+
attendees: [
|
|
914
|
+
{
|
|
915
|
+
email: attendee.email,
|
|
916
|
+
responseStatus: googleCalendarResponseStatusForRsvp(response),
|
|
917
|
+
},
|
|
918
|
+
],
|
|
919
|
+
});
|
|
920
|
+
const refreshed = patched.id
|
|
921
|
+
? await getGoogleCalendarEvent(account, context, "primary", patched.id)
|
|
922
|
+
: patched;
|
|
923
|
+
const refreshedAttendee = pickCalendarAttendee(refreshed, attendee.email, account.email);
|
|
924
|
+
return {
|
|
925
|
+
is_invite: true,
|
|
926
|
+
rsvp_supported: true,
|
|
927
|
+
response_status: refreshedAttendee?.response_status ?? mapGoogleCalendarResponseStatus(googleCalendarResponseStatusForRsvp(response)),
|
|
928
|
+
available_rsvp_responses: ["accept", "decline", "tentative"],
|
|
929
|
+
};
|
|
930
|
+
}
|
|
657
931
|
async function refreshRefsFromGmailResponse(account, context, response) {
|
|
658
932
|
const threadId = response.threadId ?? null;
|
|
659
933
|
if (threadId) {
|
|
@@ -754,11 +1028,13 @@ export class GmailApiAdapter {
|
|
|
754
1028
|
: { status: "unauthenticated", detail: status.detail };
|
|
755
1029
|
}
|
|
756
1030
|
async search(account, query, context) {
|
|
757
|
-
|
|
1031
|
+
const queryText = buildGmailSearchQuery(query);
|
|
1032
|
+
const threads = await fetchGmailThreads(account, context, {
|
|
758
1033
|
kind: "search",
|
|
759
|
-
queryText:
|
|
1034
|
+
...(queryText ? { queryText } : {}),
|
|
760
1035
|
limit: query.limit,
|
|
761
1036
|
});
|
|
1037
|
+
return threads.filter((thread) => threadMatchesStructuredFilters(thread, query)).slice(0, query.limit);
|
|
762
1038
|
}
|
|
763
1039
|
async fetchUnread(account, query, context) {
|
|
764
1040
|
return fetchGmailThreads(account, context, {
|
|
@@ -766,6 +1042,23 @@ export class GmailApiAdapter {
|
|
|
766
1042
|
limit: query.limit,
|
|
767
1043
|
});
|
|
768
1044
|
}
|
|
1045
|
+
async refreshThread(account, threadRef, context) {
|
|
1046
|
+
const locatorRow = context.db.findProviderLocator("thread", threadRef);
|
|
1047
|
+
if (!locatorRow) {
|
|
1048
|
+
throw new SurfaceError("cache_miss", `No provider locator exists for thread '${threadRef}'.`, {
|
|
1049
|
+
account: account.name,
|
|
1050
|
+
threadRef,
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
const locator = JSON.parse(locatorRow.locator_json);
|
|
1054
|
+
if (!locator.thread_id) {
|
|
1055
|
+
throw new SurfaceError("transport_error", `Thread '${threadRef}' is missing a Gmail thread id.`, {
|
|
1056
|
+
account: account.name,
|
|
1057
|
+
threadRef,
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
await fetchAndPersistGmailThread(account, context, locator.thread_id);
|
|
1061
|
+
}
|
|
769
1062
|
async readMessage(account, messageRef, refresh, context) {
|
|
770
1063
|
const stored = context.db.getStoredMessage(messageRef);
|
|
771
1064
|
if (!stored) {
|
|
@@ -884,8 +1177,16 @@ export class GmailApiAdapter {
|
|
|
884
1177
|
},
|
|
885
1178
|
};
|
|
886
1179
|
}
|
|
887
|
-
async rsvp(account,
|
|
888
|
-
|
|
1180
|
+
async rsvp(account, messageRef, response, context) {
|
|
1181
|
+
const target = await resolveGmailRsvpTarget(account, messageRef, context);
|
|
1182
|
+
const invite = await performGmailRsvp(account, context, target, response);
|
|
1183
|
+
context.db.updateInviteForThread(target.threadRef, {
|
|
1184
|
+
is_invite: true,
|
|
1185
|
+
rsvp_supported: invite.rsvp_supported,
|
|
1186
|
+
response_status: invite.response_status,
|
|
1187
|
+
available_rsvp_responses: invite.available_rsvp_responses,
|
|
1188
|
+
});
|
|
1189
|
+
return buildRsvpEnvelope(account, messageRef, target.threadRef, response, invite);
|
|
889
1190
|
}
|
|
890
1191
|
async sendMessage(account, input, context) {
|
|
891
1192
|
const recipients = {
|