@executor-js/emulate 0.6.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 (48) hide show
  1. package/README.md +1044 -0
  2. package/dist/api.d.ts +24 -0
  3. package/dist/api.js +2665 -0
  4. package/dist/api.js.map +1 -0
  5. package/dist/chunk-D6EKRYGP.js +1615 -0
  6. package/dist/chunk-D6EKRYGP.js.map +1 -0
  7. package/dist/chunk-WVQMFHQM.js +83 -0
  8. package/dist/chunk-WVQMFHQM.js.map +1 -0
  9. package/dist/dist-7FDUSG5I.js +24368 -0
  10. package/dist/dist-7FDUSG5I.js.map +1 -0
  11. package/dist/dist-7N4COJHK.js +1814 -0
  12. package/dist/dist-7N4COJHK.js.map +1 -0
  13. package/dist/dist-BTEY33DJ.js +2334 -0
  14. package/dist/dist-BTEY33DJ.js.map +1 -0
  15. package/dist/dist-DK26ESP2.js +595 -0
  16. package/dist/dist-DK26ESP2.js.map +1 -0
  17. package/dist/dist-IYZPDKJW.js +1284 -0
  18. package/dist/dist-IYZPDKJW.js.map +1 -0
  19. package/dist/dist-JJ2ZRCAX.js +189 -0
  20. package/dist/dist-JJ2ZRCAX.js.map +1 -0
  21. package/dist/dist-K4CVTD6K.js +1570 -0
  22. package/dist/dist-K4CVTD6K.js.map +1 -0
  23. package/dist/dist-M3GVASMR.js +1254 -0
  24. package/dist/dist-M3GVASMR.js.map +1 -0
  25. package/dist/dist-OYYGWKZQ.js +1533 -0
  26. package/dist/dist-OYYGWKZQ.js.map +1 -0
  27. package/dist/dist-P3SBBRFR.js +3169 -0
  28. package/dist/dist-P3SBBRFR.js.map +1 -0
  29. package/dist/dist-RMPDKZUA.js +1183 -0
  30. package/dist/dist-RMPDKZUA.js.map +1 -0
  31. package/dist/dist-WBKONLOE.js +2154 -0
  32. package/dist/dist-WBKONLOE.js.map +1 -0
  33. package/dist/dist-XM5HSBDC.js +1090 -0
  34. package/dist/dist-XM5HSBDC.js.map +1 -0
  35. package/dist/dist-XVVIYXQG.js +4241 -0
  36. package/dist/dist-XVVIYXQG.js.map +1 -0
  37. package/dist/dist-YPRJYQHW.js +5109 -0
  38. package/dist/dist-YPRJYQHW.js.map +1 -0
  39. package/dist/dist-ZEC77OKZ.js +913 -0
  40. package/dist/dist-ZEC77OKZ.js.map +1 -0
  41. package/dist/fonts/GeistPixel-Square.woff2 +0 -0
  42. package/dist/fonts/favicon.ico +0 -0
  43. package/dist/fonts/geist-sans.woff2 +0 -0
  44. package/dist/helpers-LXLP3DFE-LBOTATT5.js +17 -0
  45. package/dist/helpers-LXLP3DFE-LBOTATT5.js.map +1 -0
  46. package/dist/index.js +3005 -0
  47. package/dist/index.js.map +1 -0
  48. package/package.json +83 -0
@@ -0,0 +1,4241 @@
1
+ import {
2
+ SignJWT
3
+ } from "./chunk-D6EKRYGP.js";
4
+
5
+ // ../@emulators/google/dist/index.js
6
+ import { randomBytes } from "crypto";
7
+ import { createHash, randomBytes as randomBytes2 } from "crypto";
8
+ import { timingSafeEqual } from "crypto";
9
+ var HISTORY_CHANGE_TYPES = /* @__PURE__ */ new Set([
10
+ "messageAdded",
11
+ "messageDeleted",
12
+ "labelAdded",
13
+ "labelRemoved"
14
+ ]);
15
+ function isHistoryChangeType(value) {
16
+ return HISTORY_CHANGE_TYPES.has(value);
17
+ }
18
+ var SYSTEM_LABELS = [
19
+ { gmail_id: "INBOX", name: "INBOX", message_list_visibility: "show", label_list_visibility: "labelShow" },
20
+ { gmail_id: "SENT", name: "SENT", message_list_visibility: "show", label_list_visibility: "labelShow" },
21
+ { gmail_id: "UNREAD", name: "UNREAD", message_list_visibility: "show", label_list_visibility: "labelShow" },
22
+ { gmail_id: "STARRED", name: "STARRED", message_list_visibility: "show", label_list_visibility: "labelShow" },
23
+ { gmail_id: "IMPORTANT", name: "IMPORTANT", message_list_visibility: "show", label_list_visibility: "labelShow" },
24
+ { gmail_id: "TRASH", name: "TRASH", message_list_visibility: "show", label_list_visibility: "labelShow" },
25
+ { gmail_id: "SPAM", name: "SPAM", message_list_visibility: "show", label_list_visibility: "labelShow" },
26
+ { gmail_id: "DRAFT", name: "DRAFT", message_list_visibility: "hide", label_list_visibility: "labelHide" },
27
+ {
28
+ gmail_id: "CATEGORY_PERSONAL",
29
+ name: "CATEGORY_PERSONAL",
30
+ message_list_visibility: "hide",
31
+ label_list_visibility: "labelHide"
32
+ },
33
+ {
34
+ gmail_id: "CATEGORY_SOCIAL",
35
+ name: "CATEGORY_SOCIAL",
36
+ message_list_visibility: "hide",
37
+ label_list_visibility: "labelHide"
38
+ },
39
+ {
40
+ gmail_id: "CATEGORY_PROMOTIONS",
41
+ name: "CATEGORY_PROMOTIONS",
42
+ message_list_visibility: "hide",
43
+ label_list_visibility: "labelHide"
44
+ },
45
+ {
46
+ gmail_id: "CATEGORY_UPDATES",
47
+ name: "CATEGORY_UPDATES",
48
+ message_list_visibility: "hide",
49
+ label_list_visibility: "labelHide"
50
+ },
51
+ {
52
+ gmail_id: "CATEGORY_FORUMS",
53
+ name: "CATEGORY_FORUMS",
54
+ message_list_visibility: "hide",
55
+ label_list_visibility: "labelHide"
56
+ }
57
+ ];
58
+ var SYSTEM_LABEL_IDS = new Set(SYSTEM_LABELS.map((label) => label.gmail_id));
59
+ var LABEL_ALIASES = {
60
+ inbox: "INBOX",
61
+ sent: "SENT",
62
+ draft: "DRAFT",
63
+ drafts: "DRAFT",
64
+ unread: "UNREAD",
65
+ starred: "STARRED",
66
+ important: "IMPORTANT",
67
+ spam: "SPAM",
68
+ trash: "TRASH",
69
+ personal: "CATEGORY_PERSONAL",
70
+ social: "CATEGORY_SOCIAL",
71
+ promotions: "CATEGORY_PROMOTIONS",
72
+ updates: "CATEGORY_UPDATES",
73
+ forums: "CATEGORY_FORUMS"
74
+ };
75
+ var lastGeneratedHistoryId = 0n;
76
+ function generateUid(prefix = "") {
77
+ const id = randomBytes(12).toString("base64url").slice(0, 20);
78
+ return prefix ? `${prefix}_${id}` : id;
79
+ }
80
+ function generateDraftId() {
81
+ const entropy = randomBytes(4).readUInt32BE(0).toString();
82
+ return `r-${Date.now()}${entropy}`;
83
+ }
84
+ function generateHistoryId() {
85
+ const entropy = randomBytes(3).readUIntBE(0, 3).toString().padStart(8, "0");
86
+ let next = BigInt(`${Date.now()}${entropy}`);
87
+ if (next <= lastGeneratedHistoryId) {
88
+ next = lastGeneratedHistoryId + 1n;
89
+ }
90
+ lastGeneratedHistoryId = next;
91
+ return next.toString();
92
+ }
93
+ function getAuthenticatedEmail(c) {
94
+ const authUser = c.get("authUser");
95
+ return authUser?.login ?? null;
96
+ }
97
+ function matchesRequestedUser(userId, authEmail) {
98
+ return userId === "me" || userId === authEmail;
99
+ }
100
+ function googleApiError(c, code, message, reason, status) {
101
+ return c.json(
102
+ {
103
+ error: {
104
+ code,
105
+ message,
106
+ errors: [
107
+ {
108
+ message,
109
+ domain: "global",
110
+ reason
111
+ }
112
+ ],
113
+ status
114
+ }
115
+ },
116
+ code
117
+ );
118
+ }
119
+ function parseFormat(value) {
120
+ if (value === "metadata" || value === "minimal" || value === "raw") return value;
121
+ return "full";
122
+ }
123
+ function parseOffset(value) {
124
+ if (!value) return 0;
125
+ const parsed = Number.parseInt(value, 10);
126
+ if (Number.isNaN(parsed) || parsed < 0) return 0;
127
+ return parsed;
128
+ }
129
+ function normalizeLimit(value, fallback, max = 500) {
130
+ if (!value) return fallback;
131
+ const parsed = Number.parseInt(value, 10);
132
+ if (Number.isNaN(parsed) || parsed <= 0) return fallback;
133
+ return Math.max(1, Math.min(max, parsed));
134
+ }
135
+ function parseBooleanParam(value) {
136
+ return value === "true" || value === "1";
137
+ }
138
+ function ensureSystemLabels(gs, userEmail) {
139
+ const existingIds = new Set(gs.labels.findBy("user_email", userEmail).map((row) => row.gmail_id));
140
+ for (const label of SYSTEM_LABELS) {
141
+ if (existingIds.has(label.gmail_id)) continue;
142
+ gs.labels.insert({
143
+ gmail_id: label.gmail_id,
144
+ user_email: userEmail,
145
+ name: label.name,
146
+ type: "system",
147
+ message_list_visibility: label.message_list_visibility,
148
+ label_list_visibility: label.label_list_visibility,
149
+ color_background: null,
150
+ color_text: null
151
+ });
152
+ }
153
+ }
154
+ function ensureCustomLabel(gs, userEmail, labelId, name = labelId) {
155
+ ensureSystemLabels(gs, userEmail);
156
+ const existing = findLabelById(gs, userEmail, labelId);
157
+ if (existing) return existing;
158
+ return gs.labels.insert({
159
+ gmail_id: labelId,
160
+ user_email: userEmail,
161
+ name,
162
+ type: "user",
163
+ message_list_visibility: "show",
164
+ label_list_visibility: "labelShow",
165
+ color_background: null,
166
+ color_text: null
167
+ });
168
+ }
169
+ function createLabelRecord(gs, input) {
170
+ ensureSystemLabels(gs, input.user_email);
171
+ const labelId = input.gmail_id ?? `Label_${randomBytes(8).toString("hex")}`;
172
+ return gs.labels.insert({
173
+ gmail_id: labelId,
174
+ user_email: input.user_email,
175
+ name: input.name,
176
+ type: input.type ?? "user",
177
+ message_list_visibility: input.message_list_visibility ?? "show",
178
+ label_list_visibility: input.label_list_visibility ?? "labelShow",
179
+ color_background: input.color_background ?? null,
180
+ color_text: input.color_text ?? null
181
+ });
182
+ }
183
+ function updateLabelRecord(gs, label, input) {
184
+ return gs.labels.update(label.id, {
185
+ name: input.name !== void 0 ? input.name : label.name,
186
+ message_list_visibility: input.message_list_visibility !== void 0 ? input.message_list_visibility : label.message_list_visibility,
187
+ label_list_visibility: input.label_list_visibility !== void 0 ? input.label_list_visibility : label.label_list_visibility,
188
+ color_background: input.color_background !== void 0 ? input.color_background : label.color_background,
189
+ color_text: input.color_text !== void 0 ? input.color_text : label.color_text
190
+ }) ?? label;
191
+ }
192
+ function isSystemLabelId(labelId) {
193
+ return SYSTEM_LABEL_IDS.has(labelId);
194
+ }
195
+ function findLabelById(gs, userEmail, labelId) {
196
+ return gs.labels.findBy("user_email", userEmail).find((label) => label.gmail_id === labelId);
197
+ }
198
+ function findLabelByName(gs, userEmail, name) {
199
+ const normalized = name.trim().toLowerCase();
200
+ return gs.labels.findBy("user_email", userEmail).find((label) => label.name.trim().toLowerCase() === normalized);
201
+ }
202
+ function listLabelsForUser(gs, userEmail) {
203
+ ensureSystemLabels(gs, userEmail);
204
+ return gs.labels.findBy("user_email", userEmail).sort((a, b) => {
205
+ if (a.type !== b.type) return a.type === "system" ? -1 : 1;
206
+ return a.name.localeCompare(b.name);
207
+ });
208
+ }
209
+ function computeLabelStats(gs, userEmail) {
210
+ const stats = /* @__PURE__ */ new Map();
211
+ const messages = gs.messages.findBy("user_email", userEmail);
212
+ const isUnread = (message) => message.label_ids.includes("UNREAD");
213
+ for (const message of messages) {
214
+ for (const labelId of message.label_ids) {
215
+ let entry = stats.get(labelId);
216
+ if (!entry) {
217
+ entry = { messagesTotal: 0, messagesUnread: 0, threadsTotal: /* @__PURE__ */ new Set(), threadsUnread: /* @__PURE__ */ new Set() };
218
+ stats.set(labelId, entry);
219
+ }
220
+ entry.messagesTotal++;
221
+ entry.threadsTotal.add(message.thread_id);
222
+ if (isUnread(message)) {
223
+ entry.messagesUnread++;
224
+ entry.threadsUnread.add(message.thread_id);
225
+ }
226
+ }
227
+ }
228
+ return stats;
229
+ }
230
+ function formatLabelWithStats(label, stats) {
231
+ return {
232
+ id: label.gmail_id,
233
+ name: label.name,
234
+ type: label.type === "system" ? "system" : "user",
235
+ messageListVisibility: label.message_list_visibility ?? void 0,
236
+ labelListVisibility: label.label_list_visibility ?? void 0,
237
+ messagesTotal: stats?.messagesTotal ?? 0,
238
+ messagesUnread: stats?.messagesUnread ?? 0,
239
+ threadsTotal: stats?.threadsTotal.size ?? 0,
240
+ threadsUnread: stats?.threadsUnread.size ?? 0,
241
+ color: label.color_background || label.color_text ? {
242
+ backgroundColor: label.color_background ?? void 0,
243
+ textColor: label.color_text ?? void 0
244
+ } : void 0
245
+ };
246
+ }
247
+ function formatLabelResource(gs, label) {
248
+ const stats = computeLabelStats(gs, label.user_email);
249
+ return formatLabelWithStats(label, stats.get(label.gmail_id));
250
+ }
251
+ function formatLabelResources(gs, labels) {
252
+ if (labels.length === 0) return [];
253
+ const stats = computeLabelStats(gs, labels[0].user_email);
254
+ return labels.map((label) => formatLabelWithStats(label, stats.get(label.gmail_id)));
255
+ }
256
+ function normalizeLabelQuery(value) {
257
+ const cleaned = cleanToken(value);
258
+ const alias = LABEL_ALIASES[cleaned.toLowerCase()];
259
+ return alias ?? cleaned;
260
+ }
261
+ function findMissingLabelIds(gs, userEmail, labelIds) {
262
+ ensureSystemLabels(gs, userEmail);
263
+ return labelIds.filter((labelId) => !findLabelById(gs, userEmail, labelId));
264
+ }
265
+ function dedupeLabelIds(labelIds) {
266
+ return [...new Set(labelIds.filter(Boolean))];
267
+ }
268
+ function createStoredMessage(gs, input, options) {
269
+ ensureSystemLabels(gs, input.user_email);
270
+ const parsedRaw = input.raw ? parseRawMessage(input.raw) : null;
271
+ const merged = {
272
+ raw: input.raw ?? null,
273
+ from: input.from ?? parsedRaw?.from ?? "",
274
+ to: input.to ?? parsedRaw?.to ?? "",
275
+ cc: input.cc ?? parsedRaw?.cc ?? null,
276
+ bcc: input.bcc ?? parsedRaw?.bcc ?? null,
277
+ reply_to: input.reply_to ?? parsedRaw?.reply_to ?? null,
278
+ subject: input.subject ?? parsedRaw?.subject ?? "",
279
+ body_text: input.body_text ?? parsedRaw?.body_text ?? null,
280
+ body_html: input.body_html ?? parsedRaw?.body_html ?? null,
281
+ message_id: input.message_id ?? parsedRaw?.message_id ?? null,
282
+ references: input.references ?? parsedRaw?.references ?? null,
283
+ in_reply_to: input.in_reply_to ?? parsedRaw?.in_reply_to ?? null,
284
+ date_header: input.date ?? parsedRaw?.date_header ?? null
285
+ };
286
+ const internalDateMs = resolveInternalDate(input.internal_date ?? input.date ?? parsedRaw?.date_header ?? void 0);
287
+ const gmailId = input.gmail_id ?? generateUid("msg");
288
+ const threadId = resolveThreadId(gs, input.user_email, input.thread_id, merged.in_reply_to, merged.references);
289
+ const messageId = merged.message_id ?? `<${gmailId}@emulate.google.local>`;
290
+ const baseLabelIds = dedupeLabelIds(input.label_ids ?? options?.defaultLabelIds ?? []);
291
+ if (options?.createMissingCustomLabels) {
292
+ for (const labelId of baseLabelIds.filter((labelId2) => !isSystemLabelId(labelId2))) {
293
+ ensureCustomLabel(gs, input.user_email, labelId);
294
+ }
295
+ }
296
+ const labelIds = applyFiltersToLabelIds(gs, input.user_email, merged.from, baseLabelIds);
297
+ const snippet = input.snippet?.trim() || deriveSnippet(merged.body_text ?? merged.body_html ?? merged.subject) || merged.subject;
298
+ const raw = merged.raw ?? buildRawMessage({
299
+ from: merged.from,
300
+ to: merged.to,
301
+ cc: merged.cc,
302
+ bcc: merged.bcc,
303
+ reply_to: merged.reply_to,
304
+ subject: merged.subject,
305
+ body_text: merged.body_text,
306
+ body_html: merged.body_html,
307
+ message_id: messageId,
308
+ references: merged.references,
309
+ in_reply_to: merged.in_reply_to,
310
+ date_header: new Date(internalDateMs).toUTCString()
311
+ });
312
+ const historyId = generateHistoryId();
313
+ const message = gs.messages.insert({
314
+ gmail_id: gmailId,
315
+ thread_id: threadId,
316
+ user_email: input.user_email,
317
+ history_id: historyId,
318
+ internal_date: String(internalDateMs),
319
+ raw,
320
+ label_ids: labelIds,
321
+ snippet,
322
+ subject: merged.subject,
323
+ from: merged.from,
324
+ to: merged.to,
325
+ cc: merged.cc,
326
+ bcc: merged.bcc,
327
+ reply_to: merged.reply_to,
328
+ message_id: messageId,
329
+ references: merged.references,
330
+ in_reply_to: merged.in_reply_to,
331
+ date_header: new Date(internalDateMs).toUTCString(),
332
+ body_text: merged.body_text,
333
+ body_html: merged.body_html
334
+ });
335
+ replaceMessageAttachments(gs, message, parsedRaw?.attachments ?? []);
336
+ recordHistoryEvents(gs, message.user_email, historyId, [
337
+ {
338
+ change_type: "messageAdded",
339
+ message_gmail_id: message.gmail_id,
340
+ thread_id: message.thread_id,
341
+ label_ids: message.label_ids
342
+ }
343
+ ]);
344
+ syncDraftState(gs, message);
345
+ return message;
346
+ }
347
+ function updateStoredMessage(gs, message, input) {
348
+ const parsedRaw = input.raw ? parseRawMessage(input.raw) : null;
349
+ const internalDateMs = resolveInternalDate(
350
+ input.internal_date ?? input.date ?? parsedRaw?.date_header ?? Date.now().toString()
351
+ );
352
+ const merged = {
353
+ raw: input.raw ?? message.raw,
354
+ from: input.from ?? parsedRaw?.from ?? message.from,
355
+ to: input.to ?? parsedRaw?.to ?? message.to,
356
+ cc: input.cc ?? parsedRaw?.cc ?? message.cc,
357
+ bcc: input.bcc ?? parsedRaw?.bcc ?? message.bcc,
358
+ reply_to: input.reply_to ?? parsedRaw?.reply_to ?? message.reply_to,
359
+ subject: input.subject ?? parsedRaw?.subject ?? message.subject,
360
+ body_text: input.body_text ?? parsedRaw?.body_text ?? message.body_text,
361
+ body_html: input.body_html ?? parsedRaw?.body_html ?? message.body_html,
362
+ message_id: input.message_id ?? parsedRaw?.message_id ?? message.message_id,
363
+ references: input.references ?? parsedRaw?.references ?? message.references,
364
+ in_reply_to: input.in_reply_to ?? parsedRaw?.in_reply_to ?? message.in_reply_to,
365
+ date_header: input.date ?? parsedRaw?.date_header ?? message.date_header
366
+ };
367
+ const snippet = input.snippet?.trim() || deriveSnippet(merged.body_text ?? merged.body_html ?? merged.subject) || merged.subject;
368
+ const labelIds = dedupeLabelIds(input.label_ids ?? message.label_ids);
369
+ const raw = merged.raw ?? buildRawMessage({
370
+ from: merged.from,
371
+ to: merged.to,
372
+ cc: merged.cc,
373
+ bcc: merged.bcc,
374
+ reply_to: merged.reply_to,
375
+ subject: merged.subject,
376
+ body_text: merged.body_text,
377
+ body_html: merged.body_html,
378
+ message_id: merged.message_id,
379
+ references: merged.references,
380
+ in_reply_to: merged.in_reply_to,
381
+ date_header: new Date(internalDateMs).toUTCString()
382
+ });
383
+ const updated = gs.messages.update(message.id, {
384
+ thread_id: input.thread_id ?? message.thread_id,
385
+ history_id: generateHistoryId(),
386
+ internal_date: String(internalDateMs),
387
+ raw,
388
+ label_ids: labelIds,
389
+ snippet,
390
+ subject: merged.subject,
391
+ from: merged.from,
392
+ to: merged.to,
393
+ cc: merged.cc,
394
+ bcc: merged.bcc,
395
+ reply_to: merged.reply_to,
396
+ message_id: merged.message_id,
397
+ references: merged.references,
398
+ in_reply_to: merged.in_reply_to,
399
+ date_header: new Date(internalDateMs).toUTCString(),
400
+ body_text: merged.body_text,
401
+ body_html: merged.body_html
402
+ }) ?? message;
403
+ replaceMessageAttachments(gs, updated, parsedRaw?.attachments ?? []);
404
+ syncDraftState(gs, updated);
405
+ return updated;
406
+ }
407
+ function getMessageById(gs, userEmail, messageId) {
408
+ return gs.messages.findBy("user_email", userEmail).find((message) => message.gmail_id === messageId);
409
+ }
410
+ function getDraftById(gs, userEmail, draftId) {
411
+ return gs.drafts.findBy("user_email", userEmail).find((draft) => draft.gmail_id === draftId);
412
+ }
413
+ function getDraftMessage(gs, draft) {
414
+ return getMessageById(gs, draft.user_email, draft.message_gmail_id);
415
+ }
416
+ function getAttachmentById(gs, userEmail, messageId, attachmentId) {
417
+ return gs.attachments.findBy("message_gmail_id", messageId).find((attachment) => attachment.user_email === userEmail && attachment.gmail_id === attachmentId);
418
+ }
419
+ function listDraftsForUser(gs, userEmail) {
420
+ const drafts = gs.drafts.findBy("user_email", userEmail);
421
+ const messageMap = /* @__PURE__ */ new Map();
422
+ for (const draft of drafts) {
423
+ messageMap.set(draft.gmail_id, getDraftMessage(gs, draft));
424
+ }
425
+ return drafts.filter((draft) => {
426
+ const message = messageMap.get(draft.gmail_id);
427
+ return Boolean(message && message.label_ids.includes("DRAFT") && !message.label_ids.includes("SENT"));
428
+ }).sort((a, b) => {
429
+ const aMessage = messageMap.get(a.gmail_id);
430
+ const bMessage = messageMap.get(b.gmail_id);
431
+ return Number(bMessage?.internal_date ?? 0) - Number(aMessage?.internal_date ?? 0);
432
+ });
433
+ }
434
+ function formatDraftResource(gs, draft, format, metadataHeaders = []) {
435
+ const message = getDraftMessage(gs, draft);
436
+ if (!message) return { id: draft.gmail_id };
437
+ return {
438
+ id: draft.gmail_id,
439
+ message: formatMessageResource(gs, message, format, metadataHeaders)
440
+ };
441
+ }
442
+ function createDraftMessage(gs, input) {
443
+ const message = createStoredMessage(gs, {
444
+ ...input,
445
+ label_ids: dedupeLabelIds([...(input.label_ids ?? []).filter((labelId) => labelId !== "SENT"), "DRAFT"])
446
+ });
447
+ const draft = syncDraftState(gs, message);
448
+ return { draft, message };
449
+ }
450
+ function updateDraftMessage(gs, draft, input) {
451
+ const message = getDraftMessage(gs, draft);
452
+ if (!message) return null;
453
+ const updated = updateStoredMessage(gs, message, {
454
+ ...input,
455
+ label_ids: dedupeLabelIds([...(message.label_ids ?? []).filter((labelId) => labelId !== "SENT"), "DRAFT"])
456
+ });
457
+ return { draft: syncDraftState(gs, updated, draft.gmail_id) ?? draft, message: updated };
458
+ }
459
+ function sendDraftMessage(gs, draft) {
460
+ const message = getDraftMessage(gs, draft);
461
+ if (!message) {
462
+ gs.drafts.delete(draft.id);
463
+ return null;
464
+ }
465
+ const sent = markMessageModified(
466
+ gs,
467
+ message,
468
+ message.label_ids.filter((labelId) => labelId !== "DRAFT").concat("SENT")
469
+ );
470
+ clearDraftRecordsForMessage(gs, message.user_email, message.gmail_id);
471
+ return sent;
472
+ }
473
+ function deleteDraftMessage(gs, draft) {
474
+ const message = getDraftMessage(gs, draft);
475
+ if (!message) return gs.drafts.delete(draft.id);
476
+ return deleteMessage(gs, message);
477
+ }
478
+ function getCurrentHistoryId(gs, userEmail) {
479
+ const historyIds = [
480
+ ...gs.messages.findBy("user_email", userEmail).map((message) => message.history_id),
481
+ ...gs.history.findBy("user_email", userEmail).map((event) => event.gmail_id)
482
+ ].filter(Boolean);
483
+ if (historyIds.length === 0) return "0";
484
+ return historyIds.reduce((latest, current) => compareHistoryIds(current, latest) > 0 ? current : latest);
485
+ }
486
+ function listHistoryForUser(gs, userEmail, options) {
487
+ const requestedTypes = options.historyTypes?.length ? new Set(options.historyTypes) : null;
488
+ const events = gs.history.findBy("user_email", userEmail).filter((event) => compareHistoryIds(event.gmail_id, options.startHistoryId) > 0).filter((event) => !requestedTypes || requestedTypes.has(event.change_type)).filter((event) => !options.labelId || event.label_ids.includes(options.labelId)).sort((a, b) => compareHistoryIds(a.gmail_id, b.gmail_id) || a.id - b.id);
489
+ const grouped = /* @__PURE__ */ new Map();
490
+ for (const event of events) {
491
+ const existing = grouped.get(event.gmail_id);
492
+ if (existing) existing.push(event);
493
+ else grouped.set(event.gmail_id, [event]);
494
+ }
495
+ const historyEntries = Array.from(grouped.entries()).map(
496
+ ([historyId, entries]) => formatHistoryEntry(gs, userEmail, historyId, entries)
497
+ );
498
+ const offset = parseOffset(options.pageToken);
499
+ const limit = Math.max(1, Math.min(options.maxResults ?? 100, 500));
500
+ const page = historyEntries.slice(offset, offset + limit);
501
+ const nextPageToken = offset + limit < historyEntries.length ? String(offset + limit) : void 0;
502
+ return {
503
+ history: page,
504
+ historyId: getCurrentHistoryId(gs, userEmail),
505
+ nextPageToken
506
+ };
507
+ }
508
+ function getFilterById(gs, userEmail, filterId) {
509
+ return gs.filters.findBy("user_email", userEmail).find((filter) => filter.gmail_id === filterId);
510
+ }
511
+ function listFiltersForUser(gs, userEmail) {
512
+ return gs.filters.findBy("user_email", userEmail).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.gmail_id.localeCompare(b.gmail_id));
513
+ }
514
+ function findMatchingFilter(gs, input) {
515
+ const criteriaFrom = normalizeFilterFrom(input.criteria_from);
516
+ const addLabelIds = sortStrings(dedupeLabelIds(input.add_label_ids ?? []));
517
+ const removeLabelIds = sortStrings(dedupeLabelIds(input.remove_label_ids ?? []));
518
+ return gs.filters.findBy("user_email", input.user_email).find(
519
+ (filter) => normalizeFilterFrom(filter.criteria_from) === criteriaFrom && arrayEquals(sortStrings(filter.add_label_ids), addLabelIds) && arrayEquals(sortStrings(filter.remove_label_ids), removeLabelIds)
520
+ );
521
+ }
522
+ function createFilterRecord(gs, input) {
523
+ return gs.filters.insert({
524
+ gmail_id: input.gmail_id ?? generateUid("filter"),
525
+ user_email: input.user_email,
526
+ criteria_from: normalizeFilterFrom(input.criteria_from),
527
+ add_label_ids: dedupeLabelIds(input.add_label_ids ?? []),
528
+ remove_label_ids: dedupeLabelIds(input.remove_label_ids ?? [])
529
+ });
530
+ }
531
+ function formatFilterResource(filter) {
532
+ return {
533
+ id: filter.gmail_id,
534
+ criteria: filter.criteria_from ? { from: filter.criteria_from } : {},
535
+ action: {
536
+ ...filter.add_label_ids.length > 0 ? { addLabelIds: filter.add_label_ids } : {},
537
+ ...filter.remove_label_ids.length > 0 ? { removeLabelIds: filter.remove_label_ids } : {}
538
+ }
539
+ };
540
+ }
541
+ function listForwardingAddressesForUser(gs, userEmail) {
542
+ return gs.forwardingAddresses.findBy("user_email", userEmail).sort((a, b) => a.forwarding_email.localeCompare(b.forwarding_email));
543
+ }
544
+ function formatForwardingAddressResource(entry) {
545
+ return {
546
+ forwardingEmail: entry.forwarding_email,
547
+ verificationStatus: entry.verification_status
548
+ };
549
+ }
550
+ function listSendAsForUser(gs, userEmail) {
551
+ ensureDefaultSendAs(gs, userEmail);
552
+ return gs.sendAs.findBy("user_email", userEmail).sort((a, b) => Number(b.is_default) - Number(a.is_default) || a.send_as_email.localeCompare(b.send_as_email));
553
+ }
554
+ function formatSendAsResource(entry) {
555
+ return {
556
+ sendAsEmail: entry.send_as_email,
557
+ displayName: entry.display_name ?? void 0,
558
+ replyToAddress: entry.send_as_email,
559
+ signature: entry.signature,
560
+ isPrimary: entry.is_default,
561
+ isDefault: entry.is_default,
562
+ treatAsAlias: false,
563
+ verificationStatus: "accepted"
564
+ };
565
+ }
566
+ function listMessagesForUser(gs, userEmail, options) {
567
+ let messages = gs.messages.findBy("user_email", userEmail);
568
+ if (!options?.includeSpamTrash) {
569
+ messages = messages.filter(
570
+ (message) => !message.label_ids.includes("TRASH") && !message.label_ids.includes("SPAM")
571
+ );
572
+ }
573
+ if (options?.labelIds?.length) {
574
+ messages = messages.filter((message) => options.labelIds.every((labelId) => message.label_ids.includes(labelId)));
575
+ }
576
+ if (options?.query) {
577
+ const matcher = buildMessageQueryMatcher(gs, userEmail, options.query);
578
+ messages = messages.filter(matcher);
579
+ }
580
+ return sortMessagesByDateDesc(messages);
581
+ }
582
+ function groupThreads(messages) {
583
+ const threadMap = /* @__PURE__ */ new Map();
584
+ for (const message of messages) {
585
+ const existing = threadMap.get(message.thread_id);
586
+ if (existing) existing.push(message);
587
+ else threadMap.set(message.thread_id, [message]);
588
+ }
589
+ return Array.from(threadMap.entries()).map(([threadId, entries]) => {
590
+ const ordered = sortMessagesByDateAsc(entries);
591
+ const latest = ordered.at(-1);
592
+ return {
593
+ id: threadId,
594
+ snippet: latest.snippet,
595
+ historyId: latest.history_id,
596
+ messages: ordered
597
+ };
598
+ }).sort((a, b) => Number(b.messages.at(-1)?.internal_date ?? 0) - Number(a.messages.at(-1)?.internal_date ?? 0));
599
+ }
600
+ function getThreadMessages(gs, userEmail, threadId, options) {
601
+ let messages = gs.messages.findBy("user_email", userEmail).filter((message) => message.thread_id === threadId);
602
+ if (!options?.includeSpamTrash) {
603
+ messages = messages.filter(
604
+ (message) => !message.label_ids.includes("TRASH") && !message.label_ids.includes("SPAM")
605
+ );
606
+ }
607
+ return sortMessagesByDateAsc(messages);
608
+ }
609
+ function formatMessageResource(gs, message, format, metadataHeaders = []) {
610
+ const headers = buildHeaders(message);
611
+ const filteredHeaders = format === "metadata" && metadataHeaders.length > 0 ? headers.filter((header) => metadataHeaders.includes(header.name)) : headers;
612
+ const base = {
613
+ id: message.gmail_id,
614
+ threadId: message.thread_id,
615
+ labelIds: message.label_ids,
616
+ snippet: message.snippet,
617
+ historyId: message.history_id,
618
+ internalDate: message.internal_date,
619
+ sizeEstimate: estimateSize(message, headers)
620
+ };
621
+ if (format === "minimal") return base;
622
+ if (format === "raw") return { ...base, raw: message.raw ?? void 0 };
623
+ return {
624
+ ...base,
625
+ payload: buildPayload(gs, message, filteredHeaders, format)
626
+ };
627
+ }
628
+ function formatThreadResource(gs, messages, format, metadataHeaders = []) {
629
+ const ordered = sortMessagesByDateAsc(messages);
630
+ const latest = ordered.at(-1);
631
+ return {
632
+ id: latest.thread_id,
633
+ historyId: latest.history_id,
634
+ snippet: latest.snippet,
635
+ messages: ordered.map((message) => formatMessageResource(gs, message, format, metadataHeaders))
636
+ };
637
+ }
638
+ function applyLabelMutation(labelIds, addLabelIds = [], removeLabelIds = []) {
639
+ const next = new Set(labelIds);
640
+ for (const labelId of addLabelIds) next.add(labelId);
641
+ for (const labelId of removeLabelIds) next.delete(labelId);
642
+ return [...next];
643
+ }
644
+ function markMessageModified(gs, message, nextLabelIds) {
645
+ const dedupedLabelIds = dedupeLabelIds(nextLabelIds);
646
+ if (arrayEquals(message.label_ids, dedupedLabelIds)) {
647
+ syncDraftState(gs, message);
648
+ return message;
649
+ }
650
+ const historyId = generateHistoryId();
651
+ const addedLabelIds = dedupedLabelIds.filter((labelId) => !message.label_ids.includes(labelId));
652
+ const removedLabelIds = message.label_ids.filter((labelId) => !dedupedLabelIds.includes(labelId));
653
+ const updated = gs.messages.update(message.id, {
654
+ label_ids: dedupedLabelIds,
655
+ history_id: historyId
656
+ }) ?? message;
657
+ const historyEvents = [];
658
+ if (addedLabelIds.length > 0) {
659
+ historyEvents.push({
660
+ change_type: "labelAdded",
661
+ message_gmail_id: updated.gmail_id,
662
+ thread_id: updated.thread_id,
663
+ label_ids: addedLabelIds
664
+ });
665
+ }
666
+ if (removedLabelIds.length > 0) {
667
+ historyEvents.push({
668
+ change_type: "labelRemoved",
669
+ message_gmail_id: updated.gmail_id,
670
+ thread_id: updated.thread_id,
671
+ label_ids: removedLabelIds
672
+ });
673
+ }
674
+ if (historyEvents.length > 0) {
675
+ recordHistoryEvents(gs, updated.user_email, historyId, historyEvents);
676
+ }
677
+ syncDraftState(gs, updated);
678
+ return updated;
679
+ }
680
+ function deleteMessage(gs, message) {
681
+ const historyId = generateHistoryId();
682
+ recordHistoryEvents(gs, message.user_email, historyId, [
683
+ {
684
+ change_type: "messageDeleted",
685
+ message_gmail_id: message.gmail_id,
686
+ thread_id: message.thread_id,
687
+ label_ids: message.label_ids
688
+ }
689
+ ]);
690
+ clearDraftRecordsForMessage(gs, message.user_email, message.gmail_id);
691
+ clearMessageAttachments(gs, message.user_email, message.gmail_id);
692
+ return gs.messages.delete(message.id);
693
+ }
694
+ function trashLabelIds(labelIds) {
695
+ const next = new Set(labelIds);
696
+ next.add("TRASH");
697
+ next.delete("INBOX");
698
+ return [...next];
699
+ }
700
+ function untrashLabelIds(labelIds) {
701
+ const next = new Set(labelIds);
702
+ next.delete("TRASH");
703
+ if (!next.has("SENT") && !next.has("DRAFT")) {
704
+ next.add("INBOX");
705
+ }
706
+ return [...next];
707
+ }
708
+ function buildMessageQueryMatcher(gs, userEmail, query) {
709
+ const terms = query.match(/"[^"]+"|\S+/g) ?? [];
710
+ const predicates = terms.flatMap((term) => buildQueryPredicates(gs, userEmail, term));
711
+ if (!predicates.length) return () => true;
712
+ return (message) => predicates.every((predicate) => predicate(message));
713
+ }
714
+ function buildRawMessage(message) {
715
+ const headers = [
716
+ `From: ${message.from}`,
717
+ `To: ${message.to}`,
718
+ ...message.cc ? [`Cc: ${message.cc}`] : [],
719
+ ...message.bcc ? [`Bcc: ${message.bcc}`] : [],
720
+ ...message.reply_to ? [`Reply-To: ${message.reply_to}`] : [],
721
+ `Subject: ${message.subject}`,
722
+ ...message.message_id ? [`Message-ID: ${message.message_id}`] : [],
723
+ ...message.references ? [`References: ${message.references}`] : [],
724
+ ...message.in_reply_to ? [`In-Reply-To: ${message.in_reply_to}`] : [],
725
+ `Date: ${message.date_header ?? (/* @__PURE__ */ new Date()).toUTCString()}`,
726
+ "MIME-Version: 1.0"
727
+ ];
728
+ const attachments = message.attachments ?? [];
729
+ if (attachments.length > 0) {
730
+ const mixedBoundary = `emulate-mixed-${randomBytes(8).toString("hex")}`;
731
+ headers.push(`Content-Type: multipart/mixed; boundary="${mixedBoundary}"`);
732
+ const parts = [];
733
+ const bodyPart2 = buildMimeBodyPart({
734
+ body_text: message.body_text,
735
+ body_html: message.body_html
736
+ });
737
+ if (bodyPart2) {
738
+ parts.push(`--${mixedBoundary}`, bodyPart2);
739
+ }
740
+ for (const attachment of attachments) {
741
+ const disposition = attachment.disposition ?? "attachment";
742
+ const contentId = attachment.content_id ? ensureWrappedContentId(attachment.content_id) : null;
743
+ parts.push(`--${mixedBoundary}`);
744
+ parts.push(`Content-Type: ${attachment.mime_type}; name="${escapeMimeParameter(attachment.filename)}"`);
745
+ parts.push(`Content-Disposition: ${disposition}; filename="${escapeMimeParameter(attachment.filename)}"`);
746
+ if (contentId) parts.push(`Content-ID: ${contentId}`);
747
+ parts.push("Content-Transfer-Encoding: base64");
748
+ parts.push("");
749
+ parts.push(wrapBase64(encodeAttachmentContent(attachment.content)));
750
+ }
751
+ parts.push(`--${mixedBoundary}--`, "");
752
+ return Buffer.from(`${headers.join("\r\n")}\r
753
+ \r
754
+ ${parts.join("\r\n")}`, "utf8").toString("base64url");
755
+ }
756
+ const bodyPart = buildMimeBodyPart({
757
+ body_text: message.body_text,
758
+ body_html: message.body_html
759
+ });
760
+ if (bodyPart) {
761
+ return Buffer.from(`${headers.join("\r\n")}\r
762
+ \r
763
+ ${bodyPart}`, "utf8").toString("base64url");
764
+ }
765
+ headers.push("Content-Type: text/plain; charset=utf-8");
766
+ return Buffer.from(`${headers.join("\r\n")}\r
767
+ \r
768
+ `, "utf8").toString("base64url");
769
+ }
770
+ function buildQueryPredicates(gs, userEmail, term) {
771
+ const cleaned = cleanToken(term);
772
+ if (!cleaned) return [];
773
+ const lower = cleaned.toLowerCase();
774
+ if (lower === "or" || lower === "and") return [];
775
+ if (lower.startsWith("-label:")) {
776
+ const labelQuery = cleaned.slice(7);
777
+ return [(message) => !messageMatchesLabelQuery(gs, userEmail, message, labelQuery)];
778
+ }
779
+ if (lower.startsWith("label:")) {
780
+ const labelQuery = cleaned.slice(6);
781
+ return [(message) => messageMatchesLabelQuery(gs, userEmail, message, labelQuery)];
782
+ }
783
+ if (lower.startsWith("in:")) {
784
+ const labelQuery = cleaned.slice(3);
785
+ return [(message) => messageMatchesLabelQuery(gs, userEmail, message, labelQuery)];
786
+ }
787
+ if (lower.startsWith("is:")) {
788
+ const state = cleaned.slice(3).toLowerCase();
789
+ if (state === "read") return [(message) => !message.label_ids.includes("UNREAD")];
790
+ return [(message) => messageMatchesLabelQuery(gs, userEmail, message, state)];
791
+ }
792
+ if (lower.startsWith("from:")) {
793
+ const value2 = cleaned.slice(5).toLowerCase();
794
+ return value2 ? [(message) => message.from.toLowerCase().includes(value2)] : [];
795
+ }
796
+ if (lower.startsWith("to:")) {
797
+ const value2 = cleaned.slice(3).toLowerCase();
798
+ return value2 ? [(message) => message.to.toLowerCase().includes(value2)] : [];
799
+ }
800
+ if (lower.startsWith("subject:")) {
801
+ const value2 = cleaned.slice(8).toLowerCase();
802
+ return value2 ? [(message) => message.subject.toLowerCase().includes(value2)] : [];
803
+ }
804
+ if (lower.startsWith("rfc822msgid:")) {
805
+ const value2 = cleaned.slice(11).replace(/[<>]/g, "").toLowerCase();
806
+ return value2 ? [(message) => message.message_id.replace(/[<>]/g, "").toLowerCase() === value2] : [];
807
+ }
808
+ if (lower.startsWith("before:")) {
809
+ const timestamp = parseDateFilter(cleaned.slice(7));
810
+ return timestamp != null ? [(message) => Number(message.internal_date) < timestamp] : [];
811
+ }
812
+ if (lower.startsWith("after:")) {
813
+ const timestamp = parseDateFilter(cleaned.slice(6));
814
+ return timestamp != null ? [(message) => Number(message.internal_date) > timestamp] : [];
815
+ }
816
+ if (lower === "has:attachment") {
817
+ return [(message) => hasMessageAttachments(gs, message)];
818
+ }
819
+ const value = cleaned.toLowerCase();
820
+ return value ? [(message) => searchableText(message).includes(value)] : [];
821
+ }
822
+ function resolveInternalDate(value) {
823
+ if (!value) return Date.now();
824
+ if (/^\d+$/.test(value)) {
825
+ const parsed2 = Number.parseInt(value, 10);
826
+ if (String(parsed2).length >= 13) return parsed2;
827
+ return parsed2 * 1e3;
828
+ }
829
+ const parsed = Date.parse(value);
830
+ return Number.isFinite(parsed) ? parsed : Date.now();
831
+ }
832
+ function formatHistoryEntry(gs, userEmail, historyId, events) {
833
+ const messages = /* @__PURE__ */ new Map();
834
+ const messagesAdded = [];
835
+ const messagesDeleted = [];
836
+ const labelsAdded = [];
837
+ const labelsRemoved = [];
838
+ for (const event of events) {
839
+ const message = formatHistoryMessageRef(gs, userEmail, event);
840
+ messages.set(event.message_gmail_id, message);
841
+ if (event.change_type === "messageAdded") {
842
+ messagesAdded.push({ message });
843
+ } else if (event.change_type === "messageDeleted") {
844
+ messagesDeleted.push({ message });
845
+ } else if (event.change_type === "labelAdded") {
846
+ labelsAdded.push({ message, labelIds: event.label_ids });
847
+ } else if (event.change_type === "labelRemoved") {
848
+ labelsRemoved.push({ message, labelIds: event.label_ids });
849
+ }
850
+ }
851
+ return {
852
+ id: historyId,
853
+ messages: Array.from(messages.values()),
854
+ ...messagesAdded.length > 0 ? { messagesAdded } : {},
855
+ ...messagesDeleted.length > 0 ? { messagesDeleted } : {},
856
+ ...labelsAdded.length > 0 ? { labelsAdded } : {},
857
+ ...labelsRemoved.length > 0 ? { labelsRemoved } : {}
858
+ };
859
+ }
860
+ function formatHistoryMessageRef(gs, userEmail, event) {
861
+ const message = getMessageById(gs, userEmail, event.message_gmail_id);
862
+ return {
863
+ id: event.message_gmail_id,
864
+ threadId: message?.thread_id ?? event.thread_id,
865
+ labelIds: message?.label_ids ?? event.label_ids,
866
+ historyId: message?.history_id ?? event.gmail_id,
867
+ ...message?.internal_date ? { internalDate: message.internal_date } : {}
868
+ };
869
+ }
870
+ function compareHistoryIds(left, right) {
871
+ try {
872
+ const leftValue = BigInt(left);
873
+ const rightValue = BigInt(right);
874
+ if (leftValue === rightValue) return 0;
875
+ return leftValue > rightValue ? 1 : -1;
876
+ } catch {
877
+ return left.localeCompare(right);
878
+ }
879
+ }
880
+ function resolveThreadId(gs, userEmail, explicitThreadId, inReplyTo, references) {
881
+ if (explicitThreadId) return explicitThreadId;
882
+ const linkedIds = [inReplyTo, references].flatMap((value) => value ? value.split(/\s+/) : []).map((value) => value.trim()).filter(Boolean);
883
+ for (const headerMessageId of linkedIds) {
884
+ const linkedMessage = gs.messages.findBy("user_email", userEmail).find((message) => message.message_id === headerMessageId);
885
+ if (linkedMessage) return linkedMessage.thread_id;
886
+ }
887
+ return generateUid("thr");
888
+ }
889
+ function replaceMessageAttachments(gs, message, attachments) {
890
+ clearMessageAttachments(gs, message.user_email, message.gmail_id);
891
+ for (const attachment of attachments) {
892
+ gs.attachments.insert({
893
+ gmail_id: generateUid("att"),
894
+ user_email: message.user_email,
895
+ message_gmail_id: message.gmail_id,
896
+ filename: attachment.filename,
897
+ mime_type: attachment.mime_type,
898
+ disposition: attachment.disposition,
899
+ content_id: attachment.content_id,
900
+ transfer_encoding: attachment.transfer_encoding,
901
+ data: attachment.data,
902
+ size: attachment.size
903
+ });
904
+ }
905
+ }
906
+ function recordHistoryEvents(gs, userEmail, historyId, events) {
907
+ for (const event of events) {
908
+ gs.history.insert({
909
+ gmail_id: historyId,
910
+ user_email: userEmail,
911
+ change_type: event.change_type,
912
+ message_gmail_id: event.message_gmail_id,
913
+ thread_id: event.thread_id,
914
+ label_ids: dedupeLabelIds(event.label_ids)
915
+ });
916
+ }
917
+ }
918
+ function applyFiltersToLabelIds(gs, userEmail, from, labelIds) {
919
+ if (!from) return labelIds;
920
+ let nextLabelIds = dedupeLabelIds(labelIds);
921
+ for (const filter of gs.filters.findBy("user_email", userEmail)) {
922
+ if (!matchesFilter(filter, from)) continue;
923
+ nextLabelIds = applyLabelMutation(nextLabelIds, filter.add_label_ids, filter.remove_label_ids);
924
+ }
925
+ return nextLabelIds;
926
+ }
927
+ function syncDraftState(gs, message, preferredDraftId) {
928
+ const shouldHaveDraft = message.label_ids.includes("DRAFT") && !message.label_ids.includes("SENT");
929
+ const existing = gs.drafts.findBy("message_gmail_id", message.gmail_id).filter((draft) => draft.user_email === message.user_email);
930
+ if (!shouldHaveDraft) {
931
+ for (const draft of existing) {
932
+ gs.drafts.delete(draft.id);
933
+ }
934
+ return void 0;
935
+ }
936
+ if (existing[0]) return existing[0];
937
+ return gs.drafts.insert({
938
+ gmail_id: preferredDraftId ?? generateDraftId(),
939
+ user_email: message.user_email,
940
+ message_gmail_id: message.gmail_id
941
+ });
942
+ }
943
+ function clearDraftRecordsForMessage(gs, userEmail, messageId) {
944
+ const drafts = gs.drafts.findBy("message_gmail_id", messageId).filter((draft) => draft.user_email === userEmail);
945
+ for (const draft of drafts) {
946
+ gs.drafts.delete(draft.id);
947
+ }
948
+ }
949
+ function clearMessageAttachments(gs, userEmail, messageId) {
950
+ const attachments = gs.attachments.findBy("message_gmail_id", messageId).filter((attachment) => attachment.user_email === userEmail);
951
+ for (const attachment of attachments) {
952
+ gs.attachments.delete(attachment.id);
953
+ }
954
+ }
955
+ function listAttachmentsForMessage(gs, message) {
956
+ return gs.attachments.findBy("message_gmail_id", message.gmail_id).filter((attachment) => attachment.user_email === message.user_email).sort((a, b) => a.created_at.localeCompare(b.created_at));
957
+ }
958
+ function hasMessageAttachments(gs, message) {
959
+ return gs.attachments.findBy("message_gmail_id", message.gmail_id).some((attachment) => attachment.user_email === message.user_email);
960
+ }
961
+ function ensureDefaultSendAs(gs, userEmail) {
962
+ const existing = gs.sendAs.findBy("user_email", userEmail);
963
+ if (existing.length > 0) {
964
+ if (!existing.some((entry) => entry.is_default)) {
965
+ gs.sendAs.update(existing[0].id, { is_default: true });
966
+ }
967
+ return;
968
+ }
969
+ const user = gs.users.findOneBy("email", userEmail);
970
+ gs.sendAs.insert({
971
+ user_email: userEmail,
972
+ send_as_email: userEmail,
973
+ display_name: user?.name?.trim() || userEmail.split("@")[0],
974
+ is_default: true,
975
+ signature: ""
976
+ });
977
+ }
978
+ function matchesFilter(filter, from) {
979
+ if (filter.criteria_from) {
980
+ return from.toLowerCase().includes(filter.criteria_from.toLowerCase());
981
+ }
982
+ return true;
983
+ }
984
+ function normalizeFilterFrom(value) {
985
+ const normalized = value?.trim();
986
+ return normalized ? normalized : null;
987
+ }
988
+ function sortStrings(values) {
989
+ return [...values].sort((left, right) => left.localeCompare(right));
990
+ }
991
+ function arrayEquals(left, right) {
992
+ if (left.length !== right.length) return false;
993
+ return left.every((value, index) => value === right[index]);
994
+ }
995
+ function messageMatchesLabelQuery(gs, userEmail, message, query) {
996
+ const normalized = normalizeLabelQuery(query);
997
+ if (message.label_ids.includes(normalized)) return true;
998
+ return message.label_ids.some((labelId) => {
999
+ const label = findLabelById(gs, userEmail, labelId);
1000
+ return label?.name.toLowerCase() === cleanToken(query).toLowerCase();
1001
+ });
1002
+ }
1003
+ function parseDateFilter(value) {
1004
+ const trimmed = cleanToken(value);
1005
+ if (!trimmed) return null;
1006
+ if (/^\d+$/.test(trimmed)) {
1007
+ const parsed2 = Number.parseInt(trimmed, 10);
1008
+ return String(parsed2).length >= 13 ? parsed2 : parsed2 * 1e3;
1009
+ }
1010
+ const parsed = Date.parse(trimmed);
1011
+ return Number.isFinite(parsed) ? parsed : null;
1012
+ }
1013
+ function searchableText(message) {
1014
+ return [
1015
+ message.subject,
1016
+ message.from,
1017
+ message.to,
1018
+ message.cc ?? "",
1019
+ message.bcc ?? "",
1020
+ message.snippet,
1021
+ message.body_text ?? "",
1022
+ stripHtml(message.body_html ?? "")
1023
+ ].join(" ").toLowerCase();
1024
+ }
1025
+ function cleanToken(token) {
1026
+ return token.trim().replace(/^[()]+/, "").replace(/[()]+$/, "").replace(/^"(.*)"$/, "$1");
1027
+ }
1028
+ function stripHtml(html) {
1029
+ return html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
1030
+ }
1031
+ function buildHeaders(message) {
1032
+ const headers = [
1033
+ { name: "From", value: message.from },
1034
+ { name: "To", value: message.to },
1035
+ { name: "Cc", value: message.cc },
1036
+ { name: "Bcc", value: message.bcc },
1037
+ { name: "Reply-To", value: message.reply_to },
1038
+ { name: "Subject", value: message.subject },
1039
+ { name: "Date", value: message.date_header },
1040
+ { name: "Message-ID", value: message.message_id },
1041
+ { name: "References", value: message.references },
1042
+ { name: "In-Reply-To", value: message.in_reply_to }
1043
+ ];
1044
+ return headers.filter((header) => Boolean(header.value));
1045
+ }
1046
+ function buildPayload(gs, message, headers, format) {
1047
+ const textBody = message.body_text ?? null;
1048
+ const htmlBody = message.body_html ?? null;
1049
+ const attachments = listAttachmentsForMessage(gs, message);
1050
+ if (format === "metadata") {
1051
+ return {
1052
+ partId: "",
1053
+ mimeType: attachments.length > 0 ? "multipart/mixed" : htmlBody ? "text/html" : "text/plain",
1054
+ filename: "",
1055
+ headers,
1056
+ body: { size: 0 }
1057
+ };
1058
+ }
1059
+ if (attachments.length === 0) {
1060
+ if (textBody && htmlBody) {
1061
+ return {
1062
+ partId: "",
1063
+ mimeType: "multipart/alternative",
1064
+ filename: "",
1065
+ headers,
1066
+ body: { size: 0 },
1067
+ parts: [createTextBodyPart("0", "text/plain", textBody), createTextBodyPart("1", "text/html", htmlBody)]
1068
+ };
1069
+ }
1070
+ if (htmlBody) return createTextBodyPart("", "text/html", htmlBody, headers);
1071
+ if (textBody) return createTextBodyPart("", "text/plain", textBody, headers);
1072
+ return {
1073
+ partId: "",
1074
+ mimeType: "text/plain",
1075
+ filename: "",
1076
+ headers,
1077
+ body: { size: 0 }
1078
+ };
1079
+ }
1080
+ const parts = [];
1081
+ if (textBody && htmlBody) {
1082
+ parts.push({
1083
+ partId: "0",
1084
+ mimeType: "multipart/alternative",
1085
+ filename: "",
1086
+ headers: [],
1087
+ body: { size: 0 },
1088
+ parts: [createTextBodyPart("0.0", "text/plain", textBody), createTextBodyPart("0.1", "text/html", htmlBody)]
1089
+ });
1090
+ } else if (htmlBody) {
1091
+ parts.push(createTextBodyPart("0", "text/html", htmlBody));
1092
+ } else if (textBody) {
1093
+ parts.push(createTextBodyPart("0", "text/plain", textBody));
1094
+ }
1095
+ for (const [index, attachment] of attachments.entries()) {
1096
+ parts.push(createAttachmentPart(String(parts.length + index), attachment));
1097
+ }
1098
+ return {
1099
+ partId: "",
1100
+ mimeType: "multipart/mixed",
1101
+ filename: "",
1102
+ headers,
1103
+ body: { size: 0 },
1104
+ parts
1105
+ };
1106
+ }
1107
+ function createTextBodyPart(partId, mimeType, content, headers = []) {
1108
+ return {
1109
+ partId,
1110
+ mimeType,
1111
+ filename: "",
1112
+ headers,
1113
+ body: {
1114
+ size: Buffer.byteLength(content, "utf8"),
1115
+ data: Buffer.from(content, "utf8").toString("base64url")
1116
+ }
1117
+ };
1118
+ }
1119
+ function createAttachmentPart(partId, attachment) {
1120
+ const headers = [
1121
+ {
1122
+ name: "Content-Type",
1123
+ value: attachment.filename ? `${attachment.mime_type}; name="${attachment.filename}"` : attachment.mime_type
1124
+ },
1125
+ {
1126
+ name: "Content-Disposition",
1127
+ value: `${attachment.disposition ?? "attachment"}; filename="${attachment.filename}"`
1128
+ }
1129
+ ];
1130
+ if (attachment.transfer_encoding) {
1131
+ headers.push({ name: "Content-Transfer-Encoding", value: attachment.transfer_encoding });
1132
+ }
1133
+ if (attachment.content_id) {
1134
+ headers.push({ name: "Content-ID", value: attachment.content_id });
1135
+ }
1136
+ return {
1137
+ partId,
1138
+ mimeType: attachment.mime_type,
1139
+ filename: attachment.filename,
1140
+ headers,
1141
+ body: {
1142
+ attachmentId: attachment.gmail_id,
1143
+ size: attachment.size
1144
+ }
1145
+ };
1146
+ }
1147
+ function estimateSize(message, preBuiltHeaders) {
1148
+ if (message.raw) {
1149
+ return Buffer.byteLength(message.raw, "utf8");
1150
+ }
1151
+ const headers = (preBuiltHeaders ?? buildHeaders(message)).map((header) => `${header.name}: ${header.value}`).join("\n");
1152
+ const body = `${message.body_text ?? ""}
1153
+ ${message.body_html ?? ""}`;
1154
+ return Buffer.byteLength(`${headers}
1155
+
1156
+ ${body}`, "utf8");
1157
+ }
1158
+ function deriveSnippet(value) {
1159
+ return stripHtml(value).slice(0, 140);
1160
+ }
1161
+ function sortMessagesByDateDesc(messages) {
1162
+ return [...messages].sort((a, b) => Number(b.internal_date) - Number(a.internal_date));
1163
+ }
1164
+ function sortMessagesByDateAsc(messages) {
1165
+ return [...messages].sort((a, b) => Number(a.internal_date) - Number(b.internal_date));
1166
+ }
1167
+ function buildMimeBodyPart(input) {
1168
+ if (input.body_text && input.body_html) {
1169
+ const boundary = `emulate-alt-${randomBytes(8).toString("hex")}`;
1170
+ return [
1171
+ `Content-Type: multipart/alternative; boundary="${boundary}"`,
1172
+ "",
1173
+ `--${boundary}`,
1174
+ "Content-Type: text/plain; charset=utf-8",
1175
+ "",
1176
+ input.body_text,
1177
+ `--${boundary}`,
1178
+ "Content-Type: text/html; charset=utf-8",
1179
+ "",
1180
+ input.body_html,
1181
+ `--${boundary}--`,
1182
+ ""
1183
+ ].join("\r\n");
1184
+ }
1185
+ if (input.body_html) {
1186
+ return ["Content-Type: text/html; charset=utf-8", "", input.body_html].join("\r\n");
1187
+ }
1188
+ if (input.body_text) {
1189
+ return ["Content-Type: text/plain; charset=utf-8", "", input.body_text].join("\r\n");
1190
+ }
1191
+ return null;
1192
+ }
1193
+ function encodeAttachmentContent(content) {
1194
+ const buffer = Buffer.isBuffer(content) ? content : Buffer.from(content, "utf8");
1195
+ return buffer.toString("base64");
1196
+ }
1197
+ function wrapBase64(value) {
1198
+ return value.replace(/.{1,76}/g, "$&\r\n").trimEnd();
1199
+ }
1200
+ function escapeMimeParameter(value) {
1201
+ return value.replace(/"/g, '\\"');
1202
+ }
1203
+ function ensureWrappedContentId(value) {
1204
+ if (value.startsWith("<") && value.endsWith(">")) return value;
1205
+ return `<${value}>`;
1206
+ }
1207
+ function parseRawMessage(raw) {
1208
+ const decoded = decodeBase64Like(raw).toString("utf8").replace(/\r\n/g, "\n");
1209
+ const root = parseMimeEntity(decoded);
1210
+ const attachments = collectMimeNodes(root).filter((node) => isAttachmentNode(node)).map((node) => ({
1211
+ filename: node.filename || "attachment",
1212
+ mime_type: node.mimeType || "application/octet-stream",
1213
+ disposition: node.disposition,
1214
+ content_id: node.contentId,
1215
+ transfer_encoding: node.transferEncoding,
1216
+ data: (node.body ?? Buffer.alloc(0)).toString("base64url"),
1217
+ size: node.body?.length ?? 0
1218
+ }));
1219
+ return {
1220
+ raw,
1221
+ from: root.headers.get("from") ?? "",
1222
+ to: root.headers.get("to") ?? "",
1223
+ cc: root.headers.get("cc") ?? null,
1224
+ bcc: root.headers.get("bcc") ?? null,
1225
+ reply_to: root.headers.get("reply-to") ?? null,
1226
+ subject: root.headers.get("subject") ?? "",
1227
+ message_id: root.headers.get("message-id") ?? null,
1228
+ references: root.headers.get("references") ?? null,
1229
+ in_reply_to: root.headers.get("in-reply-to") ?? null,
1230
+ date_header: root.headers.get("date") ?? null,
1231
+ body_text: findFirstTextPart(root, "text/plain"),
1232
+ body_html: findFirstTextPart(root, "text/html"),
1233
+ attachments
1234
+ };
1235
+ }
1236
+ function parseMimeEntity(source) {
1237
+ const normalized = source.replace(/\r\n/g, "\n");
1238
+ const separatorIndex = normalized.indexOf("\n\n");
1239
+ const headerText = separatorIndex >= 0 ? normalized.slice(0, separatorIndex) : normalized;
1240
+ const bodyText = separatorIndex >= 0 ? normalized.slice(separatorIndex + 2) : "";
1241
+ const headers = parseHeaders(headerText);
1242
+ const contentType = parseHeaderWithParams(headers.get("content-type") ?? "text/plain; charset=utf-8");
1243
+ const disposition = parseHeaderWithParams(headers.get("content-disposition") ?? "");
1244
+ const boundary = contentType.params.boundary;
1245
+ const mimeType = contentType.value.toLowerCase() || "text/plain";
1246
+ const filename = disposition.params.filename ?? contentType.params.name ?? "";
1247
+ if (mimeType.startsWith("multipart/") && boundary) {
1248
+ return {
1249
+ mimeType,
1250
+ filename,
1251
+ headers,
1252
+ body: null,
1253
+ parts: splitMultipartBody(bodyText, boundary).map((part) => parseMimeEntity(part)),
1254
+ disposition: disposition.value || null,
1255
+ contentId: headers.get("content-id") ?? null,
1256
+ transferEncoding: headers.get("content-transfer-encoding")?.toLowerCase() ?? null,
1257
+ charset: contentType.params.charset ?? null
1258
+ };
1259
+ }
1260
+ return {
1261
+ mimeType,
1262
+ filename,
1263
+ headers,
1264
+ body: decodeMimeBody(bodyText, headers.get("content-transfer-encoding") ?? null),
1265
+ parts: [],
1266
+ disposition: disposition.value || null,
1267
+ contentId: headers.get("content-id") ?? null,
1268
+ transferEncoding: headers.get("content-transfer-encoding")?.toLowerCase() ?? null,
1269
+ charset: contentType.params.charset ?? null
1270
+ };
1271
+ }
1272
+ function parseHeaders(headerText) {
1273
+ const headers = /* @__PURE__ */ new Map();
1274
+ let currentKey = null;
1275
+ for (const line of headerText.split("\n")) {
1276
+ if (!line.trim()) continue;
1277
+ if ((line.startsWith(" ") || line.startsWith(" ")) && currentKey) {
1278
+ headers.set(currentKey, `${headers.get(currentKey) ?? ""} ${line.trim()}`.trim());
1279
+ continue;
1280
+ }
1281
+ const separator = line.indexOf(":");
1282
+ if (separator < 0) continue;
1283
+ currentKey = line.slice(0, separator).trim().toLowerCase();
1284
+ headers.set(currentKey, line.slice(separator + 1).trim());
1285
+ }
1286
+ return headers;
1287
+ }
1288
+ function parseHeaderWithParams(value) {
1289
+ const [base, ...rest] = value.split(";");
1290
+ const params = {};
1291
+ for (const token of rest) {
1292
+ const separator = token.indexOf("=");
1293
+ if (separator < 0) continue;
1294
+ const key = token.slice(0, separator).trim().toLowerCase();
1295
+ const rawValue = token.slice(separator + 1).trim();
1296
+ params[key] = rawValue.replace(/^"(.*)"$/, "$1");
1297
+ }
1298
+ return {
1299
+ value: base.trim(),
1300
+ params
1301
+ };
1302
+ }
1303
+ function splitMultipartBody(body, boundary) {
1304
+ const marker = `--${boundary}`;
1305
+ const chunks = [];
1306
+ for (const segment of body.split(marker)) {
1307
+ const trimmed = segment.trim();
1308
+ if (!trimmed || trimmed === "--") continue;
1309
+ chunks.push(trimmed.replace(/^\n+/, "").replace(/\n+$/, ""));
1310
+ }
1311
+ return chunks;
1312
+ }
1313
+ function decodeMimeBody(body, transferEncoding) {
1314
+ const normalizedEncoding = transferEncoding?.toLowerCase() ?? "";
1315
+ if (normalizedEncoding === "base64") {
1316
+ const compact = body.replace(/\s+/g, "");
1317
+ return compact ? Buffer.from(compact, "base64") : Buffer.alloc(0);
1318
+ }
1319
+ if (normalizedEncoding === "quoted-printable") {
1320
+ return decodeQuotedPrintable(body);
1321
+ }
1322
+ return Buffer.from(body, "utf8");
1323
+ }
1324
+ function decodeQuotedPrintable(value) {
1325
+ const normalized = value.replace(/=\r?\n/g, "");
1326
+ const bytes = [];
1327
+ for (let index = 0; index < normalized.length; index += 1) {
1328
+ const current = normalized[index];
1329
+ if (current === "=" && /^[A-Fa-f0-9]{2}$/.test(normalized.slice(index + 1, index + 3))) {
1330
+ bytes.push(Number.parseInt(normalized.slice(index + 1, index + 3), 16));
1331
+ index += 2;
1332
+ continue;
1333
+ }
1334
+ bytes.push(normalized.charCodeAt(index));
1335
+ }
1336
+ return Buffer.from(bytes);
1337
+ }
1338
+ function findFirstTextPart(root, mimeType) {
1339
+ for (const node of collectMimeNodes(root)) {
1340
+ if (node.parts.length > 0) continue;
1341
+ if (!node.mimeType.includes(mimeType)) continue;
1342
+ if (isAttachmentNode(node)) continue;
1343
+ const content = decodeTextNode(node).trim();
1344
+ if (content) return content;
1345
+ }
1346
+ return null;
1347
+ }
1348
+ function decodeTextNode(node) {
1349
+ const encoding = normalizeCharset(node.charset);
1350
+ return (node.body ?? Buffer.alloc(0)).toString(encoding);
1351
+ }
1352
+ function normalizeCharset(value) {
1353
+ const normalized = value?.trim().toLowerCase();
1354
+ if (!normalized || normalized === "utf-8" || normalized === "us-ascii") return "utf8";
1355
+ if (normalized === "iso-8859-1" || normalized === "latin1") return "latin1";
1356
+ return "utf8";
1357
+ }
1358
+ function collectMimeNodes(root) {
1359
+ const nodes = [];
1360
+ const queue = [root];
1361
+ while (queue.length > 0) {
1362
+ const node = queue.shift();
1363
+ nodes.push(node);
1364
+ if (node.parts.length > 0) {
1365
+ queue.push(...node.parts);
1366
+ }
1367
+ }
1368
+ return nodes;
1369
+ }
1370
+ function isAttachmentNode(node) {
1371
+ if (node.parts.length > 0) return false;
1372
+ const disposition = node.disposition?.toLowerCase() ?? "";
1373
+ if (node.filename) return true;
1374
+ if (disposition.includes("attachment")) return true;
1375
+ if (disposition.includes("inline") && !node.mimeType.startsWith("text/")) return true;
1376
+ return false;
1377
+ }
1378
+ function decodeBase64Like(value) {
1379
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
1380
+ const padding = normalized.length % 4 === 0 ? "" : "=".repeat(4 - normalized.length % 4);
1381
+ return Buffer.from(normalized + padding, "base64");
1382
+ }
1383
+ function ensureDefaultCalendars(gs, userEmail) {
1384
+ const existing = gs.calendars.findBy("user_email", userEmail);
1385
+ if (existing.length > 0) {
1386
+ if (!existing.some((calendar) => calendar.primary)) {
1387
+ gs.calendars.update(existing[0].id, { primary: true });
1388
+ }
1389
+ return;
1390
+ }
1391
+ gs.calendars.insert({
1392
+ google_id: "primary",
1393
+ user_email: userEmail,
1394
+ summary: userEmail,
1395
+ description: null,
1396
+ time_zone: "UTC",
1397
+ primary: true,
1398
+ selected: true,
1399
+ access_role: "owner",
1400
+ background_color: null,
1401
+ foreground_color: null
1402
+ });
1403
+ }
1404
+ function createCalendarRecord(gs, input) {
1405
+ const calendarId = input.google_id ?? generateUid("cal");
1406
+ const existing = gs.calendars.findBy("user_email", input.user_email).find((calendar) => calendar.google_id === calendarId);
1407
+ if (existing) return existing;
1408
+ const inserted = gs.calendars.insert({
1409
+ google_id: calendarId,
1410
+ user_email: input.user_email,
1411
+ summary: input.summary,
1412
+ description: input.description ?? null,
1413
+ time_zone: input.time_zone ?? "UTC",
1414
+ primary: input.primary ?? false,
1415
+ selected: input.selected ?? true,
1416
+ access_role: input.access_role ?? "owner",
1417
+ background_color: input.background_color ?? null,
1418
+ foreground_color: input.foreground_color ?? null
1419
+ });
1420
+ if (inserted.primary) {
1421
+ for (const calendar of gs.calendars.findBy("user_email", input.user_email)) {
1422
+ if (calendar.id !== inserted.id && calendar.primary) {
1423
+ gs.calendars.update(calendar.id, { primary: false });
1424
+ }
1425
+ }
1426
+ }
1427
+ return inserted;
1428
+ }
1429
+ function listCalendarsForUser(gs, userEmail) {
1430
+ ensureDefaultCalendars(gs, userEmail);
1431
+ return gs.calendars.findBy("user_email", userEmail).sort((a, b) => Number(b.primary) - Number(a.primary) || a.summary.localeCompare(b.summary));
1432
+ }
1433
+ function getCalendarById(gs, userEmail, calendarId) {
1434
+ ensureDefaultCalendars(gs, userEmail);
1435
+ if (calendarId === "primary") {
1436
+ const calendars = listCalendarsForUser(gs, userEmail);
1437
+ return calendars.find((calendar) => calendar.primary) ?? calendars[0];
1438
+ }
1439
+ return gs.calendars.findBy("user_email", userEmail).find((calendar) => calendar.google_id === calendarId);
1440
+ }
1441
+ function formatCalendarResource(calendar) {
1442
+ return {
1443
+ kind: "calendar#calendarListEntry",
1444
+ etag: `"${calendar.google_id}"`,
1445
+ id: calendar.google_id,
1446
+ summary: calendar.summary,
1447
+ description: calendar.description ?? void 0,
1448
+ timeZone: calendar.time_zone,
1449
+ selected: calendar.selected,
1450
+ primary: calendar.primary || void 0,
1451
+ accessRole: calendar.access_role,
1452
+ backgroundColor: calendar.background_color ?? void 0,
1453
+ foregroundColor: calendar.foreground_color ?? void 0
1454
+ };
1455
+ }
1456
+ function createCalendarEventRecord(gs, input) {
1457
+ const calendar = getCalendarById(gs, input.user_email, input.calendar_google_id);
1458
+ if (!calendar) {
1459
+ throw new Error("Calendar not found");
1460
+ }
1461
+ const eventId = input.google_id ?? generateUid("evt");
1462
+ const existing = gs.calendarEvents.findBy("user_email", input.user_email).find((event) => event.google_id === eventId);
1463
+ if (existing) return existing;
1464
+ const hangoutLink = input.hangout_link ?? input.conference_entry_points?.find((entry) => entry.entry_point_type === "video")?.uri ?? null;
1465
+ return gs.calendarEvents.insert({
1466
+ google_id: eventId,
1467
+ user_email: input.user_email,
1468
+ calendar_google_id: calendar.google_id,
1469
+ status: input.status ?? "confirmed",
1470
+ summary: input.summary ?? "Untitled Event",
1471
+ description: input.description ?? null,
1472
+ location: input.location ?? null,
1473
+ html_link: buildCalendarEventLink(calendar.google_id, eventId),
1474
+ hangout_link: hangoutLink,
1475
+ start_date_time: input.start_date_time ?? null,
1476
+ start_date: input.start_date ?? null,
1477
+ end_date_time: input.end_date_time ?? null,
1478
+ end_date: input.end_date ?? null,
1479
+ attendees: input.attendees ?? [],
1480
+ conference_entry_points: input.conference_entry_points ?? [],
1481
+ transparency: input.transparency ?? null
1482
+ });
1483
+ }
1484
+ function getCalendarEventById(gs, userEmail, calendarId, eventId) {
1485
+ const calendar = getCalendarById(gs, userEmail, calendarId);
1486
+ if (!calendar) return void 0;
1487
+ return gs.calendarEvents.findBy("user_email", userEmail).find((event) => event.calendar_google_id === calendar.google_id && event.google_id === eventId);
1488
+ }
1489
+ function deleteCalendarEventRecord(gs, event) {
1490
+ return gs.calendarEvents.delete(event.id);
1491
+ }
1492
+ function listCalendarEvents(gs, userEmail, calendarId, options) {
1493
+ const calendar = getCalendarById(gs, userEmail, calendarId);
1494
+ if (!calendar) return { items: [] };
1495
+ let events = gs.calendarEvents.findBy("user_email", userEmail).filter((event) => event.calendar_google_id === calendar.google_id).filter((event) => event.status !== "cancelled");
1496
+ if (options.timeMin || options.timeMax) {
1497
+ const min = options.timeMin ? Date.parse(options.timeMin) : null;
1498
+ const max = options.timeMax ? Date.parse(options.timeMax) : null;
1499
+ events = events.filter((event) => eventOverlapsRange(event, min, max));
1500
+ }
1501
+ if (options.q?.trim()) {
1502
+ const needle = options.q.trim().toLowerCase();
1503
+ events = events.filter((event) => searchableCalendarEvent(event).includes(needle));
1504
+ }
1505
+ events.sort((a, b) => getEventSortTime(a) - getEventSortTime(b));
1506
+ if (options.orderBy && options.orderBy !== "startTime") {
1507
+ events.sort((a, b) => a.summary.localeCompare(b.summary));
1508
+ }
1509
+ const offset = parseOffset(options.pageToken);
1510
+ const limit = normalizeLimit(options.maxResults, 10, 250);
1511
+ return {
1512
+ items: events.slice(offset, offset + limit),
1513
+ nextPageToken: offset + limit < events.length ? String(offset + limit) : void 0
1514
+ };
1515
+ }
1516
+ function formatCalendarEventResource(gs, event) {
1517
+ const calendar = getCalendarById(gs, event.user_email, event.calendar_google_id);
1518
+ return {
1519
+ kind: "calendar#event",
1520
+ etag: `"${event.google_id}"`,
1521
+ id: event.google_id,
1522
+ status: event.status,
1523
+ htmlLink: event.html_link ?? void 0,
1524
+ hangoutLink: event.hangout_link ?? void 0,
1525
+ summary: event.summary,
1526
+ description: event.description ?? void 0,
1527
+ location: event.location ?? void 0,
1528
+ created: event.created_at,
1529
+ updated: event.updated_at,
1530
+ start: formatCalendarDateRange(event, "start", calendar?.time_zone ?? "UTC"),
1531
+ end: formatCalendarDateRange(event, "end", calendar?.time_zone ?? "UTC"),
1532
+ attendees: event.attendees.map((attendee) => ({
1533
+ email: attendee.email,
1534
+ displayName: attendee.display_name ?? void 0,
1535
+ responseStatus: attendee.response_status ?? void 0,
1536
+ organizer: attendee.organizer || void 0,
1537
+ self: attendee.self || void 0
1538
+ })),
1539
+ conferenceData: event.conference_entry_points.length > 0 ? {
1540
+ entryPoints: event.conference_entry_points.map((entry) => ({
1541
+ entryPointType: entry.entry_point_type,
1542
+ uri: entry.uri,
1543
+ label: entry.label ?? void 0
1544
+ }))
1545
+ } : void 0
1546
+ };
1547
+ }
1548
+ function buildFreeBusyResponse(gs, userEmail, request) {
1549
+ const calendars = {};
1550
+ const min = Date.parse(request.timeMin);
1551
+ const max = Date.parse(request.timeMax);
1552
+ for (const item of request.items) {
1553
+ const calendar = getCalendarById(gs, userEmail, item.id);
1554
+ if (!calendar) continue;
1555
+ const busy = gs.calendarEvents.findBy("user_email", userEmail).filter((event) => event.calendar_google_id === calendar.google_id).filter((event) => event.status !== "cancelled" && event.transparency !== "transparent").filter((event) => eventOverlapsRange(event, min, max)).sort((a, b) => getEventSortTime(a) - getEventSortTime(b)).map((event) => ({
1556
+ start: event.start_date_time ?? `${event.start_date}T00:00:00.000Z`,
1557
+ end: event.end_date_time ?? `${event.end_date}T00:00:00.000Z`
1558
+ }));
1559
+ calendars[item.id] = { busy };
1560
+ }
1561
+ return {
1562
+ kind: "calendar#freeBusy",
1563
+ timeMin: request.timeMin,
1564
+ timeMax: request.timeMax,
1565
+ calendars
1566
+ };
1567
+ }
1568
+ function buildCalendarEventLink(calendarId, eventId) {
1569
+ return `https://calendar.google.com/calendar/u/0/r/eventedit/${calendarId}/${eventId}`;
1570
+ }
1571
+ function formatCalendarDateRange(event, prefix, timeZone) {
1572
+ const dateTime = prefix === "start" ? event.start_date_time : event.end_date_time;
1573
+ const date = prefix === "start" ? event.start_date : event.end_date;
1574
+ if (dateTime) {
1575
+ return {
1576
+ dateTime,
1577
+ timeZone
1578
+ };
1579
+ }
1580
+ return {
1581
+ date: date ?? void 0,
1582
+ timeZone
1583
+ };
1584
+ }
1585
+ function searchableCalendarEvent(event) {
1586
+ return [
1587
+ event.summary,
1588
+ event.description ?? "",
1589
+ event.location ?? "",
1590
+ ...event.attendees.map((attendee) => attendee.email),
1591
+ ...event.attendees.map((attendee) => attendee.display_name ?? "")
1592
+ ].join(" ").toLowerCase();
1593
+ }
1594
+ function eventOverlapsRange(event, min, max) {
1595
+ const start = getEventSortTime(event);
1596
+ const end = getEventEndTime(event);
1597
+ if (min != null && end <= min) return false;
1598
+ if (max != null && start >= max) return false;
1599
+ return true;
1600
+ }
1601
+ function getEventSortTime(event) {
1602
+ return parseCalendarTimestamp(event.start_date_time, event.start_date);
1603
+ }
1604
+ function getEventEndTime(event) {
1605
+ return parseCalendarTimestamp(event.end_date_time, event.end_date);
1606
+ }
1607
+ function parseCalendarTimestamp(dateTime, date) {
1608
+ if (dateTime) {
1609
+ const parsed = Date.parse(dateTime);
1610
+ if (Number.isFinite(parsed)) return parsed;
1611
+ }
1612
+ if (date) {
1613
+ const parsed = Date.parse(`${date}T00:00:00.000Z`);
1614
+ if (Number.isFinite(parsed)) return parsed;
1615
+ }
1616
+ return Date.now();
1617
+ }
1618
+ var GOOGLE_DRIVE_FOLDER_MIME_TYPE = "application/vnd.google-apps.folder";
1619
+ function createDriveItemRecord(gs, input) {
1620
+ const itemId = input.google_id ?? generateUid("drv");
1621
+ const existing = gs.driveItems.findBy("user_email", input.user_email).find((item2) => item2.google_id === itemId);
1622
+ if (existing) return existing;
1623
+ const item = gs.driveItems.insert({
1624
+ google_id: itemId,
1625
+ user_email: input.user_email,
1626
+ name: input.name,
1627
+ mime_type: input.mime_type,
1628
+ parent_google_ids: normalizeParentIds(input.parent_google_ids),
1629
+ web_view_link: input.web_view_link ?? buildDriveWebViewLink(itemId, input.mime_type),
1630
+ size: input.size ?? null,
1631
+ trashed: input.trashed ?? false,
1632
+ data: input.data ?? null
1633
+ });
1634
+ return item;
1635
+ }
1636
+ function getDriveItemById(gs, userEmail, fileId) {
1637
+ return gs.driveItems.findBy("user_email", userEmail).find((item) => item.google_id === fileId);
1638
+ }
1639
+ function listDriveItems(gs, userEmail, options) {
1640
+ let items = gs.driveItems.findBy("user_email", userEmail);
1641
+ const parsed = parseDriveQuery(options.q ?? null);
1642
+ if (parsed.parentId) {
1643
+ items = items.filter((item) => item.parent_google_ids.includes(parsed.parentId));
1644
+ }
1645
+ if (parsed.requireNotTrashed) {
1646
+ items = items.filter((item) => !item.trashed);
1647
+ }
1648
+ if (parsed.mimeTypes.length > 0) {
1649
+ items = items.filter((item) => parsed.mimeTypes.includes(item.mime_type));
1650
+ }
1651
+ if (parsed.excludeMimeTypes.length > 0) {
1652
+ items = items.filter((item) => !parsed.excludeMimeTypes.includes(item.mime_type));
1653
+ }
1654
+ if (options.orderBy?.includes("name")) {
1655
+ items = items.sort((a, b) => a.name.localeCompare(b.name));
1656
+ } else {
1657
+ items = items.sort((a, b) => a.created_at.localeCompare(b.created_at));
1658
+ }
1659
+ const offset = parseOffset(options.pageToken);
1660
+ const limit = normalizeLimit(options.pageSize, 100, 1e3);
1661
+ return {
1662
+ files: items.slice(offset, offset + limit),
1663
+ nextPageToken: offset + limit < items.length ? String(offset + limit) : void 0
1664
+ };
1665
+ }
1666
+ function updateDriveItemRecord(gs, item, input) {
1667
+ const nextParents = new Set(item.parent_google_ids);
1668
+ for (const parentId of input.addParents ?? []) {
1669
+ nextParents.add(parentId);
1670
+ }
1671
+ for (const parentId of input.removeParents ?? []) {
1672
+ nextParents.delete(parentId);
1673
+ }
1674
+ return gs.driveItems.update(item.id, {
1675
+ name: input.name ?? item.name,
1676
+ parent_google_ids: normalizeParentIds(Array.from(nextParents)),
1677
+ trashed: input.trashed ?? item.trashed,
1678
+ web_view_link: buildDriveWebViewLink(item.google_id, item.mime_type)
1679
+ }) ?? item;
1680
+ }
1681
+ function formatDriveItemResource(item) {
1682
+ return {
1683
+ kind: "drive#file",
1684
+ id: item.google_id,
1685
+ name: item.name,
1686
+ mimeType: item.mime_type,
1687
+ parents: item.parent_google_ids,
1688
+ webViewLink: item.web_view_link ?? void 0,
1689
+ createdTime: item.created_at,
1690
+ modifiedTime: item.updated_at,
1691
+ size: item.size != null ? String(item.size) : void 0,
1692
+ trashed: item.trashed || void 0
1693
+ };
1694
+ }
1695
+ function parseDriveMultipartUpload(contentType, rawBody) {
1696
+ const boundaryMatch = contentType.match(/boundary="?([^";]+)"?/i);
1697
+ const boundary = boundaryMatch?.[1];
1698
+ if (!boundary) {
1699
+ return {
1700
+ requestBody: {},
1701
+ media: void 0
1702
+ };
1703
+ }
1704
+ const raw = rawBody.toString("latin1");
1705
+ const parts = raw.split(`--${boundary}`).slice(1).filter((part) => part !== "--" && part !== "--\r\n" && part !== "--\n");
1706
+ let requestBody = {};
1707
+ let media;
1708
+ for (const part of parts) {
1709
+ const normalized = stripMultipartBoundaryPadding(part);
1710
+ const headerSeparator = normalized.includes("\r\n\r\n") ? "\r\n\r\n" : "\n\n";
1711
+ const separatorIndex = normalized.indexOf(headerSeparator);
1712
+ if (separatorIndex < 0) continue;
1713
+ const headers = normalized.slice(0, separatorIndex).toLowerCase();
1714
+ const bodyText = normalized.slice(separatorIndex + headerSeparator.length);
1715
+ if (headers.includes("application/json")) {
1716
+ try {
1717
+ const parsed = JSON.parse(bodyText);
1718
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1719
+ requestBody = parsed;
1720
+ }
1721
+ } catch {
1722
+ requestBody = {};
1723
+ }
1724
+ continue;
1725
+ }
1726
+ const mimeTypeMatch = headers.match(/content-type:\s*([^\r\n;]+)/i);
1727
+ media = {
1728
+ mimeType: mimeTypeMatch?.[1]?.trim() ?? "application/octet-stream",
1729
+ body: Buffer.from(bodyText, "latin1")
1730
+ };
1731
+ }
1732
+ return {
1733
+ requestBody,
1734
+ media
1735
+ };
1736
+ }
1737
+ function parseDriveQuery(query) {
1738
+ const source = query ?? "";
1739
+ const parentMatch = source.match(/'([^']+)' in parents/i);
1740
+ const mimeTypes = Array.from(source.matchAll(/mimeType = '([^']+)'/g)).map((match) => match[1]);
1741
+ const excludeMimeTypes = Array.from(source.matchAll(/mimeType != '([^']+)'/g)).map((match) => match[1]);
1742
+ return {
1743
+ parentId: parentMatch?.[1] ?? null,
1744
+ mimeTypes,
1745
+ excludeMimeTypes,
1746
+ requireNotTrashed: source.includes("trashed = false")
1747
+ };
1748
+ }
1749
+ function buildDriveWebViewLink(itemId, mimeType) {
1750
+ if (mimeType === GOOGLE_DRIVE_FOLDER_MIME_TYPE) {
1751
+ return `https://drive.google.com/drive/folders/${itemId}`;
1752
+ }
1753
+ return `https://drive.google.com/file/d/${itemId}/view`;
1754
+ }
1755
+ function normalizeParentIds(parentIds) {
1756
+ const normalized = [...new Set((parentIds ?? ["root"]).filter(Boolean))];
1757
+ return normalized.length > 0 ? normalized : ["root"];
1758
+ }
1759
+ function stripMultipartBoundaryPadding(part) {
1760
+ let normalized = part;
1761
+ if (normalized.startsWith("\r\n")) {
1762
+ normalized = normalized.slice(2);
1763
+ } else if (normalized.startsWith("\n")) {
1764
+ normalized = normalized.slice(1);
1765
+ }
1766
+ if (normalized.endsWith("\r\n")) {
1767
+ normalized = normalized.slice(0, -2);
1768
+ } else if (normalized.endsWith("\n")) {
1769
+ normalized = normalized.slice(0, -1);
1770
+ }
1771
+ return normalized;
1772
+ }
1773
+ function requireGoogleAuth(c) {
1774
+ const authEmail = getAuthenticatedEmail(c);
1775
+ if (!authEmail) {
1776
+ return googleApiError(c, 401, "Request had invalid authentication credentials.", "authError", "UNAUTHENTICATED");
1777
+ }
1778
+ return authEmail;
1779
+ }
1780
+ function requireGmailUser(c) {
1781
+ const authEmail = requireGoogleAuth(c);
1782
+ if (authEmail instanceof Response) {
1783
+ return authEmail;
1784
+ }
1785
+ if (!matchesRequestedUser(c.req.param("userId") ?? "", authEmail)) {
1786
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
1787
+ }
1788
+ return authEmail;
1789
+ }
1790
+ async function parseGoogleBody(c) {
1791
+ const contentType = c.req.header("Content-Type") ?? "";
1792
+ const rawText = await c.req.text();
1793
+ if (!rawText) return {};
1794
+ let parsed;
1795
+ if (contentType.includes("application/json")) {
1796
+ try {
1797
+ const json = JSON.parse(rawText);
1798
+ parsed = json && typeof json === "object" && !Array.isArray(json) ? json : {};
1799
+ } catch {
1800
+ return {};
1801
+ }
1802
+ } else if (contentType.includes("application/x-www-form-urlencoded")) {
1803
+ parsed = Object.fromEntries(new URLSearchParams(rawText));
1804
+ } else {
1805
+ parsed = {
1806
+ raw: Buffer.from(rawText, "utf8").toString("base64url")
1807
+ };
1808
+ }
1809
+ const nestedBody = parsed.requestBody;
1810
+ if (nestedBody && typeof nestedBody === "object" && !Array.isArray(nestedBody)) {
1811
+ return nestedBody;
1812
+ }
1813
+ return parsed;
1814
+ }
1815
+ function getStringArray(body, field) {
1816
+ const value = body[field];
1817
+ if (Array.isArray(value)) {
1818
+ return value.filter((item) => typeof item === "string" && item.length > 0);
1819
+ }
1820
+ if (typeof value === "string" && value.length > 0) {
1821
+ return [value];
1822
+ }
1823
+ return [];
1824
+ }
1825
+ function getString(body, ...fields) {
1826
+ for (const field of fields) {
1827
+ const value = body[field];
1828
+ if (typeof value === "string") return value;
1829
+ }
1830
+ return void 0;
1831
+ }
1832
+ function getRecord(body, ...fields) {
1833
+ for (const field of fields) {
1834
+ const value = body[field];
1835
+ if (value && typeof value === "object" && !Array.isArray(value)) {
1836
+ return value;
1837
+ }
1838
+ }
1839
+ return void 0;
1840
+ }
1841
+ function getRecordArray(body, ...fields) {
1842
+ for (const field of fields) {
1843
+ const value = body[field];
1844
+ if (!Array.isArray(value)) continue;
1845
+ return value.filter(
1846
+ (item) => Boolean(item) && typeof item === "object" && !Array.isArray(item)
1847
+ );
1848
+ }
1849
+ return [];
1850
+ }
1851
+ function parseMessageInputFromBody(body, defaults) {
1852
+ return {
1853
+ raw: getString(body, "raw"),
1854
+ thread_id: getString(body, "threadId", "thread_id"),
1855
+ from: getString(body, "from") ?? defaults?.from,
1856
+ to: getString(body, "to"),
1857
+ cc: getString(body, "cc") ?? null,
1858
+ bcc: getString(body, "bcc") ?? null,
1859
+ reply_to: getString(body, "replyTo", "reply_to") ?? null,
1860
+ subject: getString(body, "subject"),
1861
+ snippet: getString(body, "snippet"),
1862
+ body_text: getString(body, "body_text", "text") ?? null,
1863
+ body_html: getString(body, "body_html", "html") ?? null,
1864
+ date: getString(body, "date"),
1865
+ internal_date: getString(body, "internalDate", "internal_date"),
1866
+ message_id: getString(body, "messageId", "message_id"),
1867
+ references: getString(body, "references") ?? null,
1868
+ in_reply_to: getString(body, "inReplyTo", "in_reply_to") ?? null
1869
+ };
1870
+ }
1871
+ function parseCalendarEventInputFromBody(body) {
1872
+ const start = getRecord(body, "start");
1873
+ const end = getRecord(body, "end");
1874
+ const conferenceData = getRecord(body, "conferenceData");
1875
+ const conferenceEntryPoints = getRecordArray(conferenceData ?? {}, "entryPoints").map((entry) => ({
1876
+ entry_point_type: getString(entry, "entryPointType") ?? "video",
1877
+ uri: getString(entry, "uri") ?? "",
1878
+ label: getString(entry, "label") ?? null
1879
+ })).filter((entry) => entry.uri.length > 0);
1880
+ return {
1881
+ status: getString(body, "status") ?? "confirmed",
1882
+ summary: getString(body, "summary"),
1883
+ description: getString(body, "description") ?? null,
1884
+ location: getString(body, "location") ?? null,
1885
+ start_date_time: getString(start ?? {}, "dateTime") ?? null,
1886
+ start_date: getString(start ?? {}, "date") ?? null,
1887
+ end_date_time: getString(end ?? {}, "dateTime") ?? null,
1888
+ end_date: getString(end ?? {}, "date") ?? null,
1889
+ attendees: getRecordArray(body, "attendees").map((entry) => ({
1890
+ email: getString(entry, "email") ?? "",
1891
+ display_name: getString(entry, "displayName") ?? null,
1892
+ response_status: getString(entry, "responseStatus") ?? null,
1893
+ organizer: entry.organizer === true,
1894
+ self: entry.self === true
1895
+ })).filter((attendee) => attendee.email.length > 0),
1896
+ conference_entry_points: conferenceEntryPoints,
1897
+ hangout_link: getString(body, "hangoutLink") ?? conferenceEntryPoints.find((entry) => entry.entry_point_type === "video")?.uri ?? null,
1898
+ transparency: getString(body, "transparency") ?? null
1899
+ };
1900
+ }
1901
+ function parseDriveItemInputFromBody(body, defaults) {
1902
+ const parentIds = getStringArray(body, "parents");
1903
+ return {
1904
+ name: getString(body, "name")?.trim() || "Untitled",
1905
+ mime_type: getString(body, "mimeType") ?? defaults?.mimeType ?? "application/octet-stream",
1906
+ parent_google_ids: parentIds.length > 0 ? parentIds : ["root"]
1907
+ };
1908
+ }
1909
+ function getGoogleStore(store) {
1910
+ return {
1911
+ users: store.collection("google.users", ["uid", "email"]),
1912
+ oauthClients: store.collection("google.oauth_clients", ["client_id"]),
1913
+ messages: store.collection("google.messages", ["gmail_id", "thread_id", "user_email"]),
1914
+ drafts: store.collection("google.drafts", ["gmail_id", "message_gmail_id", "user_email"]),
1915
+ attachments: store.collection("google.attachments", [
1916
+ "gmail_id",
1917
+ "message_gmail_id",
1918
+ "user_email"
1919
+ ]),
1920
+ history: store.collection("google.history", ["gmail_id", "message_gmail_id", "user_email"]),
1921
+ labels: store.collection("google.labels", ["gmail_id", "user_email", "name"]),
1922
+ filters: store.collection("google.filters", ["gmail_id", "user_email"]),
1923
+ forwardingAddresses: store.collection("google.forwarding_addresses", [
1924
+ "user_email",
1925
+ "forwarding_email"
1926
+ ]),
1927
+ sendAs: store.collection("google.send_as", ["user_email", "send_as_email"]),
1928
+ calendars: store.collection("google.calendars", ["google_id", "user_email"]),
1929
+ calendarEvents: store.collection("google.calendar_events", [
1930
+ "google_id",
1931
+ "calendar_google_id",
1932
+ "user_email"
1933
+ ]),
1934
+ driveItems: store.collection("google.drive_items", ["google_id", "user_email", "mime_type"])
1935
+ };
1936
+ }
1937
+ function calendarRoutes({ app, store }) {
1938
+ const gs = getGoogleStore(store);
1939
+ app.get("/calendar/v3/users/:userId/calendarList", (c) => {
1940
+ const authEmail = requireGmailUser(c);
1941
+ if (authEmail instanceof Response) return authEmail;
1942
+ return c.json({
1943
+ kind: "calendar#calendarList",
1944
+ items: listCalendarsForUser(gs, authEmail).map((calendar) => formatCalendarResource(calendar))
1945
+ });
1946
+ });
1947
+ app.get("/calendar/v3/calendars/:calendarId/events", (c) => {
1948
+ const authEmail = requireGoogleAuth(c);
1949
+ if (authEmail instanceof Response) return authEmail;
1950
+ const calendar = getCalendarById(gs, authEmail, c.req.param("calendarId"));
1951
+ if (!calendar) {
1952
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
1953
+ }
1954
+ const url = new URL(c.req.url);
1955
+ const response = listCalendarEvents(gs, authEmail, calendar.google_id, {
1956
+ timeMin: url.searchParams.get("timeMin"),
1957
+ timeMax: url.searchParams.get("timeMax"),
1958
+ maxResults: url.searchParams.get("maxResults"),
1959
+ pageToken: url.searchParams.get("pageToken"),
1960
+ q: url.searchParams.get("q"),
1961
+ orderBy: url.searchParams.get("orderBy")
1962
+ });
1963
+ return c.json({
1964
+ kind: "calendar#events",
1965
+ items: response.items.map((event) => formatCalendarEventResource(gs, event)),
1966
+ nextPageToken: response.nextPageToken
1967
+ });
1968
+ });
1969
+ app.post("/calendar/v3/calendars/:calendarId/events", async (c) => {
1970
+ const authEmail = requireGoogleAuth(c);
1971
+ if (authEmail instanceof Response) return authEmail;
1972
+ const calendar = getCalendarById(gs, authEmail, c.req.param("calendarId"));
1973
+ if (!calendar) {
1974
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
1975
+ }
1976
+ const body = await parseGoogleBody(c);
1977
+ const requestBody = getRecord(body, "requestBody") ?? body;
1978
+ const eventInput = parseCalendarEventInputFromBody(requestBody);
1979
+ if (!eventInput.start_date_time && !eventInput.start_date || !eventInput.end_date_time && !eventInput.end_date) {
1980
+ return googleApiError(c, 400, "Event start and end are required.", "invalidArgument", "INVALID_ARGUMENT");
1981
+ }
1982
+ const event = createCalendarEventRecord(gs, {
1983
+ user_email: authEmail,
1984
+ calendar_google_id: calendar.google_id,
1985
+ ...eventInput
1986
+ });
1987
+ return c.json(formatCalendarEventResource(gs, event));
1988
+ });
1989
+ app.delete("/calendar/v3/calendars/:calendarId/events/:eventId", (c) => {
1990
+ const authEmail = requireGoogleAuth(c);
1991
+ if (authEmail instanceof Response) return authEmail;
1992
+ const event = getCalendarEventById(gs, authEmail, c.req.param("calendarId"), c.req.param("eventId"));
1993
+ if (!event) {
1994
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
1995
+ }
1996
+ deleteCalendarEventRecord(gs, event);
1997
+ return c.body(null, 204);
1998
+ });
1999
+ app.post("/calendar/v3/freeBusy", async (c) => {
2000
+ const authEmail = requireGoogleAuth(c);
2001
+ if (authEmail instanceof Response) return authEmail;
2002
+ const body = await parseGoogleBody(c);
2003
+ const requestBody = getRecord(body, "requestBody") ?? body;
2004
+ const timeMin = typeof requestBody.timeMin === "string" ? requestBody.timeMin : void 0;
2005
+ const timeMax = typeof requestBody.timeMax === "string" ? requestBody.timeMax : void 0;
2006
+ const items = getRecordArray(requestBody, "items").map((entry) => ({
2007
+ id: typeof entry.id === "string" ? entry.id : ""
2008
+ })).filter((entry) => entry.id.length > 0);
2009
+ if (!timeMin || !timeMax) {
2010
+ return googleApiError(c, 400, "timeMin and timeMax are required.", "invalidArgument", "INVALID_ARGUMENT");
2011
+ }
2012
+ return c.json(
2013
+ buildFreeBusyResponse(gs, authEmail, {
2014
+ timeMin,
2015
+ timeMax,
2016
+ items
2017
+ })
2018
+ );
2019
+ });
2020
+ }
2021
+ function draftRoutes({ app, store }) {
2022
+ const gs = getGoogleStore(store);
2023
+ const createHandler = async (c) => {
2024
+ const authEmail = requireGmailUser(c);
2025
+ if (authEmail instanceof Response) return authEmail;
2026
+ const body = await parseGoogleBody(c);
2027
+ const messageBody = getRecord(body, "message") ?? body;
2028
+ try {
2029
+ const { draft } = createDraftMessage(gs, {
2030
+ user_email: authEmail,
2031
+ ...parseMessageInputFromBody(messageBody, { from: authEmail })
2032
+ });
2033
+ return c.json(formatDraftResource(gs, draft, "full"));
2034
+ } catch {
2035
+ return googleApiError(c, 400, "Invalid raw MIME message payload.", "invalidArgument", "INVALID_ARGUMENT");
2036
+ }
2037
+ };
2038
+ const sendHandler = async (c) => {
2039
+ const authEmail = requireGmailUser(c);
2040
+ if (authEmail instanceof Response) return authEmail;
2041
+ const body = await parseGoogleBody(c);
2042
+ const draftId = getString(body, "id") ?? getString(getRecord(body, "draft") ?? {}, "id");
2043
+ if (!draftId) {
2044
+ return googleApiError(c, 400, "Draft ID is required.", "invalidArgument", "INVALID_ARGUMENT");
2045
+ }
2046
+ const draft = getDraftById(gs, authEmail, draftId);
2047
+ if (!draft) {
2048
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2049
+ }
2050
+ const message = sendDraftMessage(gs, draft);
2051
+ if (!message) {
2052
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2053
+ }
2054
+ return c.json({
2055
+ id: message.gmail_id,
2056
+ threadId: message.thread_id,
2057
+ labelIds: message.label_ids,
2058
+ snippet: message.snippet,
2059
+ historyId: message.history_id,
2060
+ internalDate: message.internal_date
2061
+ });
2062
+ };
2063
+ app.get("/gmail/v1/users/:userId/drafts", (c) => {
2064
+ const authEmail = requireGmailUser(c);
2065
+ if (authEmail instanceof Response) return authEmail;
2066
+ const drafts = listDraftsForUser(gs, authEmail);
2067
+ const url = new URL(c.req.url);
2068
+ const offset = parseOffset(url.searchParams.get("pageToken"));
2069
+ const limit = normalizeLimit(url.searchParams.get("maxResults"), 100, 500);
2070
+ const page = drafts.slice(offset, offset + limit);
2071
+ const nextPageToken = offset + limit < drafts.length ? String(offset + limit) : void 0;
2072
+ return c.json({
2073
+ drafts: page.map((draft) => {
2074
+ const resource = formatDraftResource(gs, draft, "minimal");
2075
+ return {
2076
+ id: resource.id,
2077
+ message: resource.message ? {
2078
+ id: resource.message.id,
2079
+ threadId: resource.message.threadId
2080
+ } : void 0
2081
+ };
2082
+ }),
2083
+ nextPageToken,
2084
+ resultSizeEstimate: drafts.length
2085
+ });
2086
+ });
2087
+ app.post("/gmail/v1/users/:userId/drafts", createHandler);
2088
+ app.post("/upload/gmail/v1/users/:userId/drafts", createHandler);
2089
+ app.get("/gmail/v1/users/:userId/drafts/:id", (c) => {
2090
+ const authEmail = requireGmailUser(c);
2091
+ if (authEmail instanceof Response) return authEmail;
2092
+ const draft = getDraftById(gs, authEmail, c.req.param("id"));
2093
+ if (!draft) {
2094
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2095
+ }
2096
+ if (!getDraftMessage(gs, draft)) {
2097
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2098
+ }
2099
+ const url = new URL(c.req.url);
2100
+ return c.json(
2101
+ formatDraftResource(
2102
+ gs,
2103
+ draft,
2104
+ parseFormat(url.searchParams.get("format")),
2105
+ url.searchParams.getAll("metadataHeaders")
2106
+ )
2107
+ );
2108
+ });
2109
+ app.put("/gmail/v1/users/:userId/drafts/:id", async (c) => {
2110
+ const authEmail = requireGmailUser(c);
2111
+ if (authEmail instanceof Response) return authEmail;
2112
+ const draft = getDraftById(gs, authEmail, c.req.param("id"));
2113
+ if (!draft) {
2114
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2115
+ }
2116
+ const body = await parseGoogleBody(c);
2117
+ const messageBody = getRecord(body, "message") ?? body;
2118
+ try {
2119
+ const updated = updateDraftMessage(gs, draft, parseMessageInputFromBody(messageBody));
2120
+ if (!updated) {
2121
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2122
+ }
2123
+ return c.json(formatDraftResource(gs, updated.draft, "full"));
2124
+ } catch {
2125
+ return googleApiError(c, 400, "Invalid raw MIME message payload.", "invalidArgument", "INVALID_ARGUMENT");
2126
+ }
2127
+ });
2128
+ app.post("/gmail/v1/users/:userId/drafts/send", sendHandler);
2129
+ app.post("/upload/gmail/v1/users/:userId/drafts/send", sendHandler);
2130
+ app.delete("/gmail/v1/users/:userId/drafts/:id", (c) => {
2131
+ const authEmail = requireGmailUser(c);
2132
+ if (authEmail instanceof Response) return authEmail;
2133
+ const draft = getDraftById(gs, authEmail, c.req.param("id"));
2134
+ if (!draft) {
2135
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2136
+ }
2137
+ deleteDraftMessage(gs, draft);
2138
+ return c.body(null, 204);
2139
+ });
2140
+ }
2141
+ function driveRoutes({ app, store }) {
2142
+ const gs = getGoogleStore(store);
2143
+ const createHandler = async (c) => {
2144
+ const authEmail = requireGoogleAuth(c);
2145
+ if (authEmail instanceof Response) return authEmail;
2146
+ const contentType = c.req.header("Content-Type") ?? "";
2147
+ let requestBody = {};
2148
+ let media;
2149
+ if (contentType.includes("multipart/related")) {
2150
+ const rawBody = Buffer.from(await c.req.raw.arrayBuffer());
2151
+ const parsed = parseDriveMultipartUpload(contentType, rawBody);
2152
+ requestBody = parsed.requestBody;
2153
+ media = parsed.media;
2154
+ } else {
2155
+ const body = await parseGoogleBody(c);
2156
+ requestBody = getRecord(body, "requestBody") ?? body;
2157
+ }
2158
+ const item = createDriveItemRecord(gs, {
2159
+ user_email: authEmail,
2160
+ ...parseDriveItemInputFromBody(requestBody, {
2161
+ mimeType: media?.mimeType
2162
+ }),
2163
+ size: media ? media.body.length : null,
2164
+ data: media ? media.body.toString("base64url") : null
2165
+ });
2166
+ return c.json(formatDriveItemResource(item));
2167
+ };
2168
+ app.get("/drive/v3/files", (c) => {
2169
+ const authEmail = requireGoogleAuth(c);
2170
+ if (authEmail instanceof Response) return authEmail;
2171
+ const url = new URL(c.req.url);
2172
+ const response = listDriveItems(gs, authEmail, {
2173
+ q: url.searchParams.get("q"),
2174
+ pageSize: url.searchParams.get("pageSize"),
2175
+ pageToken: url.searchParams.get("pageToken"),
2176
+ orderBy: url.searchParams.get("orderBy")
2177
+ });
2178
+ return c.json({
2179
+ kind: "drive#fileList",
2180
+ files: response.files.map((item) => formatDriveItemResource(item)),
2181
+ nextPageToken: response.nextPageToken
2182
+ });
2183
+ });
2184
+ app.post("/drive/v3/files", createHandler);
2185
+ app.post("/upload/drive/v3/files", createHandler);
2186
+ app.get("/drive/v3/files/:fileId", (c) => {
2187
+ const authEmail = requireGoogleAuth(c);
2188
+ if (authEmail instanceof Response) return authEmail;
2189
+ const item = getDriveItemById(gs, authEmail, c.req.param("fileId"));
2190
+ if (!item) {
2191
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2192
+ }
2193
+ const url = new URL(c.req.url);
2194
+ if (url.searchParams.get("alt") === "media") {
2195
+ return new Response(item.data ? Buffer.from(item.data, "base64url") : Buffer.alloc(0), {
2196
+ status: 200,
2197
+ headers: {
2198
+ "Content-Type": item.mime_type
2199
+ }
2200
+ });
2201
+ }
2202
+ return c.json(formatDriveItemResource(item));
2203
+ });
2204
+ const updateHandler = async (c) => {
2205
+ const authEmail = requireGoogleAuth(c);
2206
+ if (authEmail instanceof Response) return authEmail;
2207
+ const item = getDriveItemById(gs, authEmail, c.req.param("fileId"));
2208
+ if (!item) {
2209
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2210
+ }
2211
+ const url = new URL(c.req.url);
2212
+ const body = await parseGoogleBody(c);
2213
+ const requestBody = getRecord(body, "requestBody") ?? body;
2214
+ const addParents = (url.searchParams.get("addParents") ?? "").split(",").map((value) => value.trim()).filter(Boolean);
2215
+ const removeParents = (url.searchParams.get("removeParents") ?? "").split(",").map((value) => value.trim()).filter(Boolean);
2216
+ const updated = updateDriveItemRecord(gs, item, {
2217
+ addParents,
2218
+ removeParents,
2219
+ name: getString(requestBody, "name")
2220
+ });
2221
+ return c.json(formatDriveItemResource(updated));
2222
+ };
2223
+ app.patch("/drive/v3/files/:fileId", updateHandler);
2224
+ app.put("/drive/v3/files/:fileId", updateHandler);
2225
+ }
2226
+ var WATCH_STATE_KEY = "google.gmail.watchStates";
2227
+ function historyRoutes({ app, store }) {
2228
+ const gs = getGoogleStore(store);
2229
+ app.get("/gmail/v1/users/:userId/history", (c) => {
2230
+ const authEmail = requireGmailUser(c);
2231
+ if (authEmail instanceof Response) return authEmail;
2232
+ const url = new URL(c.req.url);
2233
+ const startHistoryId = url.searchParams.get("startHistoryId")?.trim();
2234
+ if (!startHistoryId) {
2235
+ return googleApiError(c, 400, "Start history ID is required.", "invalidArgument", "INVALID_ARGUMENT");
2236
+ }
2237
+ const historyTypes = url.searchParams.getAll("historyTypes").filter(isHistoryChangeType);
2238
+ return c.json(
2239
+ listHistoryForUser(gs, authEmail, {
2240
+ startHistoryId,
2241
+ historyTypes,
2242
+ labelId: url.searchParams.get("labelId") ?? void 0,
2243
+ maxResults: normalizeLimit(url.searchParams.get("maxResults"), 100, 500),
2244
+ pageToken: url.searchParams.get("pageToken")
2245
+ })
2246
+ );
2247
+ });
2248
+ app.post("/gmail/v1/users/:userId/watch", async (c) => {
2249
+ const authEmail = requireGmailUser(c);
2250
+ if (authEmail instanceof Response) return authEmail;
2251
+ const body = await parseGoogleBody(c);
2252
+ const topicName = getString(body, "topicName")?.trim();
2253
+ if (!topicName) {
2254
+ return googleApiError(c, 400, "Topic name is required.", "invalidArgument", "INVALID_ARGUMENT");
2255
+ }
2256
+ const labelIds = getStringArray(body, "labelIds");
2257
+ const missingLabelIds = findMissingLabelIds(gs, authEmail, labelIds);
2258
+ if (missingLabelIds.length > 0) {
2259
+ return googleApiError(
2260
+ c,
2261
+ 400,
2262
+ `Invalid label IDs: ${missingLabelIds.join(", ")}`,
2263
+ "invalidArgument",
2264
+ "INVALID_ARGUMENT"
2265
+ );
2266
+ }
2267
+ const expiration = String(Date.now() + 24 * 60 * 60 * 1e3);
2268
+ const states = store.getData(WATCH_STATE_KEY) ?? /* @__PURE__ */ new Map();
2269
+ states.set(authEmail, {
2270
+ topicName,
2271
+ labelIds,
2272
+ labelFilterBehavior: getString(body, "labelFilterBehavior", "labelFilterAction") ?? null,
2273
+ expiration
2274
+ });
2275
+ store.setData(WATCH_STATE_KEY, states);
2276
+ return c.json({
2277
+ historyId: getCurrentHistoryId(gs, authEmail),
2278
+ expiration
2279
+ });
2280
+ });
2281
+ app.post("/gmail/v1/users/:userId/stop", (c) => {
2282
+ const authEmail = requireGmailUser(c);
2283
+ if (authEmail instanceof Response) return authEmail;
2284
+ const states = store.getData(WATCH_STATE_KEY) ?? /* @__PURE__ */ new Map();
2285
+ states.delete(authEmail);
2286
+ store.setData(WATCH_STATE_KEY, states);
2287
+ return c.body(null, 200);
2288
+ });
2289
+ }
2290
+ function labelRoutes({ app, store }) {
2291
+ const gs = getGoogleStore(store);
2292
+ app.get("/gmail/v1/users/:userId/labels", (c) => {
2293
+ const authEmail = requireGmailUser(c);
2294
+ if (authEmail instanceof Response) return authEmail;
2295
+ return c.json({
2296
+ labels: formatLabelResources(gs, listLabelsForUser(gs, authEmail))
2297
+ });
2298
+ });
2299
+ app.get("/gmail/v1/users/:userId/labels/:id", (c) => {
2300
+ const authEmail = requireGmailUser(c);
2301
+ if (authEmail instanceof Response) return authEmail;
2302
+ const label = findLabelById(gs, authEmail, c.req.param("id"));
2303
+ if (!label) {
2304
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2305
+ }
2306
+ return c.json(formatLabelResource(gs, label));
2307
+ });
2308
+ app.post("/gmail/v1/users/:userId/labels", async (c) => {
2309
+ const authEmail = requireGmailUser(c);
2310
+ if (authEmail instanceof Response) return authEmail;
2311
+ const body = await parseGoogleBody(c);
2312
+ const name = getString(body, "name")?.trim();
2313
+ if (!name) {
2314
+ return googleApiError(c, 400, "Invalid label name", "invalidArgument", "INVALID_ARGUMENT");
2315
+ }
2316
+ if (findLabelByName(gs, authEmail, name)) {
2317
+ return googleApiError(c, 400, "Label name exists or conflicts", "failedPrecondition", "FAILED_PRECONDITION");
2318
+ }
2319
+ const color = body.color && typeof body.color === "object" && !Array.isArray(body.color) ? body.color : void 0;
2320
+ const label = createLabelRecord(gs, {
2321
+ user_email: authEmail,
2322
+ name,
2323
+ type: "user",
2324
+ message_list_visibility: getString(body, "messageListVisibility", "message_list_visibility") ?? "show",
2325
+ label_list_visibility: getString(body, "labelListVisibility", "label_list_visibility") ?? "labelShow",
2326
+ color_background: typeof color?.backgroundColor === "string" ? color.backgroundColor : getString(body, "color_background"),
2327
+ color_text: typeof color?.textColor === "string" ? color.textColor : getString(body, "color_text")
2328
+ });
2329
+ return c.json(formatLabelResource(gs, label));
2330
+ });
2331
+ app.put("/gmail/v1/users/:userId/labels/:id", async (c) => {
2332
+ return saveLabel(c, gs, true);
2333
+ });
2334
+ app.patch("/gmail/v1/users/:userId/labels/:id", async (c) => {
2335
+ return saveLabel(c, gs, false);
2336
+ });
2337
+ app.delete("/gmail/v1/users/:userId/labels/:id", (c) => {
2338
+ const authEmail = requireGmailUser(c);
2339
+ if (authEmail instanceof Response) return authEmail;
2340
+ const label = findLabelById(gs, authEmail, c.req.param("id"));
2341
+ if (!label) {
2342
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2343
+ }
2344
+ if (isSystemLabelId(label.gmail_id)) {
2345
+ return googleApiError(c, 400, "System labels cannot be deleted.", "invalidArgument", "INVALID_ARGUMENT");
2346
+ }
2347
+ for (const message of gs.messages.findBy("user_email", authEmail)) {
2348
+ if (!message.label_ids.includes(label.gmail_id)) continue;
2349
+ markMessageModified(
2350
+ gs,
2351
+ message,
2352
+ message.label_ids.filter((labelId) => labelId !== label.gmail_id)
2353
+ );
2354
+ }
2355
+ gs.labels.delete(label.id);
2356
+ return c.body(null, 204);
2357
+ });
2358
+ }
2359
+ async function saveLabel(c, gs, replaceMissingFields) {
2360
+ const authEmail = requireGmailUser(c);
2361
+ if (authEmail instanceof Response) return authEmail;
2362
+ const label = findLabelById(gs, authEmail, c.req.param("id"));
2363
+ if (!label) {
2364
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2365
+ }
2366
+ if (isSystemLabelId(label.gmail_id)) {
2367
+ return googleApiError(c, 400, "System labels cannot be modified.", "invalidArgument", "INVALID_ARGUMENT");
2368
+ }
2369
+ const body = await parseGoogleBody(c);
2370
+ const name = getString(body, "name")?.trim();
2371
+ const color = body.color && typeof body.color === "object" && !Array.isArray(body.color) ? body.color : void 0;
2372
+ if (name) {
2373
+ const conflicting = findLabelByName(gs, authEmail, name);
2374
+ if (conflicting && conflicting.gmail_id !== label.gmail_id) {
2375
+ return googleApiError(c, 400, "Label name exists or conflicts", "failedPrecondition", "FAILED_PRECONDITION");
2376
+ }
2377
+ }
2378
+ const updated = updateLabelRecord(gs, label, {
2379
+ name: name ?? (replaceMissingFields ? label.name : void 0),
2380
+ message_list_visibility: getString(body, "messageListVisibility", "message_list_visibility") ?? (replaceMissingFields ? "show" : void 0),
2381
+ label_list_visibility: getString(body, "labelListVisibility", "label_list_visibility") ?? (replaceMissingFields ? "labelShow" : void 0),
2382
+ color_background: typeof color?.backgroundColor === "string" ? color.backgroundColor : getString(body, "color_background") ?? (replaceMissingFields ? null : void 0),
2383
+ color_text: typeof color?.textColor === "string" ? color.textColor : getString(body, "color_text") ?? (replaceMissingFields ? null : void 0)
2384
+ });
2385
+ return c.json(formatLabelResource(gs, updated));
2386
+ }
2387
+ function messageRoutes({ app, store }) {
2388
+ const gs = getGoogleStore(store);
2389
+ const createHandler = (mode) => async (c) => {
2390
+ const authEmail = requireGmailUser(c);
2391
+ if (authEmail instanceof Response) return authEmail;
2392
+ const body = await parseGoogleBody(c);
2393
+ const labelIds = getStringArray(body, "labelIds");
2394
+ const defaultLabelIds = mode === "send" ? dedupeLabelIds([...labelIds, "SENT"]) : labelIds.length > 0 ? labelIds : mode === "import" ? ["INBOX", "UNREAD"] : [];
2395
+ const missingLabelIds = findMissingLabelIds(gs, authEmail, defaultLabelIds);
2396
+ if (missingLabelIds.length > 0) {
2397
+ return googleApiError(
2398
+ c,
2399
+ 400,
2400
+ `Invalid label IDs: ${missingLabelIds.join(", ")}`,
2401
+ "invalidArgument",
2402
+ "INVALID_ARGUMENT"
2403
+ );
2404
+ }
2405
+ const messageInput = parseMessageInputFromBody(body, {
2406
+ from: mode === "send" ? authEmail : void 0
2407
+ });
2408
+ if (!messageInput.raw && (!messageInput.from || !messageInput.to)) {
2409
+ return googleApiError(
2410
+ c,
2411
+ 400,
2412
+ "A raw MIME message or explicit from/to fields are required.",
2413
+ "invalidArgument",
2414
+ "INVALID_ARGUMENT"
2415
+ );
2416
+ }
2417
+ try {
2418
+ const message = createStoredMessage(gs, {
2419
+ user_email: authEmail,
2420
+ ...messageInput,
2421
+ label_ids: defaultLabelIds
2422
+ });
2423
+ return c.json(formatMessageResource(gs, message, "full"));
2424
+ } catch {
2425
+ return googleApiError(c, 400, "Invalid raw MIME message payload.", "invalidArgument", "INVALID_ARGUMENT");
2426
+ }
2427
+ };
2428
+ app.get("/gmail/v1/users/:userId/messages", (c) => {
2429
+ const authEmail = requireGmailUser(c);
2430
+ if (authEmail instanceof Response) return authEmail;
2431
+ const url = new URL(c.req.url);
2432
+ const messages = listMessagesForUser(gs, authEmail, {
2433
+ labelIds: url.searchParams.getAll("labelIds"),
2434
+ query: url.searchParams.get("q")?.trim() ?? void 0,
2435
+ includeSpamTrash: parseBooleanParam(url.searchParams.get("includeSpamTrash"))
2436
+ });
2437
+ const offset = parseOffset(url.searchParams.get("pageToken"));
2438
+ const limit = normalizeLimit(url.searchParams.get("maxResults"), 100, 500);
2439
+ const page = messages.slice(offset, offset + limit);
2440
+ const nextPageToken = offset + limit < messages.length ? String(offset + limit) : void 0;
2441
+ return c.json({
2442
+ messages: page.map((message) => ({
2443
+ id: message.gmail_id,
2444
+ threadId: message.thread_id
2445
+ })),
2446
+ nextPageToken,
2447
+ resultSizeEstimate: messages.length
2448
+ });
2449
+ });
2450
+ app.post("/gmail/v1/users/:userId/messages/batchModify", async (c) => {
2451
+ const authEmail = requireGmailUser(c);
2452
+ if (authEmail instanceof Response) return authEmail;
2453
+ const body = await parseGoogleBody(c);
2454
+ const ids = getStringArray(body, "ids");
2455
+ const addLabelIds = getStringArray(body, "addLabelIds");
2456
+ const removeLabelIds = getStringArray(body, "removeLabelIds");
2457
+ const missingLabelIds = findMissingLabelIds(gs, authEmail, [...addLabelIds, ...removeLabelIds]);
2458
+ if (missingLabelIds.length > 0) {
2459
+ return googleApiError(
2460
+ c,
2461
+ 400,
2462
+ `Invalid label IDs: ${missingLabelIds.join(", ")}`,
2463
+ "invalidArgument",
2464
+ "INVALID_ARGUMENT"
2465
+ );
2466
+ }
2467
+ for (const messageId of ids) {
2468
+ const message = getMessageById(gs, authEmail, messageId);
2469
+ if (!message) continue;
2470
+ markMessageModified(gs, message, applyLabelMutation(message.label_ids, addLabelIds, removeLabelIds));
2471
+ }
2472
+ return c.body(null, 204);
2473
+ });
2474
+ app.post("/gmail/v1/users/:userId/messages/batchDelete", async (c) => {
2475
+ const authEmail = requireGmailUser(c);
2476
+ if (authEmail instanceof Response) return authEmail;
2477
+ const body = await parseGoogleBody(c);
2478
+ const ids = getStringArray(body, "ids");
2479
+ for (const messageId of ids) {
2480
+ const message = getMessageById(gs, authEmail, messageId);
2481
+ if (message) deleteMessage(gs, message);
2482
+ }
2483
+ return c.body(null, 204);
2484
+ });
2485
+ app.post("/gmail/v1/users/:userId/messages/import", createHandler("import"));
2486
+ app.post("/upload/gmail/v1/users/:userId/messages/import", createHandler("import"));
2487
+ app.post("/gmail/v1/users/:userId/messages/send", createHandler("send"));
2488
+ app.post("/upload/gmail/v1/users/:userId/messages/send", createHandler("send"));
2489
+ app.post("/gmail/v1/users/:userId/messages", createHandler("insert"));
2490
+ app.post("/upload/gmail/v1/users/:userId/messages", createHandler("insert"));
2491
+ app.get("/gmail/v1/users/:userId/messages/:messageId/attachments/:id", (c) => {
2492
+ const authEmail = requireGmailUser(c);
2493
+ if (authEmail instanceof Response) return authEmail;
2494
+ const message = getMessageById(gs, authEmail, c.req.param("messageId"));
2495
+ if (!message) {
2496
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2497
+ }
2498
+ const attachment = getAttachmentById(gs, authEmail, message.gmail_id, c.req.param("id"));
2499
+ if (!attachment) {
2500
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2501
+ }
2502
+ return c.json({
2503
+ attachmentId: attachment.gmail_id,
2504
+ size: attachment.size,
2505
+ data: attachment.data
2506
+ });
2507
+ });
2508
+ app.get("/gmail/v1/users/:userId/messages/:id", (c) => {
2509
+ const authEmail = requireGmailUser(c);
2510
+ if (authEmail instanceof Response) return authEmail;
2511
+ const message = getMessageById(gs, authEmail, c.req.param("id"));
2512
+ if (!message) {
2513
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2514
+ }
2515
+ const url = new URL(c.req.url);
2516
+ return c.json(
2517
+ formatMessageResource(
2518
+ gs,
2519
+ message,
2520
+ parseFormat(url.searchParams.get("format")),
2521
+ url.searchParams.getAll("metadataHeaders")
2522
+ )
2523
+ );
2524
+ });
2525
+ app.post("/gmail/v1/users/:userId/messages/:id/modify", async (c) => {
2526
+ const authEmail = requireGmailUser(c);
2527
+ if (authEmail instanceof Response) return authEmail;
2528
+ const message = getMessageById(gs, authEmail, c.req.param("id"));
2529
+ if (!message) {
2530
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2531
+ }
2532
+ const body = await parseGoogleBody(c);
2533
+ const addLabelIds = getStringArray(body, "addLabelIds");
2534
+ const removeLabelIds = getStringArray(body, "removeLabelIds");
2535
+ const missingLabelIds = findMissingLabelIds(gs, authEmail, [...addLabelIds, ...removeLabelIds]);
2536
+ if (missingLabelIds.length > 0) {
2537
+ return googleApiError(
2538
+ c,
2539
+ 400,
2540
+ `Invalid label IDs: ${missingLabelIds.join(", ")}`,
2541
+ "invalidArgument",
2542
+ "INVALID_ARGUMENT"
2543
+ );
2544
+ }
2545
+ const updated = markMessageModified(
2546
+ gs,
2547
+ message,
2548
+ applyLabelMutation(message.label_ids, addLabelIds, removeLabelIds)
2549
+ );
2550
+ return c.json(formatMessageResource(gs, updated, "full"));
2551
+ });
2552
+ app.post("/gmail/v1/users/:userId/messages/:id/trash", (c) => {
2553
+ const authEmail = requireGmailUser(c);
2554
+ if (authEmail instanceof Response) return authEmail;
2555
+ const message = getMessageById(gs, authEmail, c.req.param("id"));
2556
+ if (!message) {
2557
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2558
+ }
2559
+ return c.json(
2560
+ formatMessageResource(gs, markMessageModified(gs, message, trashLabelIds(message.label_ids)), "full")
2561
+ );
2562
+ });
2563
+ app.post("/gmail/v1/users/:userId/messages/:id/untrash", (c) => {
2564
+ const authEmail = requireGmailUser(c);
2565
+ if (authEmail instanceof Response) return authEmail;
2566
+ const message = getMessageById(gs, authEmail, c.req.param("id"));
2567
+ if (!message) {
2568
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2569
+ }
2570
+ return c.json(
2571
+ formatMessageResource(gs, markMessageModified(gs, message, untrashLabelIds(message.label_ids)), "full")
2572
+ );
2573
+ });
2574
+ app.delete("/gmail/v1/users/:userId/messages/:id", (c) => {
2575
+ const authEmail = requireGmailUser(c);
2576
+ if (authEmail instanceof Response) return authEmail;
2577
+ const message = getMessageById(gs, authEmail, c.req.param("id"));
2578
+ if (!message) {
2579
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2580
+ }
2581
+ deleteMessage(gs, message);
2582
+ return c.body(null, 204);
2583
+ });
2584
+ }
2585
+ function createErrorHandler(documentationUrl) {
2586
+ return async (c, next) => {
2587
+ if (documentationUrl) {
2588
+ c.set("docsUrl", documentationUrl);
2589
+ }
2590
+ await next();
2591
+ };
2592
+ }
2593
+ var errorHandler = createErrorHandler();
2594
+ var isDebug = typeof process !== "undefined" && (process.env.DEBUG === "1" || process.env.DEBUG === "true" || process.env.EMULATE_DEBUG === "1");
2595
+ function debug(label, ...args) {
2596
+ if (isDebug) {
2597
+ console.log(`[${label}]`, ...args);
2598
+ }
2599
+ }
2600
+ function escapeHtml(s) {
2601
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2602
+ }
2603
+ function escapeAttr(s) {
2604
+ return escapeHtml(s).replace(/'/g, "&#39;");
2605
+ }
2606
+ var CSS = `
2607
+ @font-face{
2608
+ font-family:'Geist';font-style:normal;font-weight:100 900;font-display:swap;
2609
+ src:url('/_emulate/fonts/geist-sans.woff2') format('woff2');
2610
+ }
2611
+ @font-face{
2612
+ font-family:'Geist Pixel';font-style:normal;font-weight:400;font-display:swap;
2613
+ src:url('/_emulate/fonts/GeistPixel-Square.woff2') format('woff2');
2614
+ }
2615
+ *{box-sizing:border-box;margin:0;padding:0}
2616
+ body{
2617
+ font-family:'Geist',-apple-system,BlinkMacSystemFont,sans-serif;
2618
+ background:#000;color:#33ff00;min-height:100vh;
2619
+ -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
2620
+ }
2621
+ .emu-bar{
2622
+ border-bottom:1px solid #0a3300;padding:10px 20px;
2623
+ display:flex;align-items:center;gap:10px;font-size:.8125rem;color:#1a8c00;
2624
+ }
2625
+ .emu-bar-title{font-weight:600;color:#33ff00;font-family:'Geist Pixel',monospace;}
2626
+ .emu-bar-links{margin-left:auto;display:flex;gap:16px;}
2627
+ .emu-bar-links a{
2628
+ color:#1a8c00;font-size:.75rem;text-decoration:none;transition:color .15s;
2629
+ }
2630
+ .emu-bar-links a:hover{color:#33ff00;}
2631
+ .emu-bar-links a .full{display:inline;}
2632
+ .emu-bar-links a .short{display:none;}
2633
+ @media(max-width:600px){
2634
+ .emu-bar-links a .full{display:none;}
2635
+ .emu-bar-links a .short{display:inline;}
2636
+ }
2637
+
2638
+ .content{
2639
+ display:flex;align-items:center;justify-content:center;
2640
+ min-height:calc(100vh - 42px);padding:24px 16px;
2641
+ }
2642
+ .content-inner{width:100%;max-width:420px;}
2643
+ .card-title{
2644
+ font-family:'Geist Pixel',monospace;
2645
+ font-size:1.125rem;font-weight:600;margin-bottom:4px;color:#33ff00;
2646
+ }
2647
+ .card-subtitle{color:#1a8c00;font-size:.8125rem;margin-bottom:18px;line-height:1.45;}
2648
+ .powered-by{
2649
+ position:fixed;bottom:0;left:0;right:0;
2650
+ text-align:center;padding:12px;font-size:.6875rem;color:#0a3300;
2651
+ font-family:'Geist Pixel',monospace;
2652
+ }
2653
+ .powered-by a{color:#1a8c00;text-decoration:none;transition:color .15s;}
2654
+ .powered-by a:hover{color:#33ff00;}
2655
+
2656
+ .error-title{
2657
+ font-family:'Geist Pixel',monospace;
2658
+ color:#ff4444;font-size:1.125rem;font-weight:600;margin-bottom:8px;
2659
+ }
2660
+ .error-msg{color:#1a8c00;font-size:.875rem;line-height:1.5;}
2661
+ .error-card{text-align:center;}
2662
+
2663
+ .user-form{margin-bottom:8px;}
2664
+ .user-form:last-of-type{margin-bottom:0;}
2665
+ .user-btn{
2666
+ width:100%;display:flex;align-items:center;gap:12px;
2667
+ padding:10px 12px;border:1px solid #0a3300;border-radius:8px;
2668
+ background:#000;color:inherit;cursor:pointer;text-align:left;
2669
+ font:inherit;transition:border-color .15s;
2670
+ }
2671
+ .user-btn:hover{border-color:#33ff00;}
2672
+ .avatar{
2673
+ width:36px;height:36px;border-radius:50%;
2674
+ background:#0a3300;color:#33ff00;font-weight:600;font-size:.875rem;
2675
+ display:flex;align-items:center;justify-content:center;flex-shrink:0;
2676
+ font-family:'Geist Pixel',monospace;
2677
+ }
2678
+ .user-text{min-width:0;}
2679
+ .user-login{font-weight:600;font-size:.875rem;display:block;color:#33ff00;}
2680
+ .user-meta{color:#1a8c00;font-size:.75rem;margin-top:1px;}
2681
+ .user-email{font-size:.6875rem;color:#116600;word-break:break-all;margin-top:1px;}
2682
+
2683
+ .settings-layout{
2684
+ max-width:920px;margin:0 auto;padding:28px 20px;
2685
+ display:flex;gap:28px;
2686
+ }
2687
+ .settings-sidebar{width:200px;flex-shrink:0;}
2688
+ .settings-sidebar a{
2689
+ display:block;padding:6px 10px;border-radius:6px;color:#1a8c00;
2690
+ text-decoration:none;font-size:.8125rem;transition:color .15s;
2691
+ }
2692
+ .settings-sidebar a:hover{color:#33ff00;}
2693
+ .settings-sidebar a.active{color:#33ff00;font-weight:600;}
2694
+ .settings-main{flex:1;min-width:0;}
2695
+
2696
+ .s-card{
2697
+ padding:18px 0;margin-bottom:14px;border-bottom:1px solid #0a3300;
2698
+ }
2699
+ .s-card:last-child{border-bottom:none;}
2700
+ .s-card-header{display:flex;align-items:center;gap:14px;margin-bottom:14px;}
2701
+ .s-icon{
2702
+ width:42px;height:42px;border-radius:8px;
2703
+ background:#0a3300;display:flex;align-items:center;justify-content:center;
2704
+ font-size:1.125rem;font-weight:700;color:#116600;flex-shrink:0;
2705
+ font-family:'Geist Pixel',monospace;
2706
+ }
2707
+ .s-title{
2708
+ font-family:'Geist Pixel',monospace;
2709
+ font-size:1.25rem;font-weight:600;color:#33ff00;
2710
+ }
2711
+ .s-subtitle{font-size:.75rem;color:#1a8c00;margin-top:2px;}
2712
+ .section-heading{
2713
+ font-size:.9375rem;font-weight:600;margin-bottom:10px;color:#33ff00;
2714
+ display:flex;align-items:center;justify-content:space-between;
2715
+ }
2716
+ .perm-list{list-style:none;}
2717
+ .perm-list li{padding:5px 0;font-size:.8125rem;display:flex;align-items:center;gap:6px;color:#1a8c00;}
2718
+ .check{color:#33ff00;}
2719
+ .org-row{
2720
+ display:flex;align-items:center;gap:8px;padding:7px 0;
2721
+ border-bottom:1px solid #0a3300;font-size:.8125rem;
2722
+ }
2723
+ .org-row:last-child{border-bottom:none;}
2724
+ .org-icon{
2725
+ width:22px;height:22px;border-radius:4px;background:#0a3300;
2726
+ display:flex;align-items:center;justify-content:center;
2727
+ font-size:.625rem;font-weight:700;color:#116600;flex-shrink:0;
2728
+ font-family:'Geist Pixel',monospace;
2729
+ }
2730
+ .org-name{font-weight:600;color:#33ff00;}
2731
+ .badge{font-size:.6875rem;padding:1px 7px;border-radius:999px;font-weight:500;}
2732
+ .badge-granted{background:#0a3300;color:#33ff00;}
2733
+ .badge-denied{background:#1a0a0a;color:#ff4444;}
2734
+ .badge-requested{background:#0a3300;color:#1a8c00;}
2735
+ .btn-revoke{
2736
+ display:inline-block;padding:5px 14px;border-radius:6px;
2737
+ border:1px solid #0a3300;background:transparent;color:#ff4444;
2738
+ font-size:.75rem;font-weight:600;cursor:pointer;transition:border-color .15s;
2739
+ }
2740
+ .btn-revoke:hover{border-color:#ff4444;}
2741
+ .info-text{color:#1a8c00;font-size:.75rem;line-height:1.5;margin-top:10px;}
2742
+ .info-text a,.section-heading a{color:#1a8c00;text-decoration:none;transition:color .15s;}
2743
+ .info-text a:hover,.section-heading a:hover{color:#33ff00;}
2744
+ code{font-family:'Geist Mono','SF Mono',ui-monospace,monospace;font-size:.8125rem;color:#33ff00;word-break:break-all;}
2745
+ .code-block{
2746
+ background:#020;border:1px solid #0a3300;border-radius:6px;padding:10px 12px;
2747
+ margin:8px 0 12px;overflow-x:auto;
2748
+ }
2749
+ .code-block code{white-space:pre;word-break:normal;display:block;line-height:1.5;}
2750
+ .app-link{
2751
+ display:flex;align-items:center;gap:12px;padding:12px;
2752
+ border:1px solid #0a3300;border-radius:8px;background:#000;
2753
+ text-decoration:none;color:inherit;margin-bottom:8px;transition:border-color .15s;
2754
+ }
2755
+ .app-link:hover{border-color:#33ff00;}
2756
+ .app-link-name{font-weight:600;font-size:.875rem;color:#33ff00;}
2757
+ .app-link-scopes{font-size:.6875rem;color:#1a8c00;margin-top:1px;}
2758
+ .empty{color:#1a8c00;text-align:center;padding:28px 0;font-size:.875rem;}
2759
+
2760
+ .inspector-layout{max-width:960px;margin:0 auto;padding:28px 20px;}
2761
+ .inspector-tabs{display:flex;gap:4px;margin-bottom:20px;}
2762
+ .inspector-tabs a{
2763
+ padding:7px 16px;border-radius:6px;text-decoration:none;
2764
+ font-size:.8125rem;color:#1a8c00;border:1px solid transparent;
2765
+ transition:color .15s,border-color .15s;
2766
+ }
2767
+ .inspector-tabs a:hover{color:#33ff00;}
2768
+ .inspector-tabs a.active{color:#33ff00;font-weight:600;border-color:#0a3300;background:#0a3300;}
2769
+ .inspector-section{margin-bottom:24px;}
2770
+ .inspector-section h2{
2771
+ font-family:'Geist Pixel',monospace;
2772
+ font-size:1rem;font-weight:600;color:#33ff00;margin-bottom:10px;
2773
+ }
2774
+ .inspector-section h3{
2775
+ font-family:'Geist Pixel',monospace;
2776
+ font-size:.875rem;font-weight:600;color:#1a8c00;margin:16px 0 8px;
2777
+ }
2778
+ .inspector-table{width:100%;border-collapse:collapse;margin-bottom:12px;}
2779
+ .inspector-table th,.inspector-table td{
2780
+ text-align:left;padding:8px 12px;border-bottom:1px solid #0a3300;
2781
+ font-size:.8125rem;
2782
+ }
2783
+ .inspector-table th{color:#1a8c00;font-weight:600;font-size:.75rem;text-transform:uppercase;letter-spacing:.04em;}
2784
+ .inspector-table td{color:#33ff00;}
2785
+ .inspector-table tbody tr{transition:background .1s;}
2786
+ .inspector-table tbody tr:hover{background:#0a3300;}
2787
+ .inspector-empty{color:#1a8c00;text-align:center;padding:20px 0;font-size:.8125rem;}
2788
+
2789
+ .checkout-layout{
2790
+ display:flex;min-height:calc(100vh - 42px);
2791
+ }
2792
+ .checkout-summary{
2793
+ flex:1;background:#020;padding:48px 40px 48px 10%;
2794
+ display:flex;flex-direction:column;justify-content:center;
2795
+ border-right:1px solid #0a3300;
2796
+ }
2797
+ .checkout-form-side{
2798
+ flex:1;background:#000;padding:48px 10% 48px 40px;
2799
+ display:flex;flex-direction:column;justify-content:center;
2800
+ }
2801
+ .checkout-merchant{
2802
+ display:flex;align-items:center;gap:10px;margin-bottom:6px;
2803
+ }
2804
+ .checkout-merchant-name{
2805
+ font-family:'Geist Pixel',monospace;
2806
+ font-size:.9375rem;font-weight:600;color:#33ff00;
2807
+ }
2808
+ .checkout-test-badge{
2809
+ font-size:.625rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;
2810
+ background:#0a3300;color:#1a8c00;padding:2px 8px;border-radius:4px;
2811
+ }
2812
+ .checkout-total{
2813
+ font-family:'Geist Pixel',monospace;
2814
+ font-size:2rem;font-weight:700;color:#33ff00;margin:8px 0 28px;
2815
+ }
2816
+ .checkout-line-item{
2817
+ display:flex;align-items:center;gap:14px;padding:14px 0;
2818
+ border-bottom:1px solid #0a3300;
2819
+ }
2820
+ .checkout-line-item:first-child{border-top:1px solid #0a3300;}
2821
+ .checkout-item-icon{
2822
+ width:42px;height:42px;border-radius:6px;background:#0a3300;
2823
+ display:flex;align-items:center;justify-content:center;flex-shrink:0;
2824
+ font-family:'Geist Pixel',monospace;font-size:.875rem;font-weight:700;color:#116600;
2825
+ }
2826
+ .checkout-item-details{flex:1;min-width:0;}
2827
+ .checkout-item-name{font-size:.875rem;font-weight:600;color:#33ff00;}
2828
+ .checkout-item-qty{font-size:.75rem;color:#1a8c00;margin-top:2px;}
2829
+ .checkout-item-price{
2830
+ font-size:.875rem;font-weight:600;color:#33ff00;text-align:right;white-space:nowrap;
2831
+ }
2832
+ .checkout-item-unit{font-size:.6875rem;color:#1a8c00;text-align:right;margin-top:2px;}
2833
+ .checkout-totals{margin-top:20px;}
2834
+ .checkout-totals-row{
2835
+ display:flex;justify-content:space-between;padding:6px 0;
2836
+ font-size:.8125rem;color:#1a8c00;
2837
+ }
2838
+ .checkout-totals-row.total{
2839
+ border-top:1px solid #0a3300;margin-top:8px;padding-top:14px;
2840
+ font-size:.9375rem;font-weight:600;color:#33ff00;
2841
+ }
2842
+ .checkout-form-section{margin-bottom:24px;}
2843
+ .checkout-form-label{
2844
+ font-size:.8125rem;font-weight:600;color:#33ff00;margin-bottom:8px;display:block;
2845
+ }
2846
+ .checkout-input{
2847
+ width:100%;padding:10px 12px;border:1px solid #0a3300;border-radius:6px;
2848
+ background:#020;color:#33ff00;font:inherit;font-size:.875rem;
2849
+ transition:border-color .15s;outline:none;
2850
+ }
2851
+ .checkout-input:focus{border-color:#33ff00;}
2852
+ .checkout-input::placeholder{color:#116600;}
2853
+ .checkout-card-box{
2854
+ border:1px solid #0a3300;border-radius:6px;padding:14px;
2855
+ background:#020;
2856
+ }
2857
+ .checkout-card-row{
2858
+ display:flex;gap:12px;margin-top:10px;
2859
+ }
2860
+ .checkout-card-row .checkout-input{flex:1;}
2861
+ .checkout-sim-note{
2862
+ font-size:.6875rem;color:#1a8c00;margin-top:10px;text-align:center;
2863
+ font-style:italic;
2864
+ }
2865
+ .checkout-pay-btn{
2866
+ width:100%;padding:14px;border:none;border-radius:8px;
2867
+ background:#33ff00;color:#000;font:inherit;font-size:.9375rem;font-weight:700;
2868
+ cursor:pointer;transition:background .15s;
2869
+ font-family:'Geist Pixel',monospace;
2870
+ }
2871
+ .checkout-pay-btn:hover{background:#44ff22;}
2872
+ .checkout-cancel{
2873
+ text-align:center;margin-top:14px;
2874
+ }
2875
+ .checkout-cancel a{
2876
+ color:#1a8c00;text-decoration:none;font-size:.8125rem;
2877
+ transition:color .15s;
2878
+ }
2879
+ .checkout-cancel a:hover{color:#33ff00;}
2880
+ @media(max-width:768px){
2881
+ .checkout-layout{flex-direction:column;}
2882
+ .checkout-summary{padding:32px 20px;border-right:none;border-bottom:1px solid #0a3300;}
2883
+ .checkout-form-side{padding:32px 20px;}
2884
+ }
2885
+ `;
2886
+ var POWERED_BY = `<div class="powered-by">Powered by <a href="https://emulate.dev" target="_blank" rel="noopener">emulate</a></div>`;
2887
+ function emuBar(service) {
2888
+ const title = service ? `${escapeHtml(service)} Emulator` : "Emulator";
2889
+ return `<div class="emu-bar">
2890
+ <span class="emu-bar-title">${title}</span>
2891
+ <nav class="emu-bar-links">
2892
+ <a href="https://github.com/vercel-labs/emulate/issues" target="_blank" rel="noopener"><span class="full">Report Issue</span><span class="short">Report</span></a>
2893
+ <a href="https://github.com/vercel-labs/emulate" target="_blank" rel="noopener"><span class="full">Source Code</span><span class="short">Source</span></a>
2894
+ <a href="https://emulate.dev" target="_blank" rel="noopener"><span class="full">Learn More</span><span class="short">Learn</span></a>
2895
+ </nav>
2896
+ </div>`;
2897
+ }
2898
+ function head(title) {
2899
+ return `<!DOCTYPE html>
2900
+ <html lang="en">
2901
+ <head>
2902
+ <meta charset="utf-8"/>
2903
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
2904
+ <link rel="icon" href="/_emulate/favicon.ico"/>
2905
+ <title>${escapeHtml(title)} | emulate</title>
2906
+ <style>${CSS}</style>
2907
+ </head>`;
2908
+ }
2909
+ function renderCardPage(title, subtitle, body, service) {
2910
+ return `${head(title)}
2911
+ <body>
2912
+ ${emuBar(service)}
2913
+ <div class="content">
2914
+ <div class="content-inner">
2915
+ <div class="card-title">${escapeHtml(title)}</div>
2916
+ <div class="card-subtitle">${subtitle}</div>
2917
+ ${body}
2918
+ </div>
2919
+ </div>
2920
+ ${POWERED_BY}
2921
+ </body></html>`;
2922
+ }
2923
+ function renderErrorPage(title, message, service) {
2924
+ return `${head(title)}
2925
+ <body>
2926
+ ${emuBar(service)}
2927
+ <div class="content">
2928
+ <div class="content-inner error-card">
2929
+ <div class="error-title">${escapeHtml(title)}</div>
2930
+ <div class="error-msg">${escapeHtml(message)}</div>
2931
+ </div>
2932
+ </div>
2933
+ ${POWERED_BY}
2934
+ </body></html>`;
2935
+ }
2936
+ function renderUserButton(opts) {
2937
+ const hiddens = Object.entries(opts.hiddenFields).map(([k, v]) => `<input type="hidden" name="${escapeAttr(k)}" value="${escapeAttr(v)}"/>`).join("");
2938
+ const nameLine = opts.name ? `<div class="user-meta">${escapeHtml(opts.name)}</div>` : "";
2939
+ const emailLine = opts.email ? `<div class="user-email">${escapeHtml(opts.email)}</div>` : "";
2940
+ return `<form class="user-form" method="post" action="${escapeAttr(opts.formAction)}">
2941
+ ${hiddens}
2942
+ <button type="submit" class="user-btn">
2943
+ <span class="avatar">${escapeHtml(opts.letter)}</span>
2944
+ <span class="user-text">
2945
+ <span class="user-login">${escapeHtml(opts.login)}</span>
2946
+ ${nameLine}${emailLine}
2947
+ </span>
2948
+ </button>
2949
+ </form>`;
2950
+ }
2951
+ function normalizeUri(uri) {
2952
+ try {
2953
+ const u = new URL(uri);
2954
+ return `${u.origin}${u.pathname.replace(/\/+$/, "")}`;
2955
+ } catch {
2956
+ return uri.replace(/\/+$/, "").split("?")[0];
2957
+ }
2958
+ }
2959
+ function matchesRedirectUri(incoming, registered) {
2960
+ const normalized = normalizeUri(incoming);
2961
+ return registered.some((r) => normalizeUri(r) === normalized);
2962
+ }
2963
+ function constantTimeSecretEqual(a, b) {
2964
+ const bufA = Buffer.from(a, "utf-8");
2965
+ const bufB = Buffer.from(b, "utf-8");
2966
+ if (bufA.length !== bufB.length) return false;
2967
+ return timingSafeEqual(bufA, bufB);
2968
+ }
2969
+ function bodyStr(v) {
2970
+ if (typeof v === "string") return v;
2971
+ if (Array.isArray(v) && typeof v[0] === "string") return v[0];
2972
+ return "";
2973
+ }
2974
+ var JWT_SECRET = new TextEncoder().encode("emulate-google-jwt-secret");
2975
+ var PENDING_CODE_TTL_MS = 10 * 60 * 1e3;
2976
+ function getPendingCodes(store) {
2977
+ let map = store.getData("google.oauth.pendingCodes");
2978
+ if (!map) {
2979
+ map = /* @__PURE__ */ new Map();
2980
+ store.setData("google.oauth.pendingCodes", map);
2981
+ }
2982
+ return map;
2983
+ }
2984
+ function getRefreshTokens(store) {
2985
+ let map = store.getData("google.oauth.refreshTokens");
2986
+ if (!map) {
2987
+ map = /* @__PURE__ */ new Map();
2988
+ store.setData("google.oauth.refreshTokens", map);
2989
+ }
2990
+ return map;
2991
+ }
2992
+ function isPendingCodeExpired(p) {
2993
+ return Date.now() - p.created_at > PENDING_CODE_TTL_MS;
2994
+ }
2995
+ var SERVICE_LABEL = "Google";
2996
+ async function createIdToken(user, clientId, nonce, baseUrl) {
2997
+ const builder = new SignJWT({
2998
+ sub: user.uid,
2999
+ email: user.email,
3000
+ email_verified: user.email_verified,
3001
+ name: user.name,
3002
+ given_name: user.given_name,
3003
+ family_name: user.family_name,
3004
+ picture: user.picture,
3005
+ locale: user.locale,
3006
+ ...user.hd ? { hd: user.hd } : {},
3007
+ ...nonce ? { nonce } : {}
3008
+ }).setProtectedHeader({ alg: "HS256", typ: "JWT" }).setIssuer(baseUrl).setAudience(clientId).setIssuedAt().setExpirationTime("1h");
3009
+ return builder.sign(JWT_SECRET);
3010
+ }
3011
+ function oauthRoutes({ app, store, baseUrl, tokenMap }) {
3012
+ const gs = getGoogleStore(store);
3013
+ app.get("/.well-known/openid-configuration", (c) => {
3014
+ return c.json({
3015
+ issuer: baseUrl,
3016
+ authorization_endpoint: `${baseUrl}/o/oauth2/v2/auth`,
3017
+ token_endpoint: `${baseUrl}/oauth2/token`,
3018
+ userinfo_endpoint: `${baseUrl}/oauth2/v2/userinfo`,
3019
+ revocation_endpoint: `${baseUrl}/oauth2/revoke`,
3020
+ jwks_uri: `${baseUrl}/oauth2/v3/certs`,
3021
+ response_types_supported: ["code"],
3022
+ subject_types_supported: ["public"],
3023
+ id_token_signing_alg_values_supported: ["HS256"],
3024
+ scopes_supported: ["openid", "email", "profile"],
3025
+ token_endpoint_auth_methods_supported: ["client_secret_post", "client_secret_basic"],
3026
+ claims_supported: [
3027
+ "sub",
3028
+ "email",
3029
+ "email_verified",
3030
+ "name",
3031
+ "given_name",
3032
+ "family_name",
3033
+ "picture",
3034
+ "locale",
3035
+ "hd"
3036
+ ],
3037
+ code_challenge_methods_supported: ["plain", "S256"]
3038
+ });
3039
+ });
3040
+ app.get("/oauth2/v3/certs", (c) => {
3041
+ return c.json({ keys: [] });
3042
+ });
3043
+ app.get("/discovery/v1/apis/:api/:version/rest", async (c) => {
3044
+ const api = c.req.param("api");
3045
+ const version = c.req.param("version");
3046
+ const cache = (() => {
3047
+ let m = store.getData("google.discoveryCache");
3048
+ if (!m) {
3049
+ m = /* @__PURE__ */ new Map();
3050
+ store.setData("google.discoveryCache", m);
3051
+ }
3052
+ return m;
3053
+ })();
3054
+ const key = `${api}/${version}`;
3055
+ let doc = cache.get(key);
3056
+ if (!doc) {
3057
+ const upstream = `https://www.googleapis.com/discovery/v1/apis/${api}/${version}/rest`;
3058
+ const res = await fetch(upstream);
3059
+ if (!res.ok) return c.json({ error: "discovery_fetch_failed", api, version, status: res.status }, 502);
3060
+ doc = await res.json();
3061
+ const root = `${baseUrl}/`;
3062
+ doc.rootUrl = root;
3063
+ doc.baseUrl = root;
3064
+ if (doc.mtlsRootUrl) doc.mtlsRootUrl = root;
3065
+ cache.set(key, doc);
3066
+ }
3067
+ return c.json(doc);
3068
+ });
3069
+ app.get("/o/oauth2/v2/auth", (c) => {
3070
+ const client_id = c.req.query("client_id") ?? "";
3071
+ const redirect_uri = c.req.query("redirect_uri") ?? "";
3072
+ const scope = c.req.query("scope") ?? "";
3073
+ const state = c.req.query("state") ?? "";
3074
+ const nonce = c.req.query("nonce") ?? "";
3075
+ const code_challenge = c.req.query("code_challenge") ?? "";
3076
+ const code_challenge_method = c.req.query("code_challenge_method") ?? "";
3077
+ const clientsConfigured = gs.oauthClients.all().length > 0;
3078
+ let clientName = "";
3079
+ if (clientsConfigured) {
3080
+ const client = gs.oauthClients.findOneBy("client_id", client_id);
3081
+ if (!client) {
3082
+ return c.html(
3083
+ renderErrorPage("Application not found", `The client_id '${client_id}' is not registered.`, SERVICE_LABEL),
3084
+ 400
3085
+ );
3086
+ }
3087
+ if (redirect_uri && !matchesRedirectUri(redirect_uri, client.redirect_uris)) {
3088
+ return c.html(
3089
+ renderErrorPage(
3090
+ "Redirect URI mismatch",
3091
+ "The redirect_uri is not registered for this application.",
3092
+ SERVICE_LABEL
3093
+ ),
3094
+ 400
3095
+ );
3096
+ }
3097
+ clientName = client.name;
3098
+ }
3099
+ const subtitleText = clientName ? `Sign in to <strong>${escapeHtml(clientName)}</strong> with your Google account.` : "Choose a seeded user to continue.";
3100
+ const users = gs.users.all();
3101
+ const userButtons = users.map((user) => {
3102
+ return renderUserButton({
3103
+ letter: (user.email[0] ?? "?").toUpperCase(),
3104
+ login: user.email,
3105
+ name: user.name,
3106
+ email: user.email,
3107
+ // Absolute (baseUrl-prefixed) so the POST keeps the instance path when
3108
+ // the emulator is served under a path prefix (e.g. CF /google/<id>).
3109
+ formAction: `${baseUrl}/o/oauth2/v2/auth/callback`,
3110
+ hiddenFields: {
3111
+ email: user.email,
3112
+ redirect_uri,
3113
+ scope,
3114
+ state,
3115
+ nonce,
3116
+ client_id,
3117
+ code_challenge,
3118
+ code_challenge_method
3119
+ }
3120
+ });
3121
+ }).join("\n");
3122
+ const body = users.length === 0 ? '<p class="empty">No users in the emulator store.</p>' : userButtons;
3123
+ return c.html(renderCardPage("Sign in to Google", subtitleText, body, SERVICE_LABEL));
3124
+ });
3125
+ app.post("/o/oauth2/v2/auth/callback", async (c) => {
3126
+ const body = await c.req.parseBody();
3127
+ const email = bodyStr(body.email);
3128
+ const redirect_uri = bodyStr(body.redirect_uri);
3129
+ const scope = bodyStr(body.scope);
3130
+ const state = bodyStr(body.state);
3131
+ const client_id = bodyStr(body.client_id);
3132
+ const nonce = bodyStr(body.nonce);
3133
+ const code_challenge = bodyStr(body.code_challenge);
3134
+ const code_challenge_method = bodyStr(body.code_challenge_method);
3135
+ const code = randomBytes2(20).toString("hex");
3136
+ getPendingCodes(store).set(code, {
3137
+ email,
3138
+ scope,
3139
+ redirectUri: redirect_uri,
3140
+ clientId: client_id,
3141
+ nonce: nonce || null,
3142
+ codeChallenge: code_challenge || null,
3143
+ codeChallengeMethod: code_challenge_method || null,
3144
+ created_at: Date.now()
3145
+ });
3146
+ debug("google.oauth", `[Google callback] code=${code.slice(0, 8)}... email=${email}`);
3147
+ const url = new URL(redirect_uri);
3148
+ url.searchParams.set("code", code);
3149
+ if (state) url.searchParams.set("state", state);
3150
+ return c.redirect(url.toString(), 302);
3151
+ });
3152
+ app.post("/oauth2/token", async (c) => {
3153
+ const contentType = c.req.header("Content-Type") ?? "";
3154
+ const rawText = await c.req.text();
3155
+ let body;
3156
+ if (contentType.includes("application/json")) {
3157
+ try {
3158
+ body = JSON.parse(rawText);
3159
+ } catch {
3160
+ body = {};
3161
+ }
3162
+ } else {
3163
+ body = Object.fromEntries(new URLSearchParams(rawText));
3164
+ }
3165
+ const code = typeof body.code === "string" ? body.code : "";
3166
+ const redirect_uri = typeof body.redirect_uri === "string" ? body.redirect_uri : "";
3167
+ const grant_type = typeof body.grant_type === "string" ? body.grant_type : "";
3168
+ const code_verifier = typeof body.code_verifier === "string" ? body.code_verifier : void 0;
3169
+ const bodyClientId = typeof body.client_id === "string" ? body.client_id : "";
3170
+ const bodyClientSecret = typeof body.client_secret === "string" ? body.client_secret : "";
3171
+ const clientsConfigured = gs.oauthClients.all().length > 0;
3172
+ if (clientsConfigured) {
3173
+ const client = gs.oauthClients.findOneBy("client_id", bodyClientId);
3174
+ if (!client) {
3175
+ return c.json({ error: "invalid_client", error_description: "The client_id is incorrect." }, 401);
3176
+ }
3177
+ if (!constantTimeSecretEqual(bodyClientSecret, client.client_secret)) {
3178
+ return c.json({ error: "invalid_client", error_description: "The client_secret is incorrect." }, 401);
3179
+ }
3180
+ }
3181
+ if (grant_type === "refresh_token") {
3182
+ const refreshToken2 = typeof body.refresh_token === "string" ? body.refresh_token : "";
3183
+ const record = getRefreshTokens(store).get(refreshToken2);
3184
+ if (!record) {
3185
+ return c.json({ error: "invalid_grant", error_description: "The refresh token is invalid." }, 400);
3186
+ }
3187
+ if (clientsConfigured && record.clientId !== bodyClientId) {
3188
+ return c.json({ error: "invalid_grant", error_description: "The refresh token is invalid." }, 400);
3189
+ }
3190
+ const user2 = gs.users.findOneBy("email", record.email);
3191
+ if (!user2) {
3192
+ return c.json({ error: "invalid_grant", error_description: "User not found." }, 400);
3193
+ }
3194
+ const accessToken2 = "google_" + randomBytes2(20).toString("base64url");
3195
+ const scopes2 = record.scope ? record.scope.split(/\s+/).filter(Boolean) : [];
3196
+ if (tokenMap) {
3197
+ tokenMap.set(accessToken2, { login: user2.email, id: user2.id, scopes: scopes2 });
3198
+ }
3199
+ return c.json({
3200
+ access_token: accessToken2,
3201
+ token_type: "Bearer",
3202
+ expires_in: 3600,
3203
+ scope: record.scope || "openid email profile"
3204
+ });
3205
+ }
3206
+ if (grant_type !== "authorization_code") {
3207
+ return c.json(
3208
+ {
3209
+ error: "unsupported_grant_type",
3210
+ error_description: "Only authorization_code and refresh_token are supported."
3211
+ },
3212
+ 400
3213
+ );
3214
+ }
3215
+ const pendingMap = getPendingCodes(store);
3216
+ const pending = pendingMap.get(code);
3217
+ if (!pending) {
3218
+ return c.json({ error: "invalid_grant", error_description: "The code is incorrect or expired." }, 400);
3219
+ }
3220
+ if (isPendingCodeExpired(pending)) {
3221
+ pendingMap.delete(code);
3222
+ return c.json({ error: "invalid_grant", error_description: "The code is incorrect or expired." }, 400);
3223
+ }
3224
+ if (pending.codeChallenge != null) {
3225
+ if (code_verifier === void 0) {
3226
+ return c.json({ error: "invalid_grant", error_description: "PKCE verification failed." }, 400);
3227
+ }
3228
+ const method = (pending.codeChallengeMethod ?? "plain").toLowerCase();
3229
+ if (method === "s256") {
3230
+ const expected = createHash("sha256").update(code_verifier).digest("base64url");
3231
+ if (expected !== pending.codeChallenge) {
3232
+ return c.json({ error: "invalid_grant", error_description: "PKCE verification failed." }, 400);
3233
+ }
3234
+ } else if (method === "plain") {
3235
+ if (code_verifier !== pending.codeChallenge) {
3236
+ return c.json({ error: "invalid_grant", error_description: "PKCE verification failed." }, 400);
3237
+ }
3238
+ } else {
3239
+ return c.json({ error: "invalid_grant", error_description: "PKCE verification failed." }, 400);
3240
+ }
3241
+ }
3242
+ pendingMap.delete(code);
3243
+ const user = gs.users.findOneBy("email", pending.email);
3244
+ if (!user) {
3245
+ return c.json({ error: "invalid_grant", error_description: "User not found." }, 400);
3246
+ }
3247
+ const accessToken = "google_" + randomBytes2(20).toString("base64url");
3248
+ const refreshToken = "google_refresh_" + randomBytes2(24).toString("base64url");
3249
+ const scopes = pending.scope ? pending.scope.split(/\s+/).filter(Boolean) : [];
3250
+ if (tokenMap) {
3251
+ tokenMap.set(accessToken, { login: user.email, id: user.id, scopes });
3252
+ }
3253
+ getRefreshTokens(store).set(refreshToken, {
3254
+ email: user.email,
3255
+ scope: pending.scope,
3256
+ clientId: pending.clientId
3257
+ });
3258
+ const idToken = await createIdToken(user, pending.clientId, pending.nonce, baseUrl);
3259
+ debug("google.oauth", `[Google token] issued token for ${user.email}`);
3260
+ return c.json({
3261
+ access_token: accessToken,
3262
+ refresh_token: refreshToken,
3263
+ id_token: idToken,
3264
+ token_type: "Bearer",
3265
+ expires_in: 3600,
3266
+ scope: pending.scope || "openid email profile"
3267
+ });
3268
+ });
3269
+ app.get("/oauth2/v2/userinfo", (c) => {
3270
+ const authUser = c.get("authUser");
3271
+ if (!authUser) {
3272
+ return c.json({ error: "invalid_token", error_description: "Authentication required." }, 401);
3273
+ }
3274
+ const user = gs.users.findOneBy("email", authUser.login);
3275
+ if (!user) {
3276
+ return c.json({ error: "invalid_token", error_description: "User not found." }, 401);
3277
+ }
3278
+ return c.json({
3279
+ sub: user.uid,
3280
+ email: user.email,
3281
+ email_verified: user.email_verified,
3282
+ name: user.name,
3283
+ given_name: user.given_name,
3284
+ family_name: user.family_name,
3285
+ picture: user.picture,
3286
+ locale: user.locale,
3287
+ ...user.hd ? { hd: user.hd } : {}
3288
+ });
3289
+ });
3290
+ app.post("/oauth2/revoke", async (c) => {
3291
+ const contentType = c.req.header("Content-Type") ?? "";
3292
+ const rawText = await c.req.text();
3293
+ let token;
3294
+ if (contentType.includes("application/json")) {
3295
+ try {
3296
+ const parsed = JSON.parse(rawText);
3297
+ token = typeof parsed.token === "string" ? parsed.token : "";
3298
+ } catch {
3299
+ token = "";
3300
+ }
3301
+ } else {
3302
+ const params = new URLSearchParams(rawText);
3303
+ token = params.get("token") ?? "";
3304
+ }
3305
+ if (token && tokenMap) {
3306
+ tokenMap.delete(token);
3307
+ }
3308
+ if (token) {
3309
+ getRefreshTokens(store).delete(token);
3310
+ }
3311
+ return c.body(null, 200);
3312
+ });
3313
+ }
3314
+ function settingsRoutes({ app, store }) {
3315
+ const gs = getGoogleStore(store);
3316
+ app.get("/gmail/v1/users/:userId/settings/filters", (c) => {
3317
+ const authEmail = requireGmailUser(c);
3318
+ if (authEmail instanceof Response) return authEmail;
3319
+ return c.json({
3320
+ filter: listFiltersForUser(gs, authEmail).map((filter) => formatFilterResource(filter))
3321
+ });
3322
+ });
3323
+ app.post("/gmail/v1/users/:userId/settings/filters", async (c) => {
3324
+ const authEmail = requireGmailUser(c);
3325
+ if (authEmail instanceof Response) return authEmail;
3326
+ const body = await parseGoogleBody(c);
3327
+ const criteria = getRecord(body, "criteria") ?? {};
3328
+ const action = getRecord(body, "action") ?? {};
3329
+ const criteriaFrom = getString(criteria, "from") ?? null;
3330
+ const addLabelIds = getStringArray(action, "addLabelIds");
3331
+ const removeLabelIds = getStringArray(action, "removeLabelIds");
3332
+ if (addLabelIds.length === 0 && removeLabelIds.length === 0) {
3333
+ return googleApiError(c, 400, "Filter actions are required.", "invalidArgument", "INVALID_ARGUMENT");
3334
+ }
3335
+ const missingLabelIds = findMissingLabelIds(gs, authEmail, [...addLabelIds, ...removeLabelIds]);
3336
+ if (missingLabelIds.length > 0) {
3337
+ return googleApiError(
3338
+ c,
3339
+ 400,
3340
+ `Invalid label IDs: ${missingLabelIds.join(", ")}`,
3341
+ "invalidArgument",
3342
+ "INVALID_ARGUMENT"
3343
+ );
3344
+ }
3345
+ if (findMatchingFilter(gs, {
3346
+ user_email: authEmail,
3347
+ criteria_from: criteriaFrom,
3348
+ add_label_ids: addLabelIds,
3349
+ remove_label_ids: removeLabelIds
3350
+ })) {
3351
+ return googleApiError(c, 400, "Filter already exists", "failedPrecondition", "FAILED_PRECONDITION");
3352
+ }
3353
+ const filter = createFilterRecord(gs, {
3354
+ user_email: authEmail,
3355
+ criteria_from: criteriaFrom,
3356
+ add_label_ids: addLabelIds,
3357
+ remove_label_ids: removeLabelIds
3358
+ });
3359
+ return c.json(formatFilterResource(filter));
3360
+ });
3361
+ app.delete("/gmail/v1/users/:userId/settings/filters/:id", (c) => {
3362
+ const authEmail = requireGmailUser(c);
3363
+ if (authEmail instanceof Response) return authEmail;
3364
+ const filter = getFilterById(gs, authEmail, c.req.param("id"));
3365
+ if (!filter) {
3366
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
3367
+ }
3368
+ gs.filters.delete(filter.id);
3369
+ return c.body(null, 204);
3370
+ });
3371
+ app.get("/gmail/v1/users/:userId/settings/forwardingAddresses", (c) => {
3372
+ const authEmail = requireGmailUser(c);
3373
+ if (authEmail instanceof Response) return authEmail;
3374
+ return c.json({
3375
+ forwardingAddresses: listForwardingAddressesForUser(gs, authEmail).map(
3376
+ (entry) => formatForwardingAddressResource(entry)
3377
+ )
3378
+ });
3379
+ });
3380
+ app.get("/gmail/v1/users/:userId/settings/sendAs", (c) => {
3381
+ const authEmail = requireGmailUser(c);
3382
+ if (authEmail instanceof Response) return authEmail;
3383
+ return c.json({
3384
+ sendAs: listSendAsForUser(gs, authEmail).map((entry) => formatSendAsResource(entry))
3385
+ });
3386
+ });
3387
+ }
3388
+ function threadRoutes({ app, store }) {
3389
+ const gs = getGoogleStore(store);
3390
+ app.get("/gmail/v1/users/:userId/threads", (c) => {
3391
+ const authEmail = requireGmailUser(c);
3392
+ if (authEmail instanceof Response) return authEmail;
3393
+ const url = new URL(c.req.url);
3394
+ const threads = groupThreads(
3395
+ listMessagesForUser(gs, authEmail, {
3396
+ labelIds: url.searchParams.getAll("labelIds"),
3397
+ query: url.searchParams.get("q")?.trim() ?? void 0,
3398
+ includeSpamTrash: parseBooleanParam(url.searchParams.get("includeSpamTrash"))
3399
+ })
3400
+ );
3401
+ const offset = parseOffset(url.searchParams.get("pageToken"));
3402
+ const limit = normalizeLimit(url.searchParams.get("maxResults"), 100, 500);
3403
+ const page = threads.slice(offset, offset + limit);
3404
+ const nextPageToken = offset + limit < threads.length ? String(offset + limit) : void 0;
3405
+ return c.json({
3406
+ threads: page.map((thread) => ({
3407
+ id: thread.id,
3408
+ snippet: thread.snippet,
3409
+ historyId: thread.historyId
3410
+ })),
3411
+ nextPageToken,
3412
+ resultSizeEstimate: threads.length
3413
+ });
3414
+ });
3415
+ app.get("/gmail/v1/users/:userId/threads/:id", (c) => {
3416
+ const authEmail = requireGmailUser(c);
3417
+ if (authEmail instanceof Response) return authEmail;
3418
+ const url = new URL(c.req.url);
3419
+ const messages = getThreadMessages(gs, authEmail, c.req.param("id"), {
3420
+ includeSpamTrash: parseBooleanParam(url.searchParams.get("includeSpamTrash"))
3421
+ });
3422
+ if (messages.length === 0) {
3423
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
3424
+ }
3425
+ return c.json(
3426
+ formatThreadResource(
3427
+ gs,
3428
+ messages,
3429
+ parseFormat(url.searchParams.get("format")),
3430
+ url.searchParams.getAll("metadataHeaders")
3431
+ )
3432
+ );
3433
+ });
3434
+ app.post("/gmail/v1/users/:userId/threads/:id/modify", async (c) => {
3435
+ const authEmail = requireGmailUser(c);
3436
+ if (authEmail instanceof Response) return authEmail;
3437
+ const messages = getThreadMessages(gs, authEmail, c.req.param("id"), { includeSpamTrash: true });
3438
+ if (messages.length === 0) {
3439
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
3440
+ }
3441
+ const body = await parseGoogleBody(c);
3442
+ const addLabelIds = getStringArray(body, "addLabelIds");
3443
+ const removeLabelIds = getStringArray(body, "removeLabelIds");
3444
+ const missingLabelIds = findMissingLabelIds(gs, authEmail, [...addLabelIds, ...removeLabelIds]);
3445
+ if (missingLabelIds.length > 0) {
3446
+ return googleApiError(
3447
+ c,
3448
+ 400,
3449
+ `Invalid label IDs: ${missingLabelIds.join(", ")}`,
3450
+ "invalidArgument",
3451
+ "INVALID_ARGUMENT"
3452
+ );
3453
+ }
3454
+ const updated = messages.map(
3455
+ (message) => markMessageModified(gs, message, applyLabelMutation(message.label_ids, addLabelIds, removeLabelIds))
3456
+ );
3457
+ return c.json(formatThreadResource(gs, updated, "full"));
3458
+ });
3459
+ app.post("/gmail/v1/users/:userId/threads/:id/trash", (c) => {
3460
+ const authEmail = requireGmailUser(c);
3461
+ if (authEmail instanceof Response) return authEmail;
3462
+ const messages = getThreadMessages(gs, authEmail, c.req.param("id"), { includeSpamTrash: true });
3463
+ if (messages.length === 0) {
3464
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
3465
+ }
3466
+ const updated = messages.map((message) => markMessageModified(gs, message, trashLabelIds(message.label_ids)));
3467
+ return c.json(formatThreadResource(gs, updated, "full"));
3468
+ });
3469
+ app.post("/gmail/v1/users/:userId/threads/:id/untrash", (c) => {
3470
+ const authEmail = requireGmailUser(c);
3471
+ if (authEmail instanceof Response) return authEmail;
3472
+ const messages = getThreadMessages(gs, authEmail, c.req.param("id"), { includeSpamTrash: true });
3473
+ if (messages.length === 0) {
3474
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
3475
+ }
3476
+ const updated = messages.map((message) => markMessageModified(gs, message, untrashLabelIds(message.label_ids)));
3477
+ return c.json(formatThreadResource(gs, updated, "full"));
3478
+ });
3479
+ app.delete("/gmail/v1/users/:userId/threads/:id", (c) => {
3480
+ const authEmail = requireGmailUser(c);
3481
+ if (authEmail instanceof Response) return authEmail;
3482
+ const messages = getThreadMessages(gs, authEmail, c.req.param("id"), { includeSpamTrash: true });
3483
+ if (messages.length === 0) {
3484
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
3485
+ }
3486
+ for (const message of messages) {
3487
+ deleteMessage(gs, message);
3488
+ }
3489
+ return c.body(null, 204);
3490
+ });
3491
+ }
3492
+ var manifest = {
3493
+ id: "google",
3494
+ name: "Google",
3495
+ description: "Stateful Google OAuth, OpenID Connect, Gmail, Calendar, and Drive emulator.",
3496
+ docsUrl: "https://docs.emulators.dev/google",
3497
+ surfaces: [
3498
+ { id: "rest", kind: "rest", title: "REST API", status: "partial", basePath: "/" },
3499
+ { id: "oauth", kind: "oauth", title: "OAuth 2.0 flow", status: "supported", basePath: "/o/oauth2/v2" },
3500
+ { id: "oidc", kind: "oidc", title: "OpenID Connect", status: "supported", basePath: "/.well-known" }
3501
+ ],
3502
+ auth: [
3503
+ { id: "oauth-code", title: "OAuth authorization code", type: "oauth-authorization-code", status: "supported" },
3504
+ { id: "oidc", title: "OIDC identity tokens", type: "oidc", status: "supported" },
3505
+ { id: "bearer", title: "Bearer access token", type: "bearer-token", status: "supported" }
3506
+ ],
3507
+ specs: [
3508
+ {
3509
+ kind: "oauth-metadata",
3510
+ title: "OAuth and OIDC metadata",
3511
+ coverage: "hand-authored",
3512
+ operations: [
3513
+ {
3514
+ operationId: "openid-configuration",
3515
+ method: "GET",
3516
+ path: "/.well-known/openid-configuration",
3517
+ status: "hand-authored"
3518
+ },
3519
+ { operationId: "jwks", method: "GET", path: "/oauth2/v3/certs", status: "partial" },
3520
+ { operationId: "authorize", method: "GET", path: "/o/oauth2/v2/auth", status: "hand-authored" },
3521
+ { operationId: "token", method: "POST", path: "/oauth2/token", status: "hand-authored" },
3522
+ { operationId: "userinfo", method: "GET", path: "/oauth2/v2/userinfo", status: "hand-authored" },
3523
+ { operationId: "revoke", method: "POST", path: "/oauth2/revoke", status: "hand-authored" }
3524
+ ]
3525
+ },
3526
+ {
3527
+ kind: "google-discovery",
3528
+ title: "Gmail API subset",
3529
+ coverage: "hand-authored",
3530
+ operations: [
3531
+ {
3532
+ operationId: "gmail.users.messages.list",
3533
+ method: "GET",
3534
+ path: "/gmail/v1/users/:userId/messages",
3535
+ status: "hand-authored"
3536
+ },
3537
+ {
3538
+ operationId: "gmail.users.messages.get",
3539
+ method: "GET",
3540
+ path: "/gmail/v1/users/:userId/messages/:id",
3541
+ status: "hand-authored"
3542
+ },
3543
+ {
3544
+ operationId: "gmail.users.messages.send",
3545
+ method: "POST",
3546
+ path: "/gmail/v1/users/:userId/messages/send",
3547
+ status: "hand-authored"
3548
+ },
3549
+ {
3550
+ operationId: "gmail.users.messages.modify",
3551
+ method: "POST",
3552
+ path: "/gmail/v1/users/:userId/messages/:id/modify",
3553
+ status: "hand-authored"
3554
+ },
3555
+ {
3556
+ operationId: "gmail.users.messages.trash",
3557
+ method: "POST",
3558
+ path: "/gmail/v1/users/:userId/messages/:id/trash",
3559
+ status: "hand-authored"
3560
+ },
3561
+ {
3562
+ operationId: "gmail.users.messages.delete",
3563
+ method: "DELETE",
3564
+ path: "/gmail/v1/users/:userId/messages/:id",
3565
+ status: "hand-authored"
3566
+ },
3567
+ {
3568
+ operationId: "gmail.users.messages.batchModify",
3569
+ method: "POST",
3570
+ path: "/gmail/v1/users/:userId/messages/batchModify",
3571
+ status: "hand-authored"
3572
+ },
3573
+ {
3574
+ operationId: "gmail.users.threads.list",
3575
+ method: "GET",
3576
+ path: "/gmail/v1/users/:userId/threads",
3577
+ status: "hand-authored"
3578
+ },
3579
+ {
3580
+ operationId: "gmail.users.threads.get",
3581
+ method: "GET",
3582
+ path: "/gmail/v1/users/:userId/threads/:id",
3583
+ status: "hand-authored"
3584
+ },
3585
+ {
3586
+ operationId: "gmail.users.threads.modify",
3587
+ method: "POST",
3588
+ path: "/gmail/v1/users/:userId/threads/:id/modify",
3589
+ status: "hand-authored"
3590
+ },
3591
+ {
3592
+ operationId: "gmail.users.drafts.list",
3593
+ method: "GET",
3594
+ path: "/gmail/v1/users/:userId/drafts",
3595
+ status: "hand-authored"
3596
+ },
3597
+ {
3598
+ operationId: "gmail.users.drafts.create",
3599
+ method: "POST",
3600
+ path: "/gmail/v1/users/:userId/drafts",
3601
+ status: "hand-authored"
3602
+ },
3603
+ {
3604
+ operationId: "gmail.users.drafts.send",
3605
+ method: "POST",
3606
+ path: "/gmail/v1/users/:userId/drafts/send",
3607
+ status: "hand-authored"
3608
+ },
3609
+ {
3610
+ operationId: "gmail.users.labels.list",
3611
+ method: "GET",
3612
+ path: "/gmail/v1/users/:userId/labels",
3613
+ status: "hand-authored"
3614
+ },
3615
+ {
3616
+ operationId: "gmail.users.labels.create",
3617
+ method: "POST",
3618
+ path: "/gmail/v1/users/:userId/labels",
3619
+ status: "hand-authored"
3620
+ },
3621
+ {
3622
+ operationId: "gmail.users.history.list",
3623
+ method: "GET",
3624
+ path: "/gmail/v1/users/:userId/history",
3625
+ status: "hand-authored"
3626
+ },
3627
+ {
3628
+ operationId: "gmail.users.settings.filters.list",
3629
+ method: "GET",
3630
+ path: "/gmail/v1/users/:userId/settings/filters",
3631
+ status: "hand-authored"
3632
+ },
3633
+ {
3634
+ operationId: "gmail.users.watch",
3635
+ method: "POST",
3636
+ path: "/gmail/v1/users/:userId/watch",
3637
+ status: "partial"
3638
+ }
3639
+ ]
3640
+ },
3641
+ {
3642
+ kind: "google-discovery",
3643
+ title: "Calendar API subset",
3644
+ coverage: "hand-authored",
3645
+ operations: [
3646
+ {
3647
+ operationId: "calendar.calendarList.list",
3648
+ method: "GET",
3649
+ path: "/calendar/v3/users/:userId/calendarList",
3650
+ status: "hand-authored"
3651
+ },
3652
+ {
3653
+ operationId: "calendar.events.list",
3654
+ method: "GET",
3655
+ path: "/calendar/v3/calendars/:calendarId/events",
3656
+ status: "hand-authored"
3657
+ },
3658
+ {
3659
+ operationId: "calendar.events.insert",
3660
+ method: "POST",
3661
+ path: "/calendar/v3/calendars/:calendarId/events",
3662
+ status: "hand-authored"
3663
+ },
3664
+ {
3665
+ operationId: "calendar.events.delete",
3666
+ method: "DELETE",
3667
+ path: "/calendar/v3/calendars/:calendarId/events/:eventId",
3668
+ status: "hand-authored"
3669
+ },
3670
+ {
3671
+ operationId: "calendar.freebusy.query",
3672
+ method: "POST",
3673
+ path: "/calendar/v3/freeBusy",
3674
+ status: "hand-authored"
3675
+ }
3676
+ ]
3677
+ },
3678
+ {
3679
+ kind: "google-discovery",
3680
+ title: "Drive API subset",
3681
+ coverage: "hand-authored",
3682
+ operations: [
3683
+ { operationId: "drive.files.list", method: "GET", path: "/drive/v3/files", status: "hand-authored" },
3684
+ { operationId: "drive.files.create", method: "POST", path: "/drive/v3/files", status: "hand-authored" },
3685
+ { operationId: "drive.files.get", method: "GET", path: "/drive/v3/files/:fileId", status: "hand-authored" },
3686
+ {
3687
+ operationId: "drive.files.update",
3688
+ method: "PATCH",
3689
+ path: "/drive/v3/files/:fileId",
3690
+ status: "hand-authored"
3691
+ }
3692
+ ]
3693
+ },
3694
+ {
3695
+ kind: "manual",
3696
+ title: "Google API Discovery passthrough",
3697
+ coverage: "partial",
3698
+ notes: "Proxies Google's real discovery documents and rewrites base URLs to the emulator instance.",
3699
+ operations: [
3700
+ {
3701
+ operationId: "discovery.apis.getRest",
3702
+ method: "GET",
3703
+ path: "/discovery/v1/apis/:api/:version/rest",
3704
+ status: "partial"
3705
+ }
3706
+ ]
3707
+ }
3708
+ ],
3709
+ seedSchema: {
3710
+ description: "Seed Google users, OAuth clients, Gmail labels and messages, calendars, events, and Drive items.",
3711
+ fields: [
3712
+ {
3713
+ key: "users",
3714
+ title: "Users",
3715
+ description: "Google accounts addressable by email; used for sign-in and userinfo.",
3716
+ example: [{ email: "testuser@example.com", name: "Test User", email_verified: true }]
3717
+ },
3718
+ {
3719
+ key: "oauth_clients",
3720
+ title: "OAuth clients",
3721
+ description: "Registered OAuth applications with client credentials and redirect URIs.",
3722
+ example: [
3723
+ {
3724
+ client_id: "example-client-id.apps.googleusercontent.com",
3725
+ client_secret: "GOCSPX-example_secret",
3726
+ name: "Code App (Google)",
3727
+ redirect_uris: ["http://localhost:3000/api/auth/callback/google"]
3728
+ }
3729
+ ]
3730
+ },
3731
+ {
3732
+ key: "labels",
3733
+ title: "Gmail labels",
3734
+ example: [{ id: "Label_ops", user_email: "testuser@example.com", name: "Ops/Review" }]
3735
+ },
3736
+ {
3737
+ key: "messages",
3738
+ title: "Gmail messages",
3739
+ example: [
3740
+ {
3741
+ id: "msg_welcome",
3742
+ user_email: "testuser@example.com",
3743
+ from: "welcome@example.com",
3744
+ to: "testuser@example.com",
3745
+ subject: "Welcome to the Gmail emulator",
3746
+ label_ids: ["INBOX", "UNREAD"]
3747
+ }
3748
+ ]
3749
+ },
3750
+ {
3751
+ key: "calendars",
3752
+ title: "Calendars",
3753
+ example: [
3754
+ { id: "primary", user_email: "testuser@example.com", summary: "testuser@example.com", primary: true }
3755
+ ]
3756
+ },
3757
+ {
3758
+ key: "calendar_events",
3759
+ title: "Calendar events",
3760
+ example: [
3761
+ {
3762
+ id: "evt_kickoff",
3763
+ user_email: "testuser@example.com",
3764
+ calendar_id: "primary",
3765
+ summary: "Project Kickoff",
3766
+ start_date_time: "2025-01-10T09:00:00.000Z",
3767
+ end_date_time: "2025-01-10T09:30:00.000Z"
3768
+ }
3769
+ ]
3770
+ },
3771
+ {
3772
+ key: "drive_items",
3773
+ title: "Drive items",
3774
+ example: [
3775
+ {
3776
+ id: "drv_docs",
3777
+ user_email: "testuser@example.com",
3778
+ name: "Docs",
3779
+ mime_type: "application/vnd.google-apps.folder",
3780
+ parent_ids: ["root"]
3781
+ }
3782
+ ]
3783
+ }
3784
+ ],
3785
+ example: {
3786
+ users: [
3787
+ {
3788
+ email: "testuser@example.com",
3789
+ name: "Test User",
3790
+ picture: "https://lh3.googleusercontent.com/a/default-user",
3791
+ email_verified: true
3792
+ }
3793
+ ],
3794
+ oauth_clients: [
3795
+ {
3796
+ client_id: "example-client-id.apps.googleusercontent.com",
3797
+ client_secret: "GOCSPX-example_secret",
3798
+ name: "Code App (Google)",
3799
+ redirect_uris: ["http://localhost:3000/api/auth/callback/google"]
3800
+ }
3801
+ ],
3802
+ messages: [
3803
+ {
3804
+ id: "msg_welcome",
3805
+ user_email: "testuser@example.com",
3806
+ from: "welcome@example.com",
3807
+ to: "testuser@example.com",
3808
+ subject: "Welcome to the Gmail emulator",
3809
+ body_text: "You can now test Gmail, Calendar, and Drive flows locally.",
3810
+ label_ids: ["INBOX", "UNREAD", "CATEGORY_UPDATES"]
3811
+ }
3812
+ ],
3813
+ calendars: [
3814
+ { id: "primary", user_email: "testuser@example.com", summary: "testuser@example.com", primary: true }
3815
+ ],
3816
+ calendar_events: [
3817
+ {
3818
+ id: "evt_kickoff",
3819
+ user_email: "testuser@example.com",
3820
+ calendar_id: "primary",
3821
+ summary: "Project Kickoff",
3822
+ start_date_time: "2025-01-10T09:00:00.000Z",
3823
+ end_date_time: "2025-01-10T09:30:00.000Z"
3824
+ }
3825
+ ],
3826
+ drive_items: [
3827
+ {
3828
+ id: "drv_docs",
3829
+ user_email: "testuser@example.com",
3830
+ name: "Docs",
3831
+ mime_type: "application/vnd.google-apps.folder",
3832
+ parent_ids: ["root"]
3833
+ }
3834
+ ]
3835
+ }
3836
+ },
3837
+ stateModel: {
3838
+ description: "Entities mutated by Google OAuth, Gmail, Calendar, and Drive provider calls.",
3839
+ collections: [
3840
+ { name: "google.users" },
3841
+ { name: "google.oauth_clients" },
3842
+ { name: "google.messages" },
3843
+ { name: "google.drafts" },
3844
+ { name: "google.attachments" },
3845
+ { name: "google.history" },
3846
+ { name: "google.labels" },
3847
+ { name: "google.filters" },
3848
+ { name: "google.forwarding_addresses" },
3849
+ { name: "google.send_as" },
3850
+ { name: "google.calendars" },
3851
+ { name: "google.calendar_events" },
3852
+ { name: "google.drive_items" }
3853
+ ]
3854
+ },
3855
+ connections: [
3856
+ {
3857
+ id: "googleapis",
3858
+ title: "googleapis (TypeScript)",
3859
+ kind: "sdk",
3860
+ language: "typescript",
3861
+ description: "Point the official googleapis client at the emulator via per-service rootUrl options.",
3862
+ template: 'import { google } from "googleapis";\n\nconst auth = new google.auth.OAuth2({\n clientId: "{{clientId}}",\n clientSecret: "{{clientSecret}}",\n});\nauth.setCredentials({ access_token: "{{token}}" });\n\nconst gmail = google.gmail({\n version: "v1",\n auth,\n rootUrl: "{{baseUrl}}/",\n});\n\nconst { data } = await gmail.users.messages.list({ userId: "me" });'
3863
+ },
3864
+ {
3865
+ id: "oauth-discovery",
3866
+ title: "OpenID discovery (fetch)",
3867
+ kind: "sdk",
3868
+ language: "typescript",
3869
+ description: "Discover the OIDC endpoints, then drive the authorization code flow against the emulator.",
3870
+ template: 'const discovery = await fetch(\n "{{baseUrl}}/.well-known/openid-configuration",\n).then((r) => r.json());\n\n// discovery.authorization_endpoint, discovery.token_endpoint, etc.\nconst userinfo = await fetch(discovery.userinfo_endpoint, {\n headers: { authorization: "Bearer {{token}}" },\n}).then((r) => r.json());'
3871
+ },
3872
+ {
3873
+ id: "google-env",
3874
+ title: "Google API base URL (env)",
3875
+ kind: "env",
3876
+ language: "bash",
3877
+ description: "Point your SDK or app at the emulator instead of Google's production endpoints.",
3878
+ template: "GOOGLE_BASE_URL={{baseUrl}}\nGOOGLE_CLIENT_ID={{clientId}}\nGOOGLE_CLIENT_SECRET={{clientSecret}}\nGOOGLE_ACCESS_TOKEN={{token}}"
3879
+ },
3880
+ {
3881
+ id: "curl",
3882
+ title: "curl",
3883
+ kind: "curl",
3884
+ language: "bash",
3885
+ description: "Call the Gmail REST API directly with a bearer access token.",
3886
+ template: 'curl -s {{baseUrl}}/gmail/v1/users/me/messages -H "authorization: Bearer {{token}}"'
3887
+ }
3888
+ ]
3889
+ };
3890
+ function seedDefaults(store, _baseUrl) {
3891
+ const gs = getGoogleStore(store);
3892
+ const defaultEmail = "testuser@gmail.com";
3893
+ if (!gs.users.findOneBy("email", defaultEmail)) {
3894
+ gs.users.insert({
3895
+ uid: generateUid("goog"),
3896
+ email: defaultEmail,
3897
+ name: "Test User",
3898
+ given_name: "Test",
3899
+ family_name: "User",
3900
+ picture: null,
3901
+ email_verified: true,
3902
+ locale: "en",
3903
+ hd: null
3904
+ });
3905
+ }
3906
+ ensureSystemLabels(gs, defaultEmail);
3907
+ seedCalendars(
3908
+ store,
3909
+ [
3910
+ {
3911
+ id: "primary",
3912
+ user_email: defaultEmail,
3913
+ summary: defaultEmail,
3914
+ primary: true,
3915
+ selected: true,
3916
+ time_zone: "UTC"
3917
+ },
3918
+ {
3919
+ id: "cal_team",
3920
+ user_email: defaultEmail,
3921
+ summary: "Team Calendar",
3922
+ description: "Shared team events",
3923
+ selected: true,
3924
+ time_zone: "UTC"
3925
+ }
3926
+ ],
3927
+ defaultEmail
3928
+ );
3929
+ seedCalendarEvents(
3930
+ store,
3931
+ [
3932
+ {
3933
+ id: "evt_standup",
3934
+ user_email: defaultEmail,
3935
+ calendar_id: "primary",
3936
+ summary: "Daily Standup",
3937
+ description: "Team sync",
3938
+ start_date_time: new Date(Date.now() + 60 * 60 * 1e3).toISOString(),
3939
+ end_date_time: new Date(Date.now() + 90 * 60 * 1e3).toISOString(),
3940
+ attendees: [
3941
+ { email: defaultEmail, display_name: "Test User" },
3942
+ { email: "teammate@example.com", display_name: "Teammate" }
3943
+ ],
3944
+ conference_entry_points: [
3945
+ {
3946
+ entry_point_type: "video",
3947
+ uri: "https://meet.google.com/emulate-standup",
3948
+ label: "Google Meet"
3949
+ }
3950
+ ],
3951
+ hangout_link: "https://meet.google.com/emulate-standup"
3952
+ }
3953
+ ],
3954
+ defaultEmail
3955
+ );
3956
+ seedDriveItems(
3957
+ store,
3958
+ [
3959
+ {
3960
+ id: "drv_root_receipts",
3961
+ user_email: defaultEmail,
3962
+ name: "Receipts",
3963
+ mime_type: "application/vnd.google-apps.folder",
3964
+ parent_ids: ["root"]
3965
+ },
3966
+ {
3967
+ id: "drv_receipt_pdf",
3968
+ user_email: defaultEmail,
3969
+ name: "March Receipt.pdf",
3970
+ mime_type: "application/pdf",
3971
+ parent_ids: ["drv_root_receipts"],
3972
+ data: "receipt-pdf-data"
3973
+ }
3974
+ ],
3975
+ defaultEmail
3976
+ );
3977
+ seedMessages(
3978
+ store,
3979
+ [
3980
+ {
3981
+ id: "msg_welcome",
3982
+ thread_id: "thr_welcome",
3983
+ user_email: defaultEmail,
3984
+ from: "Welcome Team <welcome@example.com>",
3985
+ to: defaultEmail,
3986
+ subject: "Welcome to your local Gmail emulator",
3987
+ snippet: "Your OAuth flow is set up and Gmail message, thread, and label APIs are ready.",
3988
+ body_text: "Your OAuth flow is set up and Gmail message, thread, and label APIs are ready.\n\nUse this inbox to test Gmail automations locally.",
3989
+ label_ids: ["INBOX", "UNREAD", "CATEGORY_UPDATES"],
3990
+ date: new Date(Date.now() - 60 * 60 * 1e3).toISOString()
3991
+ },
3992
+ {
3993
+ id: "msg_build",
3994
+ thread_id: "thr_build",
3995
+ user_email: defaultEmail,
3996
+ from: "Build Bot <builds@example.com>",
3997
+ to: defaultEmail,
3998
+ subject: "Nightly build finished successfully",
3999
+ snippet: "The latest build completed successfully in 6 minutes.",
4000
+ body_text: "The latest build completed successfully in 6 minutes.\n\nArtifact upload finished and smoke checks passed.",
4001
+ label_ids: ["INBOX", "CATEGORY_UPDATES"],
4002
+ date: new Date(Date.now() - 2 * 60 * 60 * 1e3).toISOString()
4003
+ },
4004
+ {
4005
+ id: "msg_build_reply",
4006
+ thread_id: "thr_build",
4007
+ user_email: defaultEmail,
4008
+ from: defaultEmail,
4009
+ to: "Build Bot <builds@example.com>",
4010
+ subject: "Re: Nightly build finished successfully",
4011
+ snippet: "Thanks, I will review the artifact after lunch.",
4012
+ body_text: "Thanks, I will review the artifact after lunch.",
4013
+ label_ids: ["SENT"],
4014
+ date: new Date(Date.now() - 90 * 60 * 1e3).toISOString(),
4015
+ in_reply_to: "<msg_build@emulate.google.local>",
4016
+ references: "<msg_build@emulate.google.local>"
4017
+ },
4018
+ {
4019
+ id: "msg_draft",
4020
+ thread_id: "thr_draft",
4021
+ user_email: defaultEmail,
4022
+ from: defaultEmail,
4023
+ to: "someone@example.com",
4024
+ subject: "Draft follow-up",
4025
+ snippet: "Checking in on the open question from yesterday.",
4026
+ body_text: "Checking in on the open question from yesterday.",
4027
+ label_ids: ["DRAFT"],
4028
+ date: new Date(Date.now() - 30 * 60 * 1e3).toISOString()
4029
+ }
4030
+ ],
4031
+ defaultEmail
4032
+ );
4033
+ }
4034
+ var CONSUMER_EMAIL_DOMAINS = /* @__PURE__ */ new Set(["gmail.com", "googlemail.com"]);
4035
+ function deriveHd(email) {
4036
+ const domain = email.split("@")[1]?.toLowerCase();
4037
+ if (!domain) return null;
4038
+ if (CONSUMER_EMAIL_DOMAINS.has(domain)) return null;
4039
+ return domain;
4040
+ }
4041
+ function resolveHd(user) {
4042
+ if (user.hd !== void 0) return user.hd || null;
4043
+ return deriveHd(user.email);
4044
+ }
4045
+ function seedFromConfig(store, _baseUrl, config) {
4046
+ const gs = getGoogleStore(store);
4047
+ if (config.users) {
4048
+ for (const user of config.users) {
4049
+ const existing = gs.users.findOneBy("email", user.email);
4050
+ if (!existing) {
4051
+ const nameParts = (user.name ?? "").split(/\s+/).filter(Boolean);
4052
+ gs.users.insert({
4053
+ uid: generateUid("goog"),
4054
+ email: user.email,
4055
+ name: user.name ?? user.email.split("@")[0],
4056
+ given_name: user.given_name ?? nameParts[0] ?? "",
4057
+ family_name: user.family_name ?? nameParts.slice(1).join(" "),
4058
+ picture: user.picture ?? null,
4059
+ email_verified: user.email_verified ?? true,
4060
+ locale: user.locale ?? "en",
4061
+ hd: resolveHd(user)
4062
+ });
4063
+ }
4064
+ ensureSystemLabels(gs, user.email);
4065
+ }
4066
+ }
4067
+ if (config.oauth_clients) {
4068
+ for (const client of config.oauth_clients) {
4069
+ const existing = gs.oauthClients.findOneBy("client_id", client.client_id);
4070
+ if (existing) continue;
4071
+ gs.oauthClients.insert({
4072
+ client_id: client.client_id,
4073
+ client_secret: client.client_secret,
4074
+ name: client.name ?? "Code App (Google)",
4075
+ redirect_uris: client.redirect_uris
4076
+ });
4077
+ }
4078
+ }
4079
+ const fallbackEmail = config.users?.[0]?.email ?? gs.users.all()[0]?.email ?? "testuser@gmail.com";
4080
+ ensureSystemLabels(gs, fallbackEmail);
4081
+ if (config.labels) {
4082
+ seedLabels(store, config.labels, fallbackEmail);
4083
+ }
4084
+ if (config.messages) {
4085
+ seedMessages(store, config.messages, fallbackEmail);
4086
+ }
4087
+ if (config.calendars) {
4088
+ seedCalendars(store, config.calendars, fallbackEmail);
4089
+ }
4090
+ if (config.calendar_events) {
4091
+ seedCalendarEvents(store, config.calendar_events, fallbackEmail);
4092
+ }
4093
+ if (config.drive_items) {
4094
+ seedDriveItems(store, config.drive_items, fallbackEmail);
4095
+ }
4096
+ }
4097
+ function seedLabels(store, labels, fallbackEmail) {
4098
+ const gs = getGoogleStore(store);
4099
+ for (const label of labels) {
4100
+ const userEmail = label.user_email ?? fallbackEmail;
4101
+ ensureSystemLabels(gs, userEmail);
4102
+ const existing = (label.id ? findLabelById(gs, userEmail, label.id) : void 0) ?? findLabelByName(gs, userEmail, label.name);
4103
+ if (existing) continue;
4104
+ createLabelRecord(gs, {
4105
+ gmail_id: label.id,
4106
+ user_email: userEmail,
4107
+ name: label.name,
4108
+ type: label.type ?? "user",
4109
+ message_list_visibility: label.message_list_visibility ?? "show",
4110
+ label_list_visibility: label.label_list_visibility ?? "labelShow",
4111
+ color_background: label.color_background ?? null,
4112
+ color_text: label.color_text ?? null
4113
+ });
4114
+ }
4115
+ }
4116
+ function seedMessages(store, messages, fallbackEmail) {
4117
+ const gs = getGoogleStore(store);
4118
+ for (const message of messages) {
4119
+ const userEmail = message.user_email ?? fallbackEmail;
4120
+ ensureSystemLabels(gs, userEmail);
4121
+ if (message.id && gs.messages.findOneBy("gmail_id", message.id)) continue;
4122
+ createStoredMessage(
4123
+ gs,
4124
+ {
4125
+ gmail_id: message.id,
4126
+ thread_id: message.thread_id,
4127
+ user_email: userEmail,
4128
+ raw: message.raw ?? null,
4129
+ from: message.from,
4130
+ to: message.to,
4131
+ cc: message.cc ?? null,
4132
+ bcc: message.bcc ?? null,
4133
+ reply_to: message.reply_to ?? null,
4134
+ subject: message.subject,
4135
+ snippet: message.snippet,
4136
+ body_text: message.body_text ?? null,
4137
+ body_html: message.body_html ?? null,
4138
+ label_ids: message.label_ids ?? ["INBOX", "UNREAD"],
4139
+ date: message.date,
4140
+ internal_date: message.internal_date,
4141
+ message_id: message.message_id,
4142
+ references: message.references ?? null,
4143
+ in_reply_to: message.in_reply_to ?? null
4144
+ },
4145
+ {
4146
+ createMissingCustomLabels: true
4147
+ }
4148
+ );
4149
+ }
4150
+ }
4151
+ function seedCalendars(store, calendars, fallbackEmail) {
4152
+ const gs = getGoogleStore(store);
4153
+ for (const calendar of calendars) {
4154
+ const userEmail = calendar.user_email ?? fallbackEmail;
4155
+ createCalendarRecord(gs, {
4156
+ google_id: calendar.id,
4157
+ user_email: userEmail,
4158
+ summary: calendar.summary,
4159
+ description: calendar.description ?? null,
4160
+ time_zone: calendar.time_zone ?? "UTC",
4161
+ primary: calendar.primary ?? false,
4162
+ selected: calendar.selected ?? true,
4163
+ access_role: calendar.access_role ?? "owner"
4164
+ });
4165
+ }
4166
+ }
4167
+ function seedCalendarEvents(store, events, fallbackEmail) {
4168
+ const gs = getGoogleStore(store);
4169
+ for (const event of events) {
4170
+ const userEmail = event.user_email ?? fallbackEmail;
4171
+ createCalendarEventRecord(gs, {
4172
+ google_id: event.id,
4173
+ user_email: userEmail,
4174
+ calendar_google_id: event.calendar_id ?? "primary",
4175
+ status: event.status ?? "confirmed",
4176
+ summary: event.summary,
4177
+ description: event.description ?? null,
4178
+ location: event.location ?? null,
4179
+ start_date_time: event.start_date_time ?? null,
4180
+ start_date: event.start_date ?? null,
4181
+ end_date_time: event.end_date_time ?? null,
4182
+ end_date: event.end_date ?? null,
4183
+ attendees: (event.attendees ?? []).map((attendee) => ({
4184
+ email: attendee.email,
4185
+ display_name: attendee.display_name ?? null,
4186
+ response_status: null,
4187
+ organizer: false,
4188
+ self: attendee.email === userEmail
4189
+ })),
4190
+ conference_entry_points: (event.conference_entry_points ?? []).map((entry) => ({
4191
+ entry_point_type: entry.entry_point_type,
4192
+ uri: entry.uri,
4193
+ label: entry.label ?? null
4194
+ })),
4195
+ hangout_link: event.hangout_link ?? null
4196
+ });
4197
+ }
4198
+ }
4199
+ function seedDriveItems(store, items, fallbackEmail) {
4200
+ const gs = getGoogleStore(store);
4201
+ for (const item of items) {
4202
+ const userEmail = item.user_email ?? fallbackEmail;
4203
+ if (item.id && gs.driveItems.findOneBy("google_id", item.id)) continue;
4204
+ createDriveItemRecord(gs, {
4205
+ google_id: item.id,
4206
+ user_email: userEmail,
4207
+ name: item.name,
4208
+ mime_type: item.mime_type,
4209
+ parent_google_ids: item.parent_ids ?? ["root"],
4210
+ size: item.data ? Buffer.byteLength(item.data, "utf8") : null,
4211
+ data: item.data ? Buffer.from(item.data, "utf8").toString("base64url") : null
4212
+ });
4213
+ }
4214
+ }
4215
+ var googlePlugin = {
4216
+ name: "google",
4217
+ register(app, store, webhooks, baseUrl, tokenMap) {
4218
+ const ctx = { app, store, webhooks, baseUrl, tokenMap };
4219
+ oauthRoutes(ctx);
4220
+ calendarRoutes(ctx);
4221
+ driveRoutes(ctx);
4222
+ messageRoutes(ctx);
4223
+ draftRoutes(ctx);
4224
+ historyRoutes(ctx);
4225
+ threadRoutes(ctx);
4226
+ labelRoutes(ctx);
4227
+ settingsRoutes(ctx);
4228
+ },
4229
+ seed(store, baseUrl) {
4230
+ seedDefaults(store, baseUrl);
4231
+ }
4232
+ };
4233
+ var index_default = googlePlugin;
4234
+ export {
4235
+ index_default as default,
4236
+ getGoogleStore,
4237
+ googlePlugin,
4238
+ manifest,
4239
+ seedFromConfig
4240
+ };
4241
+ //# sourceMappingURL=dist-XVVIYXQG.js.map