emulate 0.2.0 → 0.4.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.
@@ -0,0 +1,3641 @@
1
+ import {
2
+ SignJWT
3
+ } from "./chunk-D6EKRYGP.js";
4
+ import "./chunk-TEPNEZ63.js";
5
+
6
+ // ../@emulators/google/dist/index.js
7
+ import { randomBytes } from "crypto";
8
+ import { createHash, randomBytes as randomBytes2 } from "crypto";
9
+ import { readFileSync } from "fs";
10
+ import { fileURLToPath } from "url";
11
+ import { dirname, join } from "path";
12
+ import { timingSafeEqual } from "crypto";
13
+ var HISTORY_CHANGE_TYPES = /* @__PURE__ */ new Set([
14
+ "messageAdded",
15
+ "messageDeleted",
16
+ "labelAdded",
17
+ "labelRemoved"
18
+ ]);
19
+ function isHistoryChangeType(value) {
20
+ return HISTORY_CHANGE_TYPES.has(value);
21
+ }
22
+ var SYSTEM_LABELS = [
23
+ { gmail_id: "INBOX", name: "INBOX", message_list_visibility: "show", label_list_visibility: "labelShow" },
24
+ { gmail_id: "SENT", name: "SENT", message_list_visibility: "show", label_list_visibility: "labelShow" },
25
+ { gmail_id: "UNREAD", name: "UNREAD", message_list_visibility: "show", label_list_visibility: "labelShow" },
26
+ { gmail_id: "STARRED", name: "STARRED", message_list_visibility: "show", label_list_visibility: "labelShow" },
27
+ { gmail_id: "IMPORTANT", name: "IMPORTANT", message_list_visibility: "show", label_list_visibility: "labelShow" },
28
+ { gmail_id: "TRASH", name: "TRASH", message_list_visibility: "show", label_list_visibility: "labelShow" },
29
+ { gmail_id: "SPAM", name: "SPAM", message_list_visibility: "show", label_list_visibility: "labelShow" },
30
+ { gmail_id: "DRAFT", name: "DRAFT", message_list_visibility: "hide", label_list_visibility: "labelHide" },
31
+ {
32
+ gmail_id: "CATEGORY_PERSONAL",
33
+ name: "CATEGORY_PERSONAL",
34
+ message_list_visibility: "hide",
35
+ label_list_visibility: "labelHide"
36
+ },
37
+ {
38
+ gmail_id: "CATEGORY_SOCIAL",
39
+ name: "CATEGORY_SOCIAL",
40
+ message_list_visibility: "hide",
41
+ label_list_visibility: "labelHide"
42
+ },
43
+ {
44
+ gmail_id: "CATEGORY_PROMOTIONS",
45
+ name: "CATEGORY_PROMOTIONS",
46
+ message_list_visibility: "hide",
47
+ label_list_visibility: "labelHide"
48
+ },
49
+ {
50
+ gmail_id: "CATEGORY_UPDATES",
51
+ name: "CATEGORY_UPDATES",
52
+ message_list_visibility: "hide",
53
+ label_list_visibility: "labelHide"
54
+ },
55
+ {
56
+ gmail_id: "CATEGORY_FORUMS",
57
+ name: "CATEGORY_FORUMS",
58
+ message_list_visibility: "hide",
59
+ label_list_visibility: "labelHide"
60
+ }
61
+ ];
62
+ var SYSTEM_LABEL_IDS = new Set(SYSTEM_LABELS.map((label) => label.gmail_id));
63
+ var LABEL_ALIASES = {
64
+ inbox: "INBOX",
65
+ sent: "SENT",
66
+ draft: "DRAFT",
67
+ drafts: "DRAFT",
68
+ unread: "UNREAD",
69
+ starred: "STARRED",
70
+ important: "IMPORTANT",
71
+ spam: "SPAM",
72
+ trash: "TRASH",
73
+ personal: "CATEGORY_PERSONAL",
74
+ social: "CATEGORY_SOCIAL",
75
+ promotions: "CATEGORY_PROMOTIONS",
76
+ updates: "CATEGORY_UPDATES",
77
+ forums: "CATEGORY_FORUMS"
78
+ };
79
+ function generateUid(prefix = "") {
80
+ const id = randomBytes(12).toString("base64url").slice(0, 20);
81
+ return prefix ? `${prefix}_${id}` : id;
82
+ }
83
+ function generateDraftId() {
84
+ const entropy = randomBytes(4).readUInt32BE(0).toString();
85
+ return `r-${Date.now()}${entropy}`;
86
+ }
87
+ function generateHistoryId() {
88
+ const entropy = randomBytes(3).readUIntBE(0, 3).toString().padStart(8, "0");
89
+ return `${Date.now()}${entropy}`;
90
+ }
91
+ function getAuthenticatedEmail(c) {
92
+ const authUser = c.get("authUser");
93
+ return authUser?.login ?? null;
94
+ }
95
+ function matchesRequestedUser(userId, authEmail) {
96
+ return userId === "me" || userId === authEmail;
97
+ }
98
+ function googleApiError(c, code, message, reason, status) {
99
+ return c.json(
100
+ {
101
+ error: {
102
+ code,
103
+ message,
104
+ errors: [
105
+ {
106
+ message,
107
+ domain: "global",
108
+ reason
109
+ }
110
+ ],
111
+ status
112
+ }
113
+ },
114
+ code
115
+ );
116
+ }
117
+ function parseFormat(value) {
118
+ if (value === "metadata" || value === "minimal" || value === "raw") return value;
119
+ return "full";
120
+ }
121
+ function parseOffset(value) {
122
+ if (!value) return 0;
123
+ const parsed = Number.parseInt(value, 10);
124
+ if (Number.isNaN(parsed) || parsed < 0) return 0;
125
+ return parsed;
126
+ }
127
+ function normalizeLimit(value, fallback, max = 500) {
128
+ if (!value) return fallback;
129
+ const parsed = Number.parseInt(value, 10);
130
+ if (Number.isNaN(parsed) || parsed <= 0) return fallback;
131
+ return Math.max(1, Math.min(max, parsed));
132
+ }
133
+ function parseBooleanParam(value) {
134
+ return value === "true" || value === "1";
135
+ }
136
+ function ensureSystemLabels(gs, userEmail) {
137
+ const existingIds = new Set(
138
+ gs.labels.findBy("user_email", userEmail).map((row) => row.gmail_id)
139
+ );
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(
485
+ (latest, current) => compareHistoryIds(current, latest) > 0 ? current : latest
486
+ );
487
+ }
488
+ function listHistoryForUser(gs, userEmail, options) {
489
+ const requestedTypes = options.historyTypes?.length ? new Set(options.historyTypes) : null;
490
+ 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);
491
+ const grouped = /* @__PURE__ */ new Map();
492
+ for (const event of events) {
493
+ const existing = grouped.get(event.gmail_id);
494
+ if (existing) existing.push(event);
495
+ else grouped.set(event.gmail_id, [event]);
496
+ }
497
+ const historyEntries = Array.from(grouped.entries()).map(
498
+ ([historyId, entries]) => formatHistoryEntry(gs, userEmail, historyId, entries)
499
+ );
500
+ const offset = parseOffset(options.pageToken);
501
+ const limit = Math.max(1, Math.min(options.maxResults ?? 100, 500));
502
+ const page = historyEntries.slice(offset, offset + limit);
503
+ const nextPageToken = offset + limit < historyEntries.length ? String(offset + limit) : void 0;
504
+ return {
505
+ history: page,
506
+ historyId: getCurrentHistoryId(gs, userEmail),
507
+ nextPageToken
508
+ };
509
+ }
510
+ function getFilterById(gs, userEmail, filterId) {
511
+ return gs.filters.findBy("user_email", userEmail).find((filter) => filter.gmail_id === filterId);
512
+ }
513
+ function listFiltersForUser(gs, userEmail) {
514
+ return gs.filters.findBy("user_email", userEmail).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.gmail_id.localeCompare(b.gmail_id));
515
+ }
516
+ function findMatchingFilter(gs, input) {
517
+ const criteriaFrom = normalizeFilterFrom(input.criteria_from);
518
+ const addLabelIds = sortStrings(dedupeLabelIds(input.add_label_ids ?? []));
519
+ const removeLabelIds = sortStrings(dedupeLabelIds(input.remove_label_ids ?? []));
520
+ return gs.filters.findBy("user_email", input.user_email).find(
521
+ (filter) => normalizeFilterFrom(filter.criteria_from) === criteriaFrom && arrayEquals(sortStrings(filter.add_label_ids), addLabelIds) && arrayEquals(sortStrings(filter.remove_label_ids), removeLabelIds)
522
+ );
523
+ }
524
+ function createFilterRecord(gs, input) {
525
+ return gs.filters.insert({
526
+ gmail_id: input.gmail_id ?? generateUid("filter"),
527
+ user_email: input.user_email,
528
+ criteria_from: normalizeFilterFrom(input.criteria_from),
529
+ add_label_ids: dedupeLabelIds(input.add_label_ids ?? []),
530
+ remove_label_ids: dedupeLabelIds(input.remove_label_ids ?? [])
531
+ });
532
+ }
533
+ function formatFilterResource(filter) {
534
+ return {
535
+ id: filter.gmail_id,
536
+ criteria: filter.criteria_from ? { from: filter.criteria_from } : {},
537
+ action: {
538
+ ...filter.add_label_ids.length > 0 ? { addLabelIds: filter.add_label_ids } : {},
539
+ ...filter.remove_label_ids.length > 0 ? { removeLabelIds: filter.remove_label_ids } : {}
540
+ }
541
+ };
542
+ }
543
+ function listForwardingAddressesForUser(gs, userEmail) {
544
+ return gs.forwardingAddresses.findBy("user_email", userEmail).sort((a, b) => a.forwarding_email.localeCompare(b.forwarding_email));
545
+ }
546
+ function formatForwardingAddressResource(entry) {
547
+ return {
548
+ forwardingEmail: entry.forwarding_email,
549
+ verificationStatus: entry.verification_status
550
+ };
551
+ }
552
+ function listSendAsForUser(gs, userEmail) {
553
+ ensureDefaultSendAs(gs, userEmail);
554
+ 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));
555
+ }
556
+ function formatSendAsResource(entry) {
557
+ return {
558
+ sendAsEmail: entry.send_as_email,
559
+ displayName: entry.display_name ?? void 0,
560
+ replyToAddress: entry.send_as_email,
561
+ signature: entry.signature,
562
+ isPrimary: entry.is_default,
563
+ isDefault: entry.is_default,
564
+ treatAsAlias: false,
565
+ verificationStatus: "accepted"
566
+ };
567
+ }
568
+ function listMessagesForUser(gs, userEmail, options) {
569
+ let messages = gs.messages.findBy("user_email", userEmail);
570
+ if (!options?.includeSpamTrash) {
571
+ messages = messages.filter(
572
+ (message) => !message.label_ids.includes("TRASH") && !message.label_ids.includes("SPAM")
573
+ );
574
+ }
575
+ if (options?.labelIds?.length) {
576
+ messages = messages.filter((message) => options.labelIds.every((labelId) => message.label_ids.includes(labelId)));
577
+ }
578
+ if (options?.query) {
579
+ const matcher = buildMessageQueryMatcher(gs, userEmail, options.query);
580
+ messages = messages.filter(matcher);
581
+ }
582
+ return sortMessagesByDateDesc(messages);
583
+ }
584
+ function groupThreads(messages) {
585
+ const threadMap = /* @__PURE__ */ new Map();
586
+ for (const message of messages) {
587
+ const existing = threadMap.get(message.thread_id);
588
+ if (existing) existing.push(message);
589
+ else threadMap.set(message.thread_id, [message]);
590
+ }
591
+ return Array.from(threadMap.entries()).map(([threadId, entries]) => {
592
+ const ordered = sortMessagesByDateAsc(entries);
593
+ const latest = ordered.at(-1);
594
+ return {
595
+ id: threadId,
596
+ snippet: latest.snippet,
597
+ historyId: latest.history_id,
598
+ messages: ordered
599
+ };
600
+ }).sort((a, b) => Number(b.messages.at(-1)?.internal_date ?? 0) - Number(a.messages.at(-1)?.internal_date ?? 0));
601
+ }
602
+ function getThreadMessages(gs, userEmail, threadId, options) {
603
+ let messages = gs.messages.findBy("user_email", userEmail).filter((message) => message.thread_id === threadId);
604
+ if (!options?.includeSpamTrash) {
605
+ messages = messages.filter(
606
+ (message) => !message.label_ids.includes("TRASH") && !message.label_ids.includes("SPAM")
607
+ );
608
+ }
609
+ return sortMessagesByDateAsc(messages);
610
+ }
611
+ function formatMessageResource(gs, message, format, metadataHeaders = []) {
612
+ const headers = buildHeaders(message);
613
+ const filteredHeaders = format === "metadata" && metadataHeaders.length > 0 ? headers.filter((header) => metadataHeaders.includes(header.name)) : headers;
614
+ const base = {
615
+ id: message.gmail_id,
616
+ threadId: message.thread_id,
617
+ labelIds: message.label_ids,
618
+ snippet: message.snippet,
619
+ historyId: message.history_id,
620
+ internalDate: message.internal_date,
621
+ sizeEstimate: estimateSize(message, headers)
622
+ };
623
+ if (format === "minimal") return base;
624
+ if (format === "raw") return { ...base, raw: message.raw ?? void 0 };
625
+ return {
626
+ ...base,
627
+ payload: buildPayload(gs, message, filteredHeaders, format)
628
+ };
629
+ }
630
+ function formatThreadResource(gs, messages, format, metadataHeaders = []) {
631
+ const ordered = sortMessagesByDateAsc(messages);
632
+ const latest = ordered.at(-1);
633
+ return {
634
+ id: latest.thread_id,
635
+ historyId: latest.history_id,
636
+ snippet: latest.snippet,
637
+ messages: ordered.map((message) => formatMessageResource(gs, message, format, metadataHeaders))
638
+ };
639
+ }
640
+ function applyLabelMutation(labelIds, addLabelIds = [], removeLabelIds = []) {
641
+ const next = new Set(labelIds);
642
+ for (const labelId of addLabelIds) next.add(labelId);
643
+ for (const labelId of removeLabelIds) next.delete(labelId);
644
+ return [...next];
645
+ }
646
+ function markMessageModified(gs, message, nextLabelIds) {
647
+ const dedupedLabelIds = dedupeLabelIds(nextLabelIds);
648
+ if (arrayEquals(message.label_ids, dedupedLabelIds)) {
649
+ syncDraftState(gs, message);
650
+ return message;
651
+ }
652
+ const historyId = generateHistoryId();
653
+ const addedLabelIds = dedupedLabelIds.filter((labelId) => !message.label_ids.includes(labelId));
654
+ const removedLabelIds = message.label_ids.filter((labelId) => !dedupedLabelIds.includes(labelId));
655
+ const updated = gs.messages.update(message.id, {
656
+ label_ids: dedupedLabelIds,
657
+ history_id: historyId
658
+ }) ?? message;
659
+ const historyEvents = [];
660
+ if (addedLabelIds.length > 0) {
661
+ historyEvents.push({
662
+ change_type: "labelAdded",
663
+ message_gmail_id: updated.gmail_id,
664
+ thread_id: updated.thread_id,
665
+ label_ids: addedLabelIds
666
+ });
667
+ }
668
+ if (removedLabelIds.length > 0) {
669
+ historyEvents.push({
670
+ change_type: "labelRemoved",
671
+ message_gmail_id: updated.gmail_id,
672
+ thread_id: updated.thread_id,
673
+ label_ids: removedLabelIds
674
+ });
675
+ }
676
+ if (historyEvents.length > 0) {
677
+ recordHistoryEvents(gs, updated.user_email, historyId, historyEvents);
678
+ }
679
+ syncDraftState(gs, updated);
680
+ return updated;
681
+ }
682
+ function deleteMessage(gs, message) {
683
+ const historyId = generateHistoryId();
684
+ recordHistoryEvents(gs, message.user_email, historyId, [
685
+ {
686
+ change_type: "messageDeleted",
687
+ message_gmail_id: message.gmail_id,
688
+ thread_id: message.thread_id,
689
+ label_ids: message.label_ids
690
+ }
691
+ ]);
692
+ clearDraftRecordsForMessage(gs, message.user_email, message.gmail_id);
693
+ clearMessageAttachments(gs, message.user_email, message.gmail_id);
694
+ return gs.messages.delete(message.id);
695
+ }
696
+ function trashLabelIds(labelIds) {
697
+ const next = new Set(labelIds);
698
+ next.add("TRASH");
699
+ next.delete("INBOX");
700
+ return [...next];
701
+ }
702
+ function untrashLabelIds(labelIds) {
703
+ const next = new Set(labelIds);
704
+ next.delete("TRASH");
705
+ if (!next.has("SENT") && !next.has("DRAFT")) {
706
+ next.add("INBOX");
707
+ }
708
+ return [...next];
709
+ }
710
+ function buildMessageQueryMatcher(gs, userEmail, query) {
711
+ const terms = query.match(/"[^"]+"|\S+/g) ?? [];
712
+ const predicates = terms.flatMap((term) => buildQueryPredicates(gs, userEmail, term));
713
+ if (!predicates.length) return () => true;
714
+ return (message) => predicates.every((predicate) => predicate(message));
715
+ }
716
+ function buildRawMessage(message) {
717
+ const headers = [
718
+ `From: ${message.from}`,
719
+ `To: ${message.to}`,
720
+ ...message.cc ? [`Cc: ${message.cc}`] : [],
721
+ ...message.bcc ? [`Bcc: ${message.bcc}`] : [],
722
+ ...message.reply_to ? [`Reply-To: ${message.reply_to}`] : [],
723
+ `Subject: ${message.subject}`,
724
+ ...message.message_id ? [`Message-ID: ${message.message_id}`] : [],
725
+ ...message.references ? [`References: ${message.references}`] : [],
726
+ ...message.in_reply_to ? [`In-Reply-To: ${message.in_reply_to}`] : [],
727
+ `Date: ${message.date_header ?? (/* @__PURE__ */ new Date()).toUTCString()}`,
728
+ "MIME-Version: 1.0"
729
+ ];
730
+ const attachments = message.attachments ?? [];
731
+ if (attachments.length > 0) {
732
+ const mixedBoundary = `emulate-mixed-${randomBytes(8).toString("hex")}`;
733
+ headers.push(`Content-Type: multipart/mixed; boundary="${mixedBoundary}"`);
734
+ const parts = [];
735
+ const bodyPart2 = buildMimeBodyPart({
736
+ body_text: message.body_text,
737
+ body_html: message.body_html
738
+ });
739
+ if (bodyPart2) {
740
+ parts.push(`--${mixedBoundary}`, bodyPart2);
741
+ }
742
+ for (const attachment of attachments) {
743
+ const disposition = attachment.disposition ?? "attachment";
744
+ const contentId = attachment.content_id ? ensureWrappedContentId(attachment.content_id) : null;
745
+ parts.push(`--${mixedBoundary}`);
746
+ parts.push(`Content-Type: ${attachment.mime_type}; name="${escapeMimeParameter(attachment.filename)}"`);
747
+ parts.push(`Content-Disposition: ${disposition}; filename="${escapeMimeParameter(attachment.filename)}"`);
748
+ if (contentId) parts.push(`Content-ID: ${contentId}`);
749
+ parts.push("Content-Transfer-Encoding: base64");
750
+ parts.push("");
751
+ parts.push(wrapBase64(encodeAttachmentContent(attachment.content)));
752
+ }
753
+ parts.push(`--${mixedBoundary}--`, "");
754
+ return Buffer.from(`${headers.join("\r\n")}\r
755
+ \r
756
+ ${parts.join("\r\n")}`, "utf8").toString("base64url");
757
+ }
758
+ const bodyPart = buildMimeBodyPart({
759
+ body_text: message.body_text,
760
+ body_html: message.body_html
761
+ });
762
+ if (bodyPart) {
763
+ return Buffer.from(`${headers.join("\r\n")}\r
764
+ \r
765
+ ${bodyPart}`, "utf8").toString("base64url");
766
+ }
767
+ headers.push("Content-Type: text/plain; charset=utf-8");
768
+ return Buffer.from(`${headers.join("\r\n")}\r
769
+ \r
770
+ `, "utf8").toString("base64url");
771
+ }
772
+ function buildQueryPredicates(gs, userEmail, term) {
773
+ const cleaned = cleanToken(term);
774
+ if (!cleaned) return [];
775
+ const lower = cleaned.toLowerCase();
776
+ if (lower === "or" || lower === "and") return [];
777
+ if (lower.startsWith("-label:")) {
778
+ const labelQuery = cleaned.slice(7);
779
+ return [(message) => !messageMatchesLabelQuery(gs, userEmail, message, labelQuery)];
780
+ }
781
+ if (lower.startsWith("label:")) {
782
+ const labelQuery = cleaned.slice(6);
783
+ return [(message) => messageMatchesLabelQuery(gs, userEmail, message, labelQuery)];
784
+ }
785
+ if (lower.startsWith("in:")) {
786
+ const labelQuery = cleaned.slice(3);
787
+ return [(message) => messageMatchesLabelQuery(gs, userEmail, message, labelQuery)];
788
+ }
789
+ if (lower.startsWith("is:")) {
790
+ const state = cleaned.slice(3).toLowerCase();
791
+ if (state === "read") return [(message) => !message.label_ids.includes("UNREAD")];
792
+ return [(message) => messageMatchesLabelQuery(gs, userEmail, message, state)];
793
+ }
794
+ if (lower.startsWith("from:")) {
795
+ const value2 = cleaned.slice(5).toLowerCase();
796
+ return value2 ? [(message) => message.from.toLowerCase().includes(value2)] : [];
797
+ }
798
+ if (lower.startsWith("to:")) {
799
+ const value2 = cleaned.slice(3).toLowerCase();
800
+ return value2 ? [(message) => message.to.toLowerCase().includes(value2)] : [];
801
+ }
802
+ if (lower.startsWith("subject:")) {
803
+ const value2 = cleaned.slice(8).toLowerCase();
804
+ return value2 ? [(message) => message.subject.toLowerCase().includes(value2)] : [];
805
+ }
806
+ if (lower.startsWith("rfc822msgid:")) {
807
+ const value2 = cleaned.slice(11).replace(/[<>]/g, "").toLowerCase();
808
+ return value2 ? [(message) => message.message_id.replace(/[<>]/g, "").toLowerCase() === value2] : [];
809
+ }
810
+ if (lower.startsWith("before:")) {
811
+ const timestamp = parseDateFilter(cleaned.slice(7));
812
+ return timestamp != null ? [(message) => Number(message.internal_date) < timestamp] : [];
813
+ }
814
+ if (lower.startsWith("after:")) {
815
+ const timestamp = parseDateFilter(cleaned.slice(6));
816
+ return timestamp != null ? [(message) => Number(message.internal_date) > timestamp] : [];
817
+ }
818
+ if (lower === "has:attachment") {
819
+ return [(message) => hasMessageAttachments(gs, message)];
820
+ }
821
+ const value = cleaned.toLowerCase();
822
+ return value ? [(message) => searchableText(message).includes(value)] : [];
823
+ }
824
+ function resolveInternalDate(value) {
825
+ if (!value) return Date.now();
826
+ if (/^\d+$/.test(value)) {
827
+ const parsed2 = Number.parseInt(value, 10);
828
+ if (String(parsed2).length >= 13) return parsed2;
829
+ return parsed2 * 1e3;
830
+ }
831
+ const parsed = Date.parse(value);
832
+ return Number.isFinite(parsed) ? parsed : Date.now();
833
+ }
834
+ function formatHistoryEntry(gs, userEmail, historyId, events) {
835
+ const messages = /* @__PURE__ */ new Map();
836
+ const messagesAdded = [];
837
+ const messagesDeleted = [];
838
+ const labelsAdded = [];
839
+ const labelsRemoved = [];
840
+ for (const event of events) {
841
+ const message = formatHistoryMessageRef(gs, userEmail, event);
842
+ messages.set(event.message_gmail_id, message);
843
+ if (event.change_type === "messageAdded") {
844
+ messagesAdded.push({ message });
845
+ } else if (event.change_type === "messageDeleted") {
846
+ messagesDeleted.push({ message });
847
+ } else if (event.change_type === "labelAdded") {
848
+ labelsAdded.push({ message, labelIds: event.label_ids });
849
+ } else if (event.change_type === "labelRemoved") {
850
+ labelsRemoved.push({ message, labelIds: event.label_ids });
851
+ }
852
+ }
853
+ return {
854
+ id: historyId,
855
+ messages: Array.from(messages.values()),
856
+ ...messagesAdded.length > 0 ? { messagesAdded } : {},
857
+ ...messagesDeleted.length > 0 ? { messagesDeleted } : {},
858
+ ...labelsAdded.length > 0 ? { labelsAdded } : {},
859
+ ...labelsRemoved.length > 0 ? { labelsRemoved } : {}
860
+ };
861
+ }
862
+ function formatHistoryMessageRef(gs, userEmail, event) {
863
+ const message = getMessageById(gs, userEmail, event.message_gmail_id);
864
+ return {
865
+ id: event.message_gmail_id,
866
+ threadId: message?.thread_id ?? event.thread_id,
867
+ labelIds: message?.label_ids ?? event.label_ids,
868
+ historyId: message?.history_id ?? event.gmail_id,
869
+ ...message?.internal_date ? { internalDate: message.internal_date } : {}
870
+ };
871
+ }
872
+ function compareHistoryIds(left, right) {
873
+ try {
874
+ const leftValue = BigInt(left);
875
+ const rightValue = BigInt(right);
876
+ if (leftValue === rightValue) return 0;
877
+ return leftValue > rightValue ? 1 : -1;
878
+ } catch {
879
+ return left.localeCompare(right);
880
+ }
881
+ }
882
+ function resolveThreadId(gs, userEmail, explicitThreadId, inReplyTo, references) {
883
+ if (explicitThreadId) return explicitThreadId;
884
+ const linkedIds = [inReplyTo, references].flatMap((value) => value ? value.split(/\s+/) : []).map((value) => value.trim()).filter(Boolean);
885
+ for (const headerMessageId of linkedIds) {
886
+ const linkedMessage = gs.messages.findBy("user_email", userEmail).find((message) => message.message_id === headerMessageId);
887
+ if (linkedMessage) return linkedMessage.thread_id;
888
+ }
889
+ return generateUid("thr");
890
+ }
891
+ function replaceMessageAttachments(gs, message, attachments) {
892
+ clearMessageAttachments(gs, message.user_email, message.gmail_id);
893
+ for (const attachment of attachments) {
894
+ gs.attachments.insert({
895
+ gmail_id: generateUid("att"),
896
+ user_email: message.user_email,
897
+ message_gmail_id: message.gmail_id,
898
+ filename: attachment.filename,
899
+ mime_type: attachment.mime_type,
900
+ disposition: attachment.disposition,
901
+ content_id: attachment.content_id,
902
+ transfer_encoding: attachment.transfer_encoding,
903
+ data: attachment.data,
904
+ size: attachment.size
905
+ });
906
+ }
907
+ }
908
+ function recordHistoryEvents(gs, userEmail, historyId, events) {
909
+ for (const event of events) {
910
+ gs.history.insert({
911
+ gmail_id: historyId,
912
+ user_email: userEmail,
913
+ change_type: event.change_type,
914
+ message_gmail_id: event.message_gmail_id,
915
+ thread_id: event.thread_id,
916
+ label_ids: dedupeLabelIds(event.label_ids)
917
+ });
918
+ }
919
+ }
920
+ function applyFiltersToLabelIds(gs, userEmail, from, labelIds) {
921
+ if (!from) return labelIds;
922
+ let nextLabelIds = dedupeLabelIds(labelIds);
923
+ for (const filter of gs.filters.findBy("user_email", userEmail)) {
924
+ if (!matchesFilter(filter, from)) continue;
925
+ nextLabelIds = applyLabelMutation(nextLabelIds, filter.add_label_ids, filter.remove_label_ids);
926
+ }
927
+ return nextLabelIds;
928
+ }
929
+ function syncDraftState(gs, message, preferredDraftId) {
930
+ const shouldHaveDraft = message.label_ids.includes("DRAFT") && !message.label_ids.includes("SENT");
931
+ const existing = gs.drafts.findBy("message_gmail_id", message.gmail_id).filter((draft) => draft.user_email === message.user_email);
932
+ if (!shouldHaveDraft) {
933
+ for (const draft of existing) {
934
+ gs.drafts.delete(draft.id);
935
+ }
936
+ return void 0;
937
+ }
938
+ if (existing[0]) return existing[0];
939
+ return gs.drafts.insert({
940
+ gmail_id: preferredDraftId ?? generateDraftId(),
941
+ user_email: message.user_email,
942
+ message_gmail_id: message.gmail_id
943
+ });
944
+ }
945
+ function clearDraftRecordsForMessage(gs, userEmail, messageId) {
946
+ const drafts = gs.drafts.findBy("message_gmail_id", messageId).filter((draft) => draft.user_email === userEmail);
947
+ for (const draft of drafts) {
948
+ gs.drafts.delete(draft.id);
949
+ }
950
+ }
951
+ function clearMessageAttachments(gs, userEmail, messageId) {
952
+ const attachments = gs.attachments.findBy("message_gmail_id", messageId).filter((attachment) => attachment.user_email === userEmail);
953
+ for (const attachment of attachments) {
954
+ gs.attachments.delete(attachment.id);
955
+ }
956
+ }
957
+ function listAttachmentsForMessage(gs, message) {
958
+ 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));
959
+ }
960
+ function hasMessageAttachments(gs, message) {
961
+ return gs.attachments.findBy("message_gmail_id", message.gmail_id).some((attachment) => attachment.user_email === message.user_email);
962
+ }
963
+ function ensureDefaultSendAs(gs, userEmail) {
964
+ const existing = gs.sendAs.findBy("user_email", userEmail);
965
+ if (existing.length > 0) {
966
+ if (!existing.some((entry) => entry.is_default)) {
967
+ gs.sendAs.update(existing[0].id, { is_default: true });
968
+ }
969
+ return;
970
+ }
971
+ const user = gs.users.findOneBy("email", userEmail);
972
+ gs.sendAs.insert({
973
+ user_email: userEmail,
974
+ send_as_email: userEmail,
975
+ display_name: user?.name?.trim() || userEmail.split("@")[0],
976
+ is_default: true,
977
+ signature: ""
978
+ });
979
+ }
980
+ function matchesFilter(filter, from) {
981
+ if (filter.criteria_from) {
982
+ return from.toLowerCase().includes(filter.criteria_from.toLowerCase());
983
+ }
984
+ return true;
985
+ }
986
+ function normalizeFilterFrom(value) {
987
+ const normalized = value?.trim();
988
+ return normalized ? normalized : null;
989
+ }
990
+ function sortStrings(values) {
991
+ return [...values].sort((left, right) => left.localeCompare(right));
992
+ }
993
+ function arrayEquals(left, right) {
994
+ if (left.length !== right.length) return false;
995
+ return left.every((value, index) => value === right[index]);
996
+ }
997
+ function messageMatchesLabelQuery(gs, userEmail, message, query) {
998
+ const normalized = normalizeLabelQuery(query);
999
+ if (message.label_ids.includes(normalized)) return true;
1000
+ return message.label_ids.some((labelId) => {
1001
+ const label = findLabelById(gs, userEmail, labelId);
1002
+ return label?.name.toLowerCase() === cleanToken(query).toLowerCase();
1003
+ });
1004
+ }
1005
+ function parseDateFilter(value) {
1006
+ const trimmed = cleanToken(value);
1007
+ if (!trimmed) return null;
1008
+ if (/^\d+$/.test(trimmed)) {
1009
+ const parsed2 = Number.parseInt(trimmed, 10);
1010
+ return String(parsed2).length >= 13 ? parsed2 : parsed2 * 1e3;
1011
+ }
1012
+ const parsed = Date.parse(trimmed);
1013
+ return Number.isFinite(parsed) ? parsed : null;
1014
+ }
1015
+ function searchableText(message) {
1016
+ return [
1017
+ message.subject,
1018
+ message.from,
1019
+ message.to,
1020
+ message.cc ?? "",
1021
+ message.bcc ?? "",
1022
+ message.snippet,
1023
+ message.body_text ?? "",
1024
+ stripHtml(message.body_html ?? "")
1025
+ ].join(" ").toLowerCase();
1026
+ }
1027
+ function cleanToken(token) {
1028
+ return token.trim().replace(/^[()]+/, "").replace(/[()]+$/, "").replace(/^"(.*)"$/, "$1");
1029
+ }
1030
+ function stripHtml(html) {
1031
+ return html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
1032
+ }
1033
+ function buildHeaders(message) {
1034
+ const headers = [
1035
+ { name: "From", value: message.from },
1036
+ { name: "To", value: message.to },
1037
+ { name: "Cc", value: message.cc },
1038
+ { name: "Bcc", value: message.bcc },
1039
+ { name: "Reply-To", value: message.reply_to },
1040
+ { name: "Subject", value: message.subject },
1041
+ { name: "Date", value: message.date_header },
1042
+ { name: "Message-ID", value: message.message_id },
1043
+ { name: "References", value: message.references },
1044
+ { name: "In-Reply-To", value: message.in_reply_to }
1045
+ ];
1046
+ return headers.filter((header) => Boolean(header.value));
1047
+ }
1048
+ function buildPayload(gs, message, headers, format) {
1049
+ const textBody = message.body_text ?? null;
1050
+ const htmlBody = message.body_html ?? null;
1051
+ const attachments = listAttachmentsForMessage(gs, message);
1052
+ if (format === "metadata") {
1053
+ return {
1054
+ partId: "",
1055
+ mimeType: attachments.length > 0 ? "multipart/mixed" : htmlBody ? "text/html" : "text/plain",
1056
+ filename: "",
1057
+ headers,
1058
+ body: { size: 0 }
1059
+ };
1060
+ }
1061
+ if (attachments.length === 0) {
1062
+ if (textBody && htmlBody) {
1063
+ return {
1064
+ partId: "",
1065
+ mimeType: "multipart/alternative",
1066
+ filename: "",
1067
+ headers,
1068
+ body: { size: 0 },
1069
+ parts: [
1070
+ createTextBodyPart("0", "text/plain", textBody),
1071
+ createTextBodyPart("1", "text/html", htmlBody)
1072
+ ]
1073
+ };
1074
+ }
1075
+ if (htmlBody) return createTextBodyPart("", "text/html", htmlBody, headers);
1076
+ if (textBody) return createTextBodyPart("", "text/plain", textBody, headers);
1077
+ return {
1078
+ partId: "",
1079
+ mimeType: "text/plain",
1080
+ filename: "",
1081
+ headers,
1082
+ body: { size: 0 }
1083
+ };
1084
+ }
1085
+ const parts = [];
1086
+ if (textBody && htmlBody) {
1087
+ parts.push({
1088
+ partId: "0",
1089
+ mimeType: "multipart/alternative",
1090
+ filename: "",
1091
+ headers: [],
1092
+ body: { size: 0 },
1093
+ parts: [
1094
+ createTextBodyPart("0.0", "text/plain", textBody),
1095
+ createTextBodyPart("0.1", "text/html", htmlBody)
1096
+ ]
1097
+ });
1098
+ } else if (htmlBody) {
1099
+ parts.push(createTextBodyPart("0", "text/html", htmlBody));
1100
+ } else if (textBody) {
1101
+ parts.push(createTextBodyPart("0", "text/plain", textBody));
1102
+ }
1103
+ for (const [index, attachment] of attachments.entries()) {
1104
+ parts.push(createAttachmentPart(String(parts.length + index), attachment));
1105
+ }
1106
+ return {
1107
+ partId: "",
1108
+ mimeType: "multipart/mixed",
1109
+ filename: "",
1110
+ headers,
1111
+ body: { size: 0 },
1112
+ parts
1113
+ };
1114
+ }
1115
+ function createTextBodyPart(partId, mimeType, content, headers = []) {
1116
+ return {
1117
+ partId,
1118
+ mimeType,
1119
+ filename: "",
1120
+ headers,
1121
+ body: {
1122
+ size: Buffer.byteLength(content, "utf8"),
1123
+ data: Buffer.from(content, "utf8").toString("base64url")
1124
+ }
1125
+ };
1126
+ }
1127
+ function createAttachmentPart(partId, attachment) {
1128
+ const headers = [
1129
+ {
1130
+ name: "Content-Type",
1131
+ value: attachment.filename ? `${attachment.mime_type}; name="${attachment.filename}"` : attachment.mime_type
1132
+ },
1133
+ {
1134
+ name: "Content-Disposition",
1135
+ value: `${attachment.disposition ?? "attachment"}; filename="${attachment.filename}"`
1136
+ }
1137
+ ];
1138
+ if (attachment.transfer_encoding) {
1139
+ headers.push({ name: "Content-Transfer-Encoding", value: attachment.transfer_encoding });
1140
+ }
1141
+ if (attachment.content_id) {
1142
+ headers.push({ name: "Content-ID", value: attachment.content_id });
1143
+ }
1144
+ return {
1145
+ partId,
1146
+ mimeType: attachment.mime_type,
1147
+ filename: attachment.filename,
1148
+ headers,
1149
+ body: {
1150
+ attachmentId: attachment.gmail_id,
1151
+ size: attachment.size
1152
+ }
1153
+ };
1154
+ }
1155
+ function estimateSize(message, preBuiltHeaders) {
1156
+ if (message.raw) {
1157
+ return Buffer.byteLength(message.raw, "utf8");
1158
+ }
1159
+ const headers = (preBuiltHeaders ?? buildHeaders(message)).map((header) => `${header.name}: ${header.value}`).join("\n");
1160
+ const body = `${message.body_text ?? ""}
1161
+ ${message.body_html ?? ""}`;
1162
+ return Buffer.byteLength(`${headers}
1163
+
1164
+ ${body}`, "utf8");
1165
+ }
1166
+ function deriveSnippet(value) {
1167
+ return stripHtml(value).slice(0, 140);
1168
+ }
1169
+ function sortMessagesByDateDesc(messages) {
1170
+ return [...messages].sort((a, b) => Number(b.internal_date) - Number(a.internal_date));
1171
+ }
1172
+ function sortMessagesByDateAsc(messages) {
1173
+ return [...messages].sort((a, b) => Number(a.internal_date) - Number(b.internal_date));
1174
+ }
1175
+ function buildMimeBodyPart(input) {
1176
+ if (input.body_text && input.body_html) {
1177
+ const boundary = `emulate-alt-${randomBytes(8).toString("hex")}`;
1178
+ return [
1179
+ `Content-Type: multipart/alternative; boundary="${boundary}"`,
1180
+ "",
1181
+ `--${boundary}`,
1182
+ "Content-Type: text/plain; charset=utf-8",
1183
+ "",
1184
+ input.body_text,
1185
+ `--${boundary}`,
1186
+ "Content-Type: text/html; charset=utf-8",
1187
+ "",
1188
+ input.body_html,
1189
+ `--${boundary}--`,
1190
+ ""
1191
+ ].join("\r\n");
1192
+ }
1193
+ if (input.body_html) {
1194
+ return [
1195
+ "Content-Type: text/html; charset=utf-8",
1196
+ "",
1197
+ input.body_html
1198
+ ].join("\r\n");
1199
+ }
1200
+ if (input.body_text) {
1201
+ return [
1202
+ "Content-Type: text/plain; charset=utf-8",
1203
+ "",
1204
+ input.body_text
1205
+ ].join("\r\n");
1206
+ }
1207
+ return null;
1208
+ }
1209
+ function encodeAttachmentContent(content) {
1210
+ const buffer = Buffer.isBuffer(content) ? content : Buffer.from(content, "utf8");
1211
+ return buffer.toString("base64");
1212
+ }
1213
+ function wrapBase64(value) {
1214
+ return value.replace(/.{1,76}/g, "$&\r\n").trimEnd();
1215
+ }
1216
+ function escapeMimeParameter(value) {
1217
+ return value.replace(/"/g, '\\"');
1218
+ }
1219
+ function ensureWrappedContentId(value) {
1220
+ if (value.startsWith("<") && value.endsWith(">")) return value;
1221
+ return `<${value}>`;
1222
+ }
1223
+ function parseRawMessage(raw) {
1224
+ const decoded = decodeBase64Like(raw).toString("utf8").replace(/\r\n/g, "\n");
1225
+ const root = parseMimeEntity(decoded);
1226
+ const attachments = collectMimeNodes(root).filter((node) => isAttachmentNode(node)).map((node) => ({
1227
+ filename: node.filename || "attachment",
1228
+ mime_type: node.mimeType || "application/octet-stream",
1229
+ disposition: node.disposition,
1230
+ content_id: node.contentId,
1231
+ transfer_encoding: node.transferEncoding,
1232
+ data: (node.body ?? Buffer.alloc(0)).toString("base64url"),
1233
+ size: node.body?.length ?? 0
1234
+ }));
1235
+ return {
1236
+ raw,
1237
+ from: root.headers.get("from") ?? "",
1238
+ to: root.headers.get("to") ?? "",
1239
+ cc: root.headers.get("cc") ?? null,
1240
+ bcc: root.headers.get("bcc") ?? null,
1241
+ reply_to: root.headers.get("reply-to") ?? null,
1242
+ subject: root.headers.get("subject") ?? "",
1243
+ message_id: root.headers.get("message-id") ?? null,
1244
+ references: root.headers.get("references") ?? null,
1245
+ in_reply_to: root.headers.get("in-reply-to") ?? null,
1246
+ date_header: root.headers.get("date") ?? null,
1247
+ body_text: findFirstTextPart(root, "text/plain"),
1248
+ body_html: findFirstTextPart(root, "text/html"),
1249
+ attachments
1250
+ };
1251
+ }
1252
+ function parseMimeEntity(source) {
1253
+ const normalized = source.replace(/\r\n/g, "\n");
1254
+ const separatorIndex = normalized.indexOf("\n\n");
1255
+ const headerText = separatorIndex >= 0 ? normalized.slice(0, separatorIndex) : normalized;
1256
+ const bodyText = separatorIndex >= 0 ? normalized.slice(separatorIndex + 2) : "";
1257
+ const headers = parseHeaders(headerText);
1258
+ const contentType = parseHeaderWithParams(headers.get("content-type") ?? "text/plain; charset=utf-8");
1259
+ const disposition = parseHeaderWithParams(headers.get("content-disposition") ?? "");
1260
+ const boundary = contentType.params.boundary;
1261
+ const mimeType = contentType.value.toLowerCase() || "text/plain";
1262
+ const filename = disposition.params.filename ?? contentType.params.name ?? "";
1263
+ if (mimeType.startsWith("multipart/") && boundary) {
1264
+ return {
1265
+ mimeType,
1266
+ filename,
1267
+ headers,
1268
+ body: null,
1269
+ parts: splitMultipartBody(bodyText, boundary).map((part) => parseMimeEntity(part)),
1270
+ disposition: disposition.value || null,
1271
+ contentId: headers.get("content-id") ?? null,
1272
+ transferEncoding: headers.get("content-transfer-encoding")?.toLowerCase() ?? null,
1273
+ charset: contentType.params.charset ?? null
1274
+ };
1275
+ }
1276
+ return {
1277
+ mimeType,
1278
+ filename,
1279
+ headers,
1280
+ body: decodeMimeBody(bodyText, headers.get("content-transfer-encoding") ?? null),
1281
+ parts: [],
1282
+ disposition: disposition.value || null,
1283
+ contentId: headers.get("content-id") ?? null,
1284
+ transferEncoding: headers.get("content-transfer-encoding")?.toLowerCase() ?? null,
1285
+ charset: contentType.params.charset ?? null
1286
+ };
1287
+ }
1288
+ function parseHeaders(headerText) {
1289
+ const headers = /* @__PURE__ */ new Map();
1290
+ let currentKey = null;
1291
+ for (const line of headerText.split("\n")) {
1292
+ if (!line.trim()) continue;
1293
+ if ((line.startsWith(" ") || line.startsWith(" ")) && currentKey) {
1294
+ headers.set(currentKey, `${headers.get(currentKey) ?? ""} ${line.trim()}`.trim());
1295
+ continue;
1296
+ }
1297
+ const separator = line.indexOf(":");
1298
+ if (separator < 0) continue;
1299
+ currentKey = line.slice(0, separator).trim().toLowerCase();
1300
+ headers.set(currentKey, line.slice(separator + 1).trim());
1301
+ }
1302
+ return headers;
1303
+ }
1304
+ function parseHeaderWithParams(value) {
1305
+ const [base, ...rest] = value.split(";");
1306
+ const params = {};
1307
+ for (const token of rest) {
1308
+ const separator = token.indexOf("=");
1309
+ if (separator < 0) continue;
1310
+ const key = token.slice(0, separator).trim().toLowerCase();
1311
+ const rawValue = token.slice(separator + 1).trim();
1312
+ params[key] = rawValue.replace(/^"(.*)"$/, "$1");
1313
+ }
1314
+ return {
1315
+ value: base.trim(),
1316
+ params
1317
+ };
1318
+ }
1319
+ function splitMultipartBody(body, boundary) {
1320
+ const marker = `--${boundary}`;
1321
+ const chunks = [];
1322
+ for (const segment of body.split(marker)) {
1323
+ const trimmed = segment.trim();
1324
+ if (!trimmed || trimmed === "--") continue;
1325
+ chunks.push(trimmed.replace(/^\n+/, "").replace(/\n+$/, ""));
1326
+ }
1327
+ return chunks;
1328
+ }
1329
+ function decodeMimeBody(body, transferEncoding) {
1330
+ const normalizedEncoding = transferEncoding?.toLowerCase() ?? "";
1331
+ if (normalizedEncoding === "base64") {
1332
+ const compact = body.replace(/\s+/g, "");
1333
+ return compact ? Buffer.from(compact, "base64") : Buffer.alloc(0);
1334
+ }
1335
+ if (normalizedEncoding === "quoted-printable") {
1336
+ return decodeQuotedPrintable(body);
1337
+ }
1338
+ return Buffer.from(body, "utf8");
1339
+ }
1340
+ function decodeQuotedPrintable(value) {
1341
+ const normalized = value.replace(/=\r?\n/g, "");
1342
+ const bytes = [];
1343
+ for (let index = 0; index < normalized.length; index += 1) {
1344
+ const current = normalized[index];
1345
+ if (current === "=" && /^[A-Fa-f0-9]{2}$/.test(normalized.slice(index + 1, index + 3))) {
1346
+ bytes.push(Number.parseInt(normalized.slice(index + 1, index + 3), 16));
1347
+ index += 2;
1348
+ continue;
1349
+ }
1350
+ bytes.push(normalized.charCodeAt(index));
1351
+ }
1352
+ return Buffer.from(bytes);
1353
+ }
1354
+ function findFirstTextPart(root, mimeType) {
1355
+ for (const node of collectMimeNodes(root)) {
1356
+ if (node.parts.length > 0) continue;
1357
+ if (!node.mimeType.includes(mimeType)) continue;
1358
+ if (isAttachmentNode(node)) continue;
1359
+ const content = decodeTextNode(node).trim();
1360
+ if (content) return content;
1361
+ }
1362
+ return null;
1363
+ }
1364
+ function decodeTextNode(node) {
1365
+ const encoding = normalizeCharset(node.charset);
1366
+ return (node.body ?? Buffer.alloc(0)).toString(encoding);
1367
+ }
1368
+ function normalizeCharset(value) {
1369
+ const normalized = value?.trim().toLowerCase();
1370
+ if (!normalized || normalized === "utf-8" || normalized === "us-ascii") return "utf8";
1371
+ if (normalized === "iso-8859-1" || normalized === "latin1") return "latin1";
1372
+ return "utf8";
1373
+ }
1374
+ function collectMimeNodes(root) {
1375
+ const nodes = [];
1376
+ const queue = [root];
1377
+ while (queue.length > 0) {
1378
+ const node = queue.shift();
1379
+ nodes.push(node);
1380
+ if (node.parts.length > 0) {
1381
+ queue.push(...node.parts);
1382
+ }
1383
+ }
1384
+ return nodes;
1385
+ }
1386
+ function isAttachmentNode(node) {
1387
+ if (node.parts.length > 0) return false;
1388
+ const disposition = node.disposition?.toLowerCase() ?? "";
1389
+ if (node.filename) return true;
1390
+ if (disposition.includes("attachment")) return true;
1391
+ if (disposition.includes("inline") && !node.mimeType.startsWith("text/")) return true;
1392
+ return false;
1393
+ }
1394
+ function decodeBase64Like(value) {
1395
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
1396
+ const padding = normalized.length % 4 === 0 ? "" : "=".repeat(4 - normalized.length % 4);
1397
+ return Buffer.from(normalized + padding, "base64");
1398
+ }
1399
+ function ensureDefaultCalendars(gs, userEmail) {
1400
+ const existing = gs.calendars.findBy("user_email", userEmail);
1401
+ if (existing.length > 0) {
1402
+ if (!existing.some((calendar) => calendar.primary)) {
1403
+ gs.calendars.update(existing[0].id, { primary: true });
1404
+ }
1405
+ return;
1406
+ }
1407
+ gs.calendars.insert({
1408
+ google_id: "primary",
1409
+ user_email: userEmail,
1410
+ summary: userEmail,
1411
+ description: null,
1412
+ time_zone: "UTC",
1413
+ primary: true,
1414
+ selected: true,
1415
+ access_role: "owner",
1416
+ background_color: null,
1417
+ foreground_color: null
1418
+ });
1419
+ }
1420
+ function createCalendarRecord(gs, input) {
1421
+ const calendarId = input.google_id ?? generateUid("cal");
1422
+ const existing = gs.calendars.findBy("user_email", input.user_email).find((calendar) => calendar.google_id === calendarId);
1423
+ if (existing) return existing;
1424
+ const inserted = gs.calendars.insert({
1425
+ google_id: calendarId,
1426
+ user_email: input.user_email,
1427
+ summary: input.summary,
1428
+ description: input.description ?? null,
1429
+ time_zone: input.time_zone ?? "UTC",
1430
+ primary: input.primary ?? false,
1431
+ selected: input.selected ?? true,
1432
+ access_role: input.access_role ?? "owner",
1433
+ background_color: input.background_color ?? null,
1434
+ foreground_color: input.foreground_color ?? null
1435
+ });
1436
+ if (inserted.primary) {
1437
+ for (const calendar of gs.calendars.findBy("user_email", input.user_email)) {
1438
+ if (calendar.id !== inserted.id && calendar.primary) {
1439
+ gs.calendars.update(calendar.id, { primary: false });
1440
+ }
1441
+ }
1442
+ }
1443
+ return inserted;
1444
+ }
1445
+ function listCalendarsForUser(gs, userEmail) {
1446
+ ensureDefaultCalendars(gs, userEmail);
1447
+ return gs.calendars.findBy("user_email", userEmail).sort((a, b) => Number(b.primary) - Number(a.primary) || a.summary.localeCompare(b.summary));
1448
+ }
1449
+ function getCalendarById(gs, userEmail, calendarId) {
1450
+ ensureDefaultCalendars(gs, userEmail);
1451
+ if (calendarId === "primary") {
1452
+ const calendars = listCalendarsForUser(gs, userEmail);
1453
+ return calendars.find((calendar) => calendar.primary) ?? calendars[0];
1454
+ }
1455
+ return gs.calendars.findBy("user_email", userEmail).find((calendar) => calendar.google_id === calendarId);
1456
+ }
1457
+ function formatCalendarResource(calendar) {
1458
+ return {
1459
+ kind: "calendar#calendarListEntry",
1460
+ etag: `"${calendar.google_id}"`,
1461
+ id: calendar.google_id,
1462
+ summary: calendar.summary,
1463
+ description: calendar.description ?? void 0,
1464
+ timeZone: calendar.time_zone,
1465
+ selected: calendar.selected,
1466
+ primary: calendar.primary || void 0,
1467
+ accessRole: calendar.access_role,
1468
+ backgroundColor: calendar.background_color ?? void 0,
1469
+ foregroundColor: calendar.foreground_color ?? void 0
1470
+ };
1471
+ }
1472
+ function createCalendarEventRecord(gs, input) {
1473
+ const calendar = getCalendarById(gs, input.user_email, input.calendar_google_id);
1474
+ if (!calendar) {
1475
+ throw new Error("Calendar not found");
1476
+ }
1477
+ const eventId = input.google_id ?? generateUid("evt");
1478
+ const existing = gs.calendarEvents.findBy("user_email", input.user_email).find((event) => event.google_id === eventId);
1479
+ if (existing) return existing;
1480
+ const hangoutLink = input.hangout_link ?? input.conference_entry_points?.find((entry) => entry.entry_point_type === "video")?.uri ?? null;
1481
+ return gs.calendarEvents.insert({
1482
+ google_id: eventId,
1483
+ user_email: input.user_email,
1484
+ calendar_google_id: calendar.google_id,
1485
+ status: input.status ?? "confirmed",
1486
+ summary: input.summary ?? "Untitled Event",
1487
+ description: input.description ?? null,
1488
+ location: input.location ?? null,
1489
+ html_link: buildCalendarEventLink(calendar.google_id, eventId),
1490
+ hangout_link: hangoutLink,
1491
+ start_date_time: input.start_date_time ?? null,
1492
+ start_date: input.start_date ?? null,
1493
+ end_date_time: input.end_date_time ?? null,
1494
+ end_date: input.end_date ?? null,
1495
+ attendees: input.attendees ?? [],
1496
+ conference_entry_points: input.conference_entry_points ?? [],
1497
+ transparency: input.transparency ?? null
1498
+ });
1499
+ }
1500
+ function getCalendarEventById(gs, userEmail, calendarId, eventId) {
1501
+ const calendar = getCalendarById(gs, userEmail, calendarId);
1502
+ if (!calendar) return void 0;
1503
+ return gs.calendarEvents.findBy("user_email", userEmail).find((event) => event.calendar_google_id === calendar.google_id && event.google_id === eventId);
1504
+ }
1505
+ function deleteCalendarEventRecord(gs, event) {
1506
+ return gs.calendarEvents.delete(event.id);
1507
+ }
1508
+ function listCalendarEvents(gs, userEmail, calendarId, options) {
1509
+ const calendar = getCalendarById(gs, userEmail, calendarId);
1510
+ if (!calendar) return { items: [] };
1511
+ let events = gs.calendarEvents.findBy("user_email", userEmail).filter((event) => event.calendar_google_id === calendar.google_id).filter((event) => event.status !== "cancelled");
1512
+ if (options.timeMin || options.timeMax) {
1513
+ const min = options.timeMin ? Date.parse(options.timeMin) : null;
1514
+ const max = options.timeMax ? Date.parse(options.timeMax) : null;
1515
+ events = events.filter((event) => eventOverlapsRange(event, min, max));
1516
+ }
1517
+ if (options.q?.trim()) {
1518
+ const needle = options.q.trim().toLowerCase();
1519
+ events = events.filter((event) => searchableCalendarEvent(event).includes(needle));
1520
+ }
1521
+ events.sort((a, b) => getEventSortTime(a) - getEventSortTime(b));
1522
+ if (options.orderBy && options.orderBy !== "startTime") {
1523
+ events.sort((a, b) => a.summary.localeCompare(b.summary));
1524
+ }
1525
+ const offset = parseOffset(options.pageToken);
1526
+ const limit = normalizeLimit(options.maxResults, 10, 250);
1527
+ return {
1528
+ items: events.slice(offset, offset + limit),
1529
+ nextPageToken: offset + limit < events.length ? String(offset + limit) : void 0
1530
+ };
1531
+ }
1532
+ function formatCalendarEventResource(gs, event) {
1533
+ const calendar = getCalendarById(gs, event.user_email, event.calendar_google_id);
1534
+ return {
1535
+ kind: "calendar#event",
1536
+ etag: `"${event.google_id}"`,
1537
+ id: event.google_id,
1538
+ status: event.status,
1539
+ htmlLink: event.html_link ?? void 0,
1540
+ hangoutLink: event.hangout_link ?? void 0,
1541
+ summary: event.summary,
1542
+ description: event.description ?? void 0,
1543
+ location: event.location ?? void 0,
1544
+ created: event.created_at,
1545
+ updated: event.updated_at,
1546
+ start: formatCalendarDateRange(event, "start", calendar?.time_zone ?? "UTC"),
1547
+ end: formatCalendarDateRange(event, "end", calendar?.time_zone ?? "UTC"),
1548
+ attendees: event.attendees.map((attendee) => ({
1549
+ email: attendee.email,
1550
+ displayName: attendee.display_name ?? void 0,
1551
+ responseStatus: attendee.response_status ?? void 0,
1552
+ organizer: attendee.organizer || void 0,
1553
+ self: attendee.self || void 0
1554
+ })),
1555
+ conferenceData: event.conference_entry_points.length > 0 ? {
1556
+ entryPoints: event.conference_entry_points.map((entry) => ({
1557
+ entryPointType: entry.entry_point_type,
1558
+ uri: entry.uri,
1559
+ label: entry.label ?? void 0
1560
+ }))
1561
+ } : void 0
1562
+ };
1563
+ }
1564
+ function buildFreeBusyResponse(gs, userEmail, request) {
1565
+ const calendars = {};
1566
+ const min = Date.parse(request.timeMin);
1567
+ const max = Date.parse(request.timeMax);
1568
+ for (const item of request.items) {
1569
+ const calendar = getCalendarById(gs, userEmail, item.id);
1570
+ if (!calendar) continue;
1571
+ 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) => ({
1572
+ start: event.start_date_time ?? `${event.start_date}T00:00:00.000Z`,
1573
+ end: event.end_date_time ?? `${event.end_date}T00:00:00.000Z`
1574
+ }));
1575
+ calendars[item.id] = { busy };
1576
+ }
1577
+ return {
1578
+ kind: "calendar#freeBusy",
1579
+ timeMin: request.timeMin,
1580
+ timeMax: request.timeMax,
1581
+ calendars
1582
+ };
1583
+ }
1584
+ function buildCalendarEventLink(calendarId, eventId) {
1585
+ return `https://calendar.google.com/calendar/u/0/r/eventedit/${calendarId}/${eventId}`;
1586
+ }
1587
+ function formatCalendarDateRange(event, prefix, timeZone) {
1588
+ const dateTime = prefix === "start" ? event.start_date_time : event.end_date_time;
1589
+ const date = prefix === "start" ? event.start_date : event.end_date;
1590
+ if (dateTime) {
1591
+ return {
1592
+ dateTime,
1593
+ timeZone
1594
+ };
1595
+ }
1596
+ return {
1597
+ date: date ?? void 0,
1598
+ timeZone
1599
+ };
1600
+ }
1601
+ function searchableCalendarEvent(event) {
1602
+ return [
1603
+ event.summary,
1604
+ event.description ?? "",
1605
+ event.location ?? "",
1606
+ ...event.attendees.map((attendee) => attendee.email),
1607
+ ...event.attendees.map((attendee) => attendee.display_name ?? "")
1608
+ ].join(" ").toLowerCase();
1609
+ }
1610
+ function eventOverlapsRange(event, min, max) {
1611
+ const start = getEventSortTime(event);
1612
+ const end = getEventEndTime(event);
1613
+ if (min != null && end <= min) return false;
1614
+ if (max != null && start >= max) return false;
1615
+ return true;
1616
+ }
1617
+ function getEventSortTime(event) {
1618
+ return parseCalendarTimestamp(event.start_date_time, event.start_date);
1619
+ }
1620
+ function getEventEndTime(event) {
1621
+ return parseCalendarTimestamp(event.end_date_time, event.end_date);
1622
+ }
1623
+ function parseCalendarTimestamp(dateTime, date) {
1624
+ if (dateTime) {
1625
+ const parsed = Date.parse(dateTime);
1626
+ if (Number.isFinite(parsed)) return parsed;
1627
+ }
1628
+ if (date) {
1629
+ const parsed = Date.parse(`${date}T00:00:00.000Z`);
1630
+ if (Number.isFinite(parsed)) return parsed;
1631
+ }
1632
+ return Date.now();
1633
+ }
1634
+ var GOOGLE_DRIVE_FOLDER_MIME_TYPE = "application/vnd.google-apps.folder";
1635
+ function createDriveItemRecord(gs, input) {
1636
+ const itemId = input.google_id ?? generateUid("drv");
1637
+ const existing = gs.driveItems.findBy("user_email", input.user_email).find((item2) => item2.google_id === itemId);
1638
+ if (existing) return existing;
1639
+ const item = gs.driveItems.insert({
1640
+ google_id: itemId,
1641
+ user_email: input.user_email,
1642
+ name: input.name,
1643
+ mime_type: input.mime_type,
1644
+ parent_google_ids: normalizeParentIds(input.parent_google_ids),
1645
+ web_view_link: input.web_view_link ?? buildDriveWebViewLink(itemId, input.mime_type),
1646
+ size: input.size ?? null,
1647
+ trashed: input.trashed ?? false,
1648
+ data: input.data ?? null
1649
+ });
1650
+ return item;
1651
+ }
1652
+ function getDriveItemById(gs, userEmail, fileId) {
1653
+ return gs.driveItems.findBy("user_email", userEmail).find((item) => item.google_id === fileId);
1654
+ }
1655
+ function listDriveItems(gs, userEmail, options) {
1656
+ let items = gs.driveItems.findBy("user_email", userEmail);
1657
+ const parsed = parseDriveQuery(options.q ?? null);
1658
+ if (parsed.parentId) {
1659
+ items = items.filter((item) => item.parent_google_ids.includes(parsed.parentId));
1660
+ }
1661
+ if (parsed.requireNotTrashed) {
1662
+ items = items.filter((item) => !item.trashed);
1663
+ }
1664
+ if (parsed.mimeTypes.length > 0) {
1665
+ items = items.filter((item) => parsed.mimeTypes.includes(item.mime_type));
1666
+ }
1667
+ if (parsed.excludeMimeTypes.length > 0) {
1668
+ items = items.filter((item) => !parsed.excludeMimeTypes.includes(item.mime_type));
1669
+ }
1670
+ if (options.orderBy?.includes("name")) {
1671
+ items = items.sort((a, b) => a.name.localeCompare(b.name));
1672
+ } else {
1673
+ items = items.sort((a, b) => a.created_at.localeCompare(b.created_at));
1674
+ }
1675
+ const offset = parseOffset(options.pageToken);
1676
+ const limit = normalizeLimit(options.pageSize, 100, 1e3);
1677
+ return {
1678
+ files: items.slice(offset, offset + limit),
1679
+ nextPageToken: offset + limit < items.length ? String(offset + limit) : void 0
1680
+ };
1681
+ }
1682
+ function updateDriveItemRecord(gs, item, input) {
1683
+ const nextParents = new Set(item.parent_google_ids);
1684
+ for (const parentId of input.addParents ?? []) {
1685
+ nextParents.add(parentId);
1686
+ }
1687
+ for (const parentId of input.removeParents ?? []) {
1688
+ nextParents.delete(parentId);
1689
+ }
1690
+ return gs.driveItems.update(item.id, {
1691
+ name: input.name ?? item.name,
1692
+ parent_google_ids: normalizeParentIds(Array.from(nextParents)),
1693
+ trashed: input.trashed ?? item.trashed,
1694
+ web_view_link: buildDriveWebViewLink(item.google_id, item.mime_type)
1695
+ }) ?? item;
1696
+ }
1697
+ function formatDriveItemResource(item) {
1698
+ return {
1699
+ kind: "drive#file",
1700
+ id: item.google_id,
1701
+ name: item.name,
1702
+ mimeType: item.mime_type,
1703
+ parents: item.parent_google_ids,
1704
+ webViewLink: item.web_view_link ?? void 0,
1705
+ createdTime: item.created_at,
1706
+ modifiedTime: item.updated_at,
1707
+ size: item.size != null ? String(item.size) : void 0,
1708
+ trashed: item.trashed || void 0
1709
+ };
1710
+ }
1711
+ function parseDriveMultipartUpload(contentType, rawBody) {
1712
+ const boundaryMatch = contentType.match(/boundary="?([^";]+)"?/i);
1713
+ const boundary = boundaryMatch?.[1];
1714
+ if (!boundary) {
1715
+ return {
1716
+ requestBody: {},
1717
+ media: void 0
1718
+ };
1719
+ }
1720
+ const raw = rawBody.toString("latin1");
1721
+ const parts = raw.split(`--${boundary}`).slice(1).filter((part) => part !== "--" && part !== "--\r\n" && part !== "--\n");
1722
+ let requestBody = {};
1723
+ let media;
1724
+ for (const part of parts) {
1725
+ const normalized = stripMultipartBoundaryPadding(part);
1726
+ const headerSeparator = normalized.includes("\r\n\r\n") ? "\r\n\r\n" : "\n\n";
1727
+ const separatorIndex = normalized.indexOf(headerSeparator);
1728
+ if (separatorIndex < 0) continue;
1729
+ const headers = normalized.slice(0, separatorIndex).toLowerCase();
1730
+ const bodyText = normalized.slice(separatorIndex + headerSeparator.length);
1731
+ if (headers.includes("application/json")) {
1732
+ try {
1733
+ const parsed = JSON.parse(bodyText);
1734
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1735
+ requestBody = parsed;
1736
+ }
1737
+ } catch {
1738
+ requestBody = {};
1739
+ }
1740
+ continue;
1741
+ }
1742
+ const mimeTypeMatch = headers.match(/content-type:\s*([^\r\n;]+)/i);
1743
+ media = {
1744
+ mimeType: mimeTypeMatch?.[1]?.trim() ?? "application/octet-stream",
1745
+ body: Buffer.from(bodyText, "latin1")
1746
+ };
1747
+ }
1748
+ return {
1749
+ requestBody,
1750
+ media
1751
+ };
1752
+ }
1753
+ function parseDriveQuery(query) {
1754
+ const source = query ?? "";
1755
+ const parentMatch = source.match(/'([^']+)' in parents/i);
1756
+ const mimeTypes = Array.from(source.matchAll(/mimeType = '([^']+)'/g)).map((match) => match[1]);
1757
+ const excludeMimeTypes = Array.from(source.matchAll(/mimeType != '([^']+)'/g)).map((match) => match[1]);
1758
+ return {
1759
+ parentId: parentMatch?.[1] ?? null,
1760
+ mimeTypes,
1761
+ excludeMimeTypes,
1762
+ requireNotTrashed: source.includes("trashed = false")
1763
+ };
1764
+ }
1765
+ function buildDriveWebViewLink(itemId, mimeType) {
1766
+ if (mimeType === GOOGLE_DRIVE_FOLDER_MIME_TYPE) {
1767
+ return `https://drive.google.com/drive/folders/${itemId}`;
1768
+ }
1769
+ return `https://drive.google.com/file/d/${itemId}/view`;
1770
+ }
1771
+ function normalizeParentIds(parentIds) {
1772
+ const normalized = [...new Set((parentIds ?? ["root"]).filter(Boolean))];
1773
+ return normalized.length > 0 ? normalized : ["root"];
1774
+ }
1775
+ function stripMultipartBoundaryPadding(part) {
1776
+ let normalized = part;
1777
+ if (normalized.startsWith("\r\n")) {
1778
+ normalized = normalized.slice(2);
1779
+ } else if (normalized.startsWith("\n")) {
1780
+ normalized = normalized.slice(1);
1781
+ }
1782
+ if (normalized.endsWith("\r\n")) {
1783
+ normalized = normalized.slice(0, -2);
1784
+ } else if (normalized.endsWith("\n")) {
1785
+ normalized = normalized.slice(0, -1);
1786
+ }
1787
+ return normalized;
1788
+ }
1789
+ function requireGoogleAuth(c) {
1790
+ const authEmail = getAuthenticatedEmail(c);
1791
+ if (!authEmail) {
1792
+ return googleApiError(c, 401, "Request had invalid authentication credentials.", "authError", "UNAUTHENTICATED");
1793
+ }
1794
+ return authEmail;
1795
+ }
1796
+ function requireGmailUser(c) {
1797
+ const authEmail = requireGoogleAuth(c);
1798
+ if (authEmail instanceof Response) {
1799
+ return authEmail;
1800
+ }
1801
+ if (!matchesRequestedUser(c.req.param("userId"), authEmail)) {
1802
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
1803
+ }
1804
+ return authEmail;
1805
+ }
1806
+ async function parseGoogleBody(c) {
1807
+ const contentType = c.req.header("Content-Type") ?? "";
1808
+ const rawText = await c.req.text();
1809
+ if (!rawText) return {};
1810
+ let parsed;
1811
+ if (contentType.includes("application/json")) {
1812
+ try {
1813
+ const json = JSON.parse(rawText);
1814
+ parsed = json && typeof json === "object" && !Array.isArray(json) ? json : {};
1815
+ } catch {
1816
+ return {};
1817
+ }
1818
+ } else if (contentType.includes("application/x-www-form-urlencoded")) {
1819
+ parsed = Object.fromEntries(new URLSearchParams(rawText));
1820
+ } else {
1821
+ parsed = {
1822
+ raw: Buffer.from(rawText, "utf8").toString("base64url")
1823
+ };
1824
+ }
1825
+ const nestedBody = parsed.requestBody;
1826
+ if (nestedBody && typeof nestedBody === "object" && !Array.isArray(nestedBody)) {
1827
+ return nestedBody;
1828
+ }
1829
+ return parsed;
1830
+ }
1831
+ function getStringArray(body, field) {
1832
+ const value = body[field];
1833
+ if (Array.isArray(value)) {
1834
+ return value.filter((item) => typeof item === "string" && item.length > 0);
1835
+ }
1836
+ if (typeof value === "string" && value.length > 0) {
1837
+ return [value];
1838
+ }
1839
+ return [];
1840
+ }
1841
+ function getString(body, ...fields) {
1842
+ for (const field of fields) {
1843
+ const value = body[field];
1844
+ if (typeof value === "string") return value;
1845
+ }
1846
+ return void 0;
1847
+ }
1848
+ function getRecord(body, ...fields) {
1849
+ for (const field of fields) {
1850
+ const value = body[field];
1851
+ if (value && typeof value === "object" && !Array.isArray(value)) {
1852
+ return value;
1853
+ }
1854
+ }
1855
+ return void 0;
1856
+ }
1857
+ function getRecordArray(body, ...fields) {
1858
+ for (const field of fields) {
1859
+ const value = body[field];
1860
+ if (!Array.isArray(value)) continue;
1861
+ return value.filter(
1862
+ (item) => Boolean(item) && typeof item === "object" && !Array.isArray(item)
1863
+ );
1864
+ }
1865
+ return [];
1866
+ }
1867
+ function parseMessageInputFromBody(body, defaults) {
1868
+ return {
1869
+ raw: getString(body, "raw"),
1870
+ thread_id: getString(body, "threadId", "thread_id"),
1871
+ from: getString(body, "from") ?? defaults?.from,
1872
+ to: getString(body, "to"),
1873
+ cc: getString(body, "cc") ?? null,
1874
+ bcc: getString(body, "bcc") ?? null,
1875
+ reply_to: getString(body, "replyTo", "reply_to") ?? null,
1876
+ subject: getString(body, "subject"),
1877
+ snippet: getString(body, "snippet"),
1878
+ body_text: getString(body, "body_text", "text") ?? null,
1879
+ body_html: getString(body, "body_html", "html") ?? null,
1880
+ date: getString(body, "date"),
1881
+ internal_date: getString(body, "internalDate", "internal_date"),
1882
+ message_id: getString(body, "messageId", "message_id"),
1883
+ references: getString(body, "references") ?? null,
1884
+ in_reply_to: getString(body, "inReplyTo", "in_reply_to") ?? null
1885
+ };
1886
+ }
1887
+ function parseCalendarEventInputFromBody(body) {
1888
+ const start = getRecord(body, "start");
1889
+ const end = getRecord(body, "end");
1890
+ const conferenceData = getRecord(body, "conferenceData");
1891
+ const conferenceEntryPoints = getRecordArray(conferenceData ?? {}, "entryPoints").map((entry) => ({
1892
+ entry_point_type: getString(entry, "entryPointType") ?? "video",
1893
+ uri: getString(entry, "uri") ?? "",
1894
+ label: getString(entry, "label") ?? null
1895
+ })).filter((entry) => entry.uri.length > 0);
1896
+ return {
1897
+ status: getString(body, "status") ?? "confirmed",
1898
+ summary: getString(body, "summary"),
1899
+ description: getString(body, "description") ?? null,
1900
+ location: getString(body, "location") ?? null,
1901
+ start_date_time: getString(start ?? {}, "dateTime") ?? null,
1902
+ start_date: getString(start ?? {}, "date") ?? null,
1903
+ end_date_time: getString(end ?? {}, "dateTime") ?? null,
1904
+ end_date: getString(end ?? {}, "date") ?? null,
1905
+ attendees: getRecordArray(body, "attendees").map((entry) => ({
1906
+ email: getString(entry, "email") ?? "",
1907
+ display_name: getString(entry, "displayName") ?? null,
1908
+ response_status: getString(entry, "responseStatus") ?? null,
1909
+ organizer: entry.organizer === true,
1910
+ self: entry.self === true
1911
+ })).filter((attendee) => attendee.email.length > 0),
1912
+ conference_entry_points: conferenceEntryPoints,
1913
+ hangout_link: getString(body, "hangoutLink") ?? conferenceEntryPoints.find((entry) => entry.entry_point_type === "video")?.uri ?? null,
1914
+ transparency: getString(body, "transparency") ?? null
1915
+ };
1916
+ }
1917
+ function parseDriveItemInputFromBody(body, defaults) {
1918
+ const parentIds = getStringArray(body, "parents");
1919
+ return {
1920
+ name: getString(body, "name")?.trim() || "Untitled",
1921
+ mime_type: getString(body, "mimeType") ?? defaults?.mimeType ?? "application/octet-stream",
1922
+ parent_google_ids: parentIds.length > 0 ? parentIds : ["root"]
1923
+ };
1924
+ }
1925
+ function getGoogleStore(store) {
1926
+ return {
1927
+ users: store.collection("google.users", ["uid", "email"]),
1928
+ oauthClients: store.collection("google.oauth_clients", ["client_id"]),
1929
+ messages: store.collection("google.messages", ["gmail_id", "thread_id", "user_email"]),
1930
+ drafts: store.collection("google.drafts", ["gmail_id", "message_gmail_id", "user_email"]),
1931
+ attachments: store.collection("google.attachments", ["gmail_id", "message_gmail_id", "user_email"]),
1932
+ history: store.collection("google.history", ["gmail_id", "message_gmail_id", "user_email"]),
1933
+ labels: store.collection("google.labels", ["gmail_id", "user_email", "name"]),
1934
+ filters: store.collection("google.filters", ["gmail_id", "user_email"]),
1935
+ forwardingAddresses: store.collection("google.forwarding_addresses", ["user_email", "forwarding_email"]),
1936
+ sendAs: store.collection("google.send_as", ["user_email", "send_as_email"]),
1937
+ calendars: store.collection("google.calendars", ["google_id", "user_email"]),
1938
+ calendarEvents: store.collection("google.calendar_events", ["google_id", "calendar_google_id", "user_email"]),
1939
+ driveItems: store.collection("google.drive_items", ["google_id", "user_email", "mime_type"])
1940
+ };
1941
+ }
1942
+ function calendarRoutes({ app, store }) {
1943
+ const gs = getGoogleStore(store);
1944
+ app.get("/calendar/v3/users/:userId/calendarList", (c) => {
1945
+ const authEmail = requireGmailUser(c);
1946
+ if (authEmail instanceof Response) return authEmail;
1947
+ return c.json({
1948
+ kind: "calendar#calendarList",
1949
+ items: listCalendarsForUser(gs, authEmail).map((calendar) => formatCalendarResource(calendar))
1950
+ });
1951
+ });
1952
+ app.get("/calendar/v3/calendars/:calendarId/events", (c) => {
1953
+ const authEmail = requireGoogleAuth(c);
1954
+ if (authEmail instanceof Response) return authEmail;
1955
+ const calendar = getCalendarById(gs, authEmail, c.req.param("calendarId"));
1956
+ if (!calendar) {
1957
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
1958
+ }
1959
+ const url = new URL(c.req.url);
1960
+ const response = listCalendarEvents(gs, authEmail, calendar.google_id, {
1961
+ timeMin: url.searchParams.get("timeMin"),
1962
+ timeMax: url.searchParams.get("timeMax"),
1963
+ maxResults: url.searchParams.get("maxResults"),
1964
+ pageToken: url.searchParams.get("pageToken"),
1965
+ q: url.searchParams.get("q"),
1966
+ orderBy: url.searchParams.get("orderBy")
1967
+ });
1968
+ return c.json({
1969
+ kind: "calendar#events",
1970
+ items: response.items.map((event) => formatCalendarEventResource(gs, event)),
1971
+ nextPageToken: response.nextPageToken
1972
+ });
1973
+ });
1974
+ app.post("/calendar/v3/calendars/:calendarId/events", async (c) => {
1975
+ const authEmail = requireGoogleAuth(c);
1976
+ if (authEmail instanceof Response) return authEmail;
1977
+ const calendar = getCalendarById(gs, authEmail, c.req.param("calendarId"));
1978
+ if (!calendar) {
1979
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
1980
+ }
1981
+ const body = await parseGoogleBody(c);
1982
+ const requestBody = getRecord(body, "requestBody") ?? body;
1983
+ const eventInput = parseCalendarEventInputFromBody(requestBody);
1984
+ if (!eventInput.start_date_time && !eventInput.start_date || !eventInput.end_date_time && !eventInput.end_date) {
1985
+ return googleApiError(c, 400, "Event start and end are required.", "invalidArgument", "INVALID_ARGUMENT");
1986
+ }
1987
+ const event = createCalendarEventRecord(gs, {
1988
+ user_email: authEmail,
1989
+ calendar_google_id: calendar.google_id,
1990
+ ...eventInput
1991
+ });
1992
+ return c.json(formatCalendarEventResource(gs, event));
1993
+ });
1994
+ app.delete("/calendar/v3/calendars/:calendarId/events/:eventId", (c) => {
1995
+ const authEmail = requireGoogleAuth(c);
1996
+ if (authEmail instanceof Response) return authEmail;
1997
+ const event = getCalendarEventById(gs, authEmail, c.req.param("calendarId"), c.req.param("eventId"));
1998
+ if (!event) {
1999
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2000
+ }
2001
+ deleteCalendarEventRecord(gs, event);
2002
+ return c.body(null, 204);
2003
+ });
2004
+ app.post("/calendar/v3/freeBusy", async (c) => {
2005
+ const authEmail = requireGoogleAuth(c);
2006
+ if (authEmail instanceof Response) return authEmail;
2007
+ const body = await parseGoogleBody(c);
2008
+ const requestBody = getRecord(body, "requestBody") ?? body;
2009
+ const timeMin = typeof requestBody.timeMin === "string" ? requestBody.timeMin : void 0;
2010
+ const timeMax = typeof requestBody.timeMax === "string" ? requestBody.timeMax : void 0;
2011
+ const items = getRecordArray(requestBody, "items").map((entry) => ({
2012
+ id: typeof entry.id === "string" ? entry.id : ""
2013
+ })).filter((entry) => entry.id.length > 0);
2014
+ if (!timeMin || !timeMax) {
2015
+ return googleApiError(c, 400, "timeMin and timeMax are required.", "invalidArgument", "INVALID_ARGUMENT");
2016
+ }
2017
+ return c.json(
2018
+ buildFreeBusyResponse(gs, authEmail, {
2019
+ timeMin,
2020
+ timeMax,
2021
+ items
2022
+ })
2023
+ );
2024
+ });
2025
+ }
2026
+ function draftRoutes({ app, store }) {
2027
+ const gs = getGoogleStore(store);
2028
+ const createHandler = async (c) => {
2029
+ const authEmail = requireGmailUser(c);
2030
+ if (authEmail instanceof Response) return authEmail;
2031
+ const body = await parseGoogleBody(c);
2032
+ const messageBody = getRecord(body, "message") ?? body;
2033
+ try {
2034
+ const { draft } = createDraftMessage(gs, {
2035
+ user_email: authEmail,
2036
+ ...parseMessageInputFromBody(messageBody, { from: authEmail })
2037
+ });
2038
+ return c.json(formatDraftResource(gs, draft, "full"));
2039
+ } catch {
2040
+ return googleApiError(
2041
+ c,
2042
+ 400,
2043
+ "Invalid raw MIME message payload.",
2044
+ "invalidArgument",
2045
+ "INVALID_ARGUMENT"
2046
+ );
2047
+ }
2048
+ };
2049
+ const sendHandler = async (c) => {
2050
+ const authEmail = requireGmailUser(c);
2051
+ if (authEmail instanceof Response) return authEmail;
2052
+ const body = await parseGoogleBody(c);
2053
+ const draftId = getString(body, "id") ?? getString(getRecord(body, "draft") ?? {}, "id");
2054
+ if (!draftId) {
2055
+ return googleApiError(c, 400, "Draft ID is required.", "invalidArgument", "INVALID_ARGUMENT");
2056
+ }
2057
+ const draft = getDraftById(gs, authEmail, draftId);
2058
+ if (!draft) {
2059
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2060
+ }
2061
+ const message = sendDraftMessage(gs, draft);
2062
+ if (!message) {
2063
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2064
+ }
2065
+ return c.json({
2066
+ id: message.gmail_id,
2067
+ threadId: message.thread_id,
2068
+ labelIds: message.label_ids,
2069
+ snippet: message.snippet,
2070
+ historyId: message.history_id,
2071
+ internalDate: message.internal_date
2072
+ });
2073
+ };
2074
+ app.get("/gmail/v1/users/:userId/drafts", (c) => {
2075
+ const authEmail = requireGmailUser(c);
2076
+ if (authEmail instanceof Response) return authEmail;
2077
+ const drafts = listDraftsForUser(gs, authEmail);
2078
+ const url = new URL(c.req.url);
2079
+ const offset = parseOffset(url.searchParams.get("pageToken"));
2080
+ const limit = normalizeLimit(url.searchParams.get("maxResults"), 100, 500);
2081
+ const page = drafts.slice(offset, offset + limit);
2082
+ const nextPageToken = offset + limit < drafts.length ? String(offset + limit) : void 0;
2083
+ return c.json({
2084
+ drafts: page.map((draft) => {
2085
+ const resource = formatDraftResource(gs, draft, "minimal");
2086
+ return {
2087
+ id: resource.id,
2088
+ message: resource.message ? {
2089
+ id: resource.message.id,
2090
+ threadId: resource.message.threadId
2091
+ } : void 0
2092
+ };
2093
+ }),
2094
+ nextPageToken,
2095
+ resultSizeEstimate: drafts.length
2096
+ });
2097
+ });
2098
+ app.post("/gmail/v1/users/:userId/drafts", createHandler);
2099
+ app.post("/upload/gmail/v1/users/:userId/drafts", createHandler);
2100
+ app.get("/gmail/v1/users/:userId/drafts/:id", (c) => {
2101
+ const authEmail = requireGmailUser(c);
2102
+ if (authEmail instanceof Response) return authEmail;
2103
+ const draft = getDraftById(gs, authEmail, c.req.param("id"));
2104
+ if (!draft) {
2105
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2106
+ }
2107
+ if (!getDraftMessage(gs, draft)) {
2108
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2109
+ }
2110
+ const url = new URL(c.req.url);
2111
+ return c.json(
2112
+ formatDraftResource(
2113
+ gs,
2114
+ draft,
2115
+ parseFormat(url.searchParams.get("format")),
2116
+ url.searchParams.getAll("metadataHeaders")
2117
+ )
2118
+ );
2119
+ });
2120
+ app.put("/gmail/v1/users/:userId/drafts/:id", async (c) => {
2121
+ const authEmail = requireGmailUser(c);
2122
+ if (authEmail instanceof Response) return authEmail;
2123
+ const draft = getDraftById(gs, authEmail, c.req.param("id"));
2124
+ if (!draft) {
2125
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2126
+ }
2127
+ const body = await parseGoogleBody(c);
2128
+ const messageBody = getRecord(body, "message") ?? body;
2129
+ try {
2130
+ const updated = updateDraftMessage(gs, draft, parseMessageInputFromBody(messageBody));
2131
+ if (!updated) {
2132
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2133
+ }
2134
+ return c.json(formatDraftResource(gs, updated.draft, "full"));
2135
+ } catch {
2136
+ return googleApiError(
2137
+ c,
2138
+ 400,
2139
+ "Invalid raw MIME message payload.",
2140
+ "invalidArgument",
2141
+ "INVALID_ARGUMENT"
2142
+ );
2143
+ }
2144
+ });
2145
+ app.post("/gmail/v1/users/:userId/drafts/send", sendHandler);
2146
+ app.post("/upload/gmail/v1/users/:userId/drafts/send", sendHandler);
2147
+ app.delete("/gmail/v1/users/:userId/drafts/:id", (c) => {
2148
+ const authEmail = requireGmailUser(c);
2149
+ if (authEmail instanceof Response) return authEmail;
2150
+ const draft = getDraftById(gs, authEmail, c.req.param("id"));
2151
+ if (!draft) {
2152
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2153
+ }
2154
+ deleteDraftMessage(gs, draft);
2155
+ return c.body(null, 204);
2156
+ });
2157
+ }
2158
+ function driveRoutes({ app, store }) {
2159
+ const gs = getGoogleStore(store);
2160
+ const createHandler = async (c) => {
2161
+ const authEmail = requireGoogleAuth(c);
2162
+ if (authEmail instanceof Response) return authEmail;
2163
+ const contentType = c.req.header("Content-Type") ?? "";
2164
+ let requestBody = {};
2165
+ let media;
2166
+ if (contentType.includes("multipart/related")) {
2167
+ const rawBody = Buffer.from(await c.req.raw.arrayBuffer());
2168
+ const parsed = parseDriveMultipartUpload(contentType, rawBody);
2169
+ requestBody = parsed.requestBody;
2170
+ media = parsed.media;
2171
+ } else {
2172
+ const body = await parseGoogleBody(c);
2173
+ requestBody = getRecord(body, "requestBody") ?? body;
2174
+ }
2175
+ const item = createDriveItemRecord(gs, {
2176
+ user_email: authEmail,
2177
+ ...parseDriveItemInputFromBody(requestBody, {
2178
+ mimeType: media?.mimeType
2179
+ }),
2180
+ size: media ? media.body.length : null,
2181
+ data: media ? media.body.toString("base64url") : null
2182
+ });
2183
+ return c.json(formatDriveItemResource(item));
2184
+ };
2185
+ app.get("/drive/v3/files", (c) => {
2186
+ const authEmail = requireGoogleAuth(c);
2187
+ if (authEmail instanceof Response) return authEmail;
2188
+ const url = new URL(c.req.url);
2189
+ const response = listDriveItems(gs, authEmail, {
2190
+ q: url.searchParams.get("q"),
2191
+ pageSize: url.searchParams.get("pageSize"),
2192
+ pageToken: url.searchParams.get("pageToken"),
2193
+ orderBy: url.searchParams.get("orderBy")
2194
+ });
2195
+ return c.json({
2196
+ kind: "drive#fileList",
2197
+ files: response.files.map((item) => formatDriveItemResource(item)),
2198
+ nextPageToken: response.nextPageToken
2199
+ });
2200
+ });
2201
+ app.post("/drive/v3/files", createHandler);
2202
+ app.post("/upload/drive/v3/files", createHandler);
2203
+ app.get("/drive/v3/files/:fileId", (c) => {
2204
+ const authEmail = requireGoogleAuth(c);
2205
+ if (authEmail instanceof Response) return authEmail;
2206
+ const item = getDriveItemById(gs, authEmail, c.req.param("fileId"));
2207
+ if (!item) {
2208
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2209
+ }
2210
+ const url = new URL(c.req.url);
2211
+ if (url.searchParams.get("alt") === "media") {
2212
+ return new Response(item.data ? Buffer.from(item.data, "base64url") : Buffer.alloc(0), {
2213
+ status: 200,
2214
+ headers: {
2215
+ "Content-Type": item.mime_type
2216
+ }
2217
+ });
2218
+ }
2219
+ return c.json(formatDriveItemResource(item));
2220
+ });
2221
+ const updateHandler = async (c) => {
2222
+ const authEmail = requireGoogleAuth(c);
2223
+ if (authEmail instanceof Response) return authEmail;
2224
+ const item = getDriveItemById(gs, authEmail, c.req.param("fileId"));
2225
+ if (!item) {
2226
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2227
+ }
2228
+ const url = new URL(c.req.url);
2229
+ const body = await parseGoogleBody(c);
2230
+ const requestBody = getRecord(body, "requestBody") ?? body;
2231
+ const addParents = (url.searchParams.get("addParents") ?? "").split(",").map((value) => value.trim()).filter(Boolean);
2232
+ const removeParents = (url.searchParams.get("removeParents") ?? "").split(",").map((value) => value.trim()).filter(Boolean);
2233
+ const updated = updateDriveItemRecord(gs, item, {
2234
+ addParents,
2235
+ removeParents,
2236
+ name: getString(requestBody, "name")
2237
+ });
2238
+ return c.json(formatDriveItemResource(updated));
2239
+ };
2240
+ app.patch("/drive/v3/files/:fileId", updateHandler);
2241
+ app.put("/drive/v3/files/:fileId", updateHandler);
2242
+ }
2243
+ var WATCH_STATE_KEY = "google.gmail.watchStates";
2244
+ function historyRoutes({ app, store }) {
2245
+ const gs = getGoogleStore(store);
2246
+ app.get("/gmail/v1/users/:userId/history", (c) => {
2247
+ const authEmail = requireGmailUser(c);
2248
+ if (authEmail instanceof Response) return authEmail;
2249
+ const url = new URL(c.req.url);
2250
+ const startHistoryId = url.searchParams.get("startHistoryId")?.trim();
2251
+ if (!startHistoryId) {
2252
+ return googleApiError(c, 400, "Start history ID is required.", "invalidArgument", "INVALID_ARGUMENT");
2253
+ }
2254
+ const historyTypes = url.searchParams.getAll("historyTypes").filter(isHistoryChangeType);
2255
+ return c.json(
2256
+ listHistoryForUser(gs, authEmail, {
2257
+ startHistoryId,
2258
+ historyTypes,
2259
+ labelId: url.searchParams.get("labelId") ?? void 0,
2260
+ maxResults: normalizeLimit(url.searchParams.get("maxResults"), 100, 500),
2261
+ pageToken: url.searchParams.get("pageToken")
2262
+ })
2263
+ );
2264
+ });
2265
+ app.post("/gmail/v1/users/:userId/watch", async (c) => {
2266
+ const authEmail = requireGmailUser(c);
2267
+ if (authEmail instanceof Response) return authEmail;
2268
+ const body = await parseGoogleBody(c);
2269
+ const topicName = getString(body, "topicName")?.trim();
2270
+ if (!topicName) {
2271
+ return googleApiError(c, 400, "Topic name is required.", "invalidArgument", "INVALID_ARGUMENT");
2272
+ }
2273
+ const labelIds = getStringArray(body, "labelIds");
2274
+ const missingLabelIds = findMissingLabelIds(gs, authEmail, labelIds);
2275
+ if (missingLabelIds.length > 0) {
2276
+ return googleApiError(c, 400, `Invalid label IDs: ${missingLabelIds.join(", ")}`, "invalidArgument", "INVALID_ARGUMENT");
2277
+ }
2278
+ const expiration = String(Date.now() + 24 * 60 * 60 * 1e3);
2279
+ const states = store.getData(WATCH_STATE_KEY) ?? /* @__PURE__ */ new Map();
2280
+ states.set(authEmail, {
2281
+ topicName,
2282
+ labelIds,
2283
+ labelFilterBehavior: getString(body, "labelFilterBehavior", "labelFilterAction") ?? null,
2284
+ expiration
2285
+ });
2286
+ store.setData(WATCH_STATE_KEY, states);
2287
+ return c.json({
2288
+ historyId: getCurrentHistoryId(gs, authEmail),
2289
+ expiration
2290
+ });
2291
+ });
2292
+ app.post("/gmail/v1/users/:userId/stop", (c) => {
2293
+ const authEmail = requireGmailUser(c);
2294
+ if (authEmail instanceof Response) return authEmail;
2295
+ const states = store.getData(WATCH_STATE_KEY) ?? /* @__PURE__ */ new Map();
2296
+ states.delete(authEmail);
2297
+ store.setData(WATCH_STATE_KEY, states);
2298
+ return c.body(null, 200);
2299
+ });
2300
+ }
2301
+ function labelRoutes({ app, store }) {
2302
+ const gs = getGoogleStore(store);
2303
+ app.get("/gmail/v1/users/:userId/labels", (c) => {
2304
+ const authEmail = requireGmailUser(c);
2305
+ if (authEmail instanceof Response) return authEmail;
2306
+ return c.json({
2307
+ labels: formatLabelResources(gs, listLabelsForUser(gs, authEmail))
2308
+ });
2309
+ });
2310
+ app.get("/gmail/v1/users/:userId/labels/:id", (c) => {
2311
+ const authEmail = requireGmailUser(c);
2312
+ if (authEmail instanceof Response) return authEmail;
2313
+ const label = findLabelById(gs, authEmail, c.req.param("id"));
2314
+ if (!label) {
2315
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2316
+ }
2317
+ return c.json(formatLabelResource(gs, label));
2318
+ });
2319
+ app.post("/gmail/v1/users/:userId/labels", async (c) => {
2320
+ const authEmail = requireGmailUser(c);
2321
+ if (authEmail instanceof Response) return authEmail;
2322
+ const body = await parseGoogleBody(c);
2323
+ const name = getString(body, "name")?.trim();
2324
+ if (!name) {
2325
+ return googleApiError(c, 400, "Invalid label name", "invalidArgument", "INVALID_ARGUMENT");
2326
+ }
2327
+ if (findLabelByName(gs, authEmail, name)) {
2328
+ return googleApiError(
2329
+ c,
2330
+ 400,
2331
+ "Label name exists or conflicts",
2332
+ "failedPrecondition",
2333
+ "FAILED_PRECONDITION"
2334
+ );
2335
+ }
2336
+ const color = body.color && typeof body.color === "object" && !Array.isArray(body.color) ? body.color : void 0;
2337
+ const label = createLabelRecord(gs, {
2338
+ user_email: authEmail,
2339
+ name,
2340
+ type: "user",
2341
+ message_list_visibility: getString(body, "messageListVisibility", "message_list_visibility") ?? "show",
2342
+ label_list_visibility: getString(body, "labelListVisibility", "label_list_visibility") ?? "labelShow",
2343
+ color_background: typeof color?.backgroundColor === "string" ? color.backgroundColor : getString(body, "color_background"),
2344
+ color_text: typeof color?.textColor === "string" ? color.textColor : getString(body, "color_text")
2345
+ });
2346
+ return c.json(formatLabelResource(gs, label));
2347
+ });
2348
+ app.put("/gmail/v1/users/:userId/labels/:id", async (c) => {
2349
+ return saveLabel(c, gs, true);
2350
+ });
2351
+ app.patch("/gmail/v1/users/:userId/labels/:id", async (c) => {
2352
+ return saveLabel(c, gs, false);
2353
+ });
2354
+ app.delete("/gmail/v1/users/:userId/labels/:id", (c) => {
2355
+ const authEmail = requireGmailUser(c);
2356
+ if (authEmail instanceof Response) return authEmail;
2357
+ const label = findLabelById(gs, authEmail, c.req.param("id"));
2358
+ if (!label) {
2359
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2360
+ }
2361
+ if (isSystemLabelId(label.gmail_id)) {
2362
+ return googleApiError(c, 400, "System labels cannot be deleted.", "invalidArgument", "INVALID_ARGUMENT");
2363
+ }
2364
+ for (const message of gs.messages.findBy("user_email", authEmail)) {
2365
+ if (!message.label_ids.includes(label.gmail_id)) continue;
2366
+ markMessageModified(
2367
+ gs,
2368
+ message,
2369
+ message.label_ids.filter((labelId) => labelId !== label.gmail_id)
2370
+ );
2371
+ }
2372
+ gs.labels.delete(label.id);
2373
+ return c.body(null, 204);
2374
+ });
2375
+ }
2376
+ async function saveLabel(c, gs, replaceMissingFields) {
2377
+ const authEmail = requireGmailUser(c);
2378
+ if (authEmail instanceof Response) return authEmail;
2379
+ const label = findLabelById(gs, authEmail, c.req.param("id"));
2380
+ if (!label) {
2381
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2382
+ }
2383
+ if (isSystemLabelId(label.gmail_id)) {
2384
+ return googleApiError(c, 400, "System labels cannot be modified.", "invalidArgument", "INVALID_ARGUMENT");
2385
+ }
2386
+ const body = await parseGoogleBody(c);
2387
+ const name = getString(body, "name")?.trim();
2388
+ const color = body.color && typeof body.color === "object" && !Array.isArray(body.color) ? body.color : void 0;
2389
+ if (name) {
2390
+ const conflicting = findLabelByName(gs, authEmail, name);
2391
+ if (conflicting && conflicting.gmail_id !== label.gmail_id) {
2392
+ return googleApiError(
2393
+ c,
2394
+ 400,
2395
+ "Label name exists or conflicts",
2396
+ "failedPrecondition",
2397
+ "FAILED_PRECONDITION"
2398
+ );
2399
+ }
2400
+ }
2401
+ const updated = updateLabelRecord(gs, label, {
2402
+ name: name ?? (replaceMissingFields ? label.name : void 0),
2403
+ message_list_visibility: getString(body, "messageListVisibility", "message_list_visibility") ?? (replaceMissingFields ? "show" : void 0),
2404
+ label_list_visibility: getString(body, "labelListVisibility", "label_list_visibility") ?? (replaceMissingFields ? "labelShow" : void 0),
2405
+ color_background: typeof color?.backgroundColor === "string" ? color.backgroundColor : getString(body, "color_background") ?? (replaceMissingFields ? null : void 0),
2406
+ color_text: typeof color?.textColor === "string" ? color.textColor : getString(body, "color_text") ?? (replaceMissingFields ? null : void 0)
2407
+ });
2408
+ return c.json(formatLabelResource(gs, updated));
2409
+ }
2410
+ function messageRoutes({ app, store }) {
2411
+ const gs = getGoogleStore(store);
2412
+ const createHandler = (mode) => async (c) => {
2413
+ const authEmail = requireGmailUser(c);
2414
+ if (authEmail instanceof Response) return authEmail;
2415
+ const body = await parseGoogleBody(c);
2416
+ const labelIds = getStringArray(body, "labelIds");
2417
+ const defaultLabelIds = mode === "send" ? dedupeLabelIds([...labelIds, "SENT"]) : labelIds.length > 0 ? labelIds : mode === "import" ? ["INBOX", "UNREAD"] : [];
2418
+ const missingLabelIds = findMissingLabelIds(gs, authEmail, defaultLabelIds);
2419
+ if (missingLabelIds.length > 0) {
2420
+ return googleApiError(c, 400, `Invalid label IDs: ${missingLabelIds.join(", ")}`, "invalidArgument", "INVALID_ARGUMENT");
2421
+ }
2422
+ const messageInput = parseMessageInputFromBody(body, {
2423
+ from: mode === "send" ? authEmail : void 0
2424
+ });
2425
+ if (!messageInput.raw && (!messageInput.from || !messageInput.to)) {
2426
+ return googleApiError(
2427
+ c,
2428
+ 400,
2429
+ "A raw MIME message or explicit from/to fields are required.",
2430
+ "invalidArgument",
2431
+ "INVALID_ARGUMENT"
2432
+ );
2433
+ }
2434
+ try {
2435
+ const message = createStoredMessage(gs, {
2436
+ user_email: authEmail,
2437
+ ...messageInput,
2438
+ label_ids: defaultLabelIds
2439
+ });
2440
+ return c.json(formatMessageResource(gs, message, "full"));
2441
+ } catch {
2442
+ return googleApiError(
2443
+ c,
2444
+ 400,
2445
+ "Invalid raw MIME message payload.",
2446
+ "invalidArgument",
2447
+ "INVALID_ARGUMENT"
2448
+ );
2449
+ }
2450
+ };
2451
+ app.get("/gmail/v1/users/:userId/messages", (c) => {
2452
+ const authEmail = requireGmailUser(c);
2453
+ if (authEmail instanceof Response) return authEmail;
2454
+ const url = new URL(c.req.url);
2455
+ const messages = listMessagesForUser(gs, authEmail, {
2456
+ labelIds: url.searchParams.getAll("labelIds"),
2457
+ query: url.searchParams.get("q")?.trim() ?? void 0,
2458
+ includeSpamTrash: parseBooleanParam(url.searchParams.get("includeSpamTrash"))
2459
+ });
2460
+ const offset = parseOffset(url.searchParams.get("pageToken"));
2461
+ const limit = normalizeLimit(url.searchParams.get("maxResults"), 100, 500);
2462
+ const page = messages.slice(offset, offset + limit);
2463
+ const nextPageToken = offset + limit < messages.length ? String(offset + limit) : void 0;
2464
+ return c.json({
2465
+ messages: page.map((message) => ({
2466
+ id: message.gmail_id,
2467
+ threadId: message.thread_id
2468
+ })),
2469
+ nextPageToken,
2470
+ resultSizeEstimate: messages.length
2471
+ });
2472
+ });
2473
+ app.post("/gmail/v1/users/:userId/messages/batchModify", async (c) => {
2474
+ const authEmail = requireGmailUser(c);
2475
+ if (authEmail instanceof Response) return authEmail;
2476
+ const body = await parseGoogleBody(c);
2477
+ const ids = getStringArray(body, "ids");
2478
+ const addLabelIds = getStringArray(body, "addLabelIds");
2479
+ const removeLabelIds = getStringArray(body, "removeLabelIds");
2480
+ const missingLabelIds = findMissingLabelIds(gs, authEmail, [...addLabelIds, ...removeLabelIds]);
2481
+ if (missingLabelIds.length > 0) {
2482
+ return googleApiError(c, 400, `Invalid label IDs: ${missingLabelIds.join(", ")}`, "invalidArgument", "INVALID_ARGUMENT");
2483
+ }
2484
+ for (const messageId of ids) {
2485
+ const message = getMessageById(gs, authEmail, messageId);
2486
+ if (!message) continue;
2487
+ markMessageModified(
2488
+ gs,
2489
+ message,
2490
+ applyLabelMutation(message.label_ids, addLabelIds, removeLabelIds)
2491
+ );
2492
+ }
2493
+ return c.body(null, 204);
2494
+ });
2495
+ app.post("/gmail/v1/users/:userId/messages/batchDelete", async (c) => {
2496
+ const authEmail = requireGmailUser(c);
2497
+ if (authEmail instanceof Response) return authEmail;
2498
+ const body = await parseGoogleBody(c);
2499
+ const ids = getStringArray(body, "ids");
2500
+ for (const messageId of ids) {
2501
+ const message = getMessageById(gs, authEmail, messageId);
2502
+ if (message) deleteMessage(gs, message);
2503
+ }
2504
+ return c.body(null, 204);
2505
+ });
2506
+ app.post("/gmail/v1/users/:userId/messages/import", createHandler("import"));
2507
+ app.post("/upload/gmail/v1/users/:userId/messages/import", createHandler("import"));
2508
+ app.post("/gmail/v1/users/:userId/messages/send", createHandler("send"));
2509
+ app.post("/upload/gmail/v1/users/:userId/messages/send", createHandler("send"));
2510
+ app.post("/gmail/v1/users/:userId/messages", createHandler("insert"));
2511
+ app.post("/upload/gmail/v1/users/:userId/messages", createHandler("insert"));
2512
+ app.get("/gmail/v1/users/:userId/messages/:messageId/attachments/:id", (c) => {
2513
+ const authEmail = requireGmailUser(c);
2514
+ if (authEmail instanceof Response) return authEmail;
2515
+ const message = getMessageById(gs, authEmail, c.req.param("messageId"));
2516
+ if (!message) {
2517
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2518
+ }
2519
+ const attachment = getAttachmentById(gs, authEmail, message.gmail_id, c.req.param("id"));
2520
+ if (!attachment) {
2521
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2522
+ }
2523
+ return c.json({
2524
+ attachmentId: attachment.gmail_id,
2525
+ size: attachment.size,
2526
+ data: attachment.data
2527
+ });
2528
+ });
2529
+ app.get("/gmail/v1/users/:userId/messages/:id", (c) => {
2530
+ const authEmail = requireGmailUser(c);
2531
+ if (authEmail instanceof Response) return authEmail;
2532
+ const message = getMessageById(gs, authEmail, c.req.param("id"));
2533
+ if (!message) {
2534
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2535
+ }
2536
+ const url = new URL(c.req.url);
2537
+ return c.json(
2538
+ formatMessageResource(
2539
+ gs,
2540
+ message,
2541
+ parseFormat(url.searchParams.get("format")),
2542
+ url.searchParams.getAll("metadataHeaders")
2543
+ )
2544
+ );
2545
+ });
2546
+ app.post("/gmail/v1/users/:userId/messages/:id/modify", async (c) => {
2547
+ const authEmail = requireGmailUser(c);
2548
+ if (authEmail instanceof Response) return authEmail;
2549
+ const message = getMessageById(gs, authEmail, c.req.param("id"));
2550
+ if (!message) {
2551
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2552
+ }
2553
+ const body = await parseGoogleBody(c);
2554
+ const addLabelIds = getStringArray(body, "addLabelIds");
2555
+ const removeLabelIds = getStringArray(body, "removeLabelIds");
2556
+ const missingLabelIds = findMissingLabelIds(gs, authEmail, [...addLabelIds, ...removeLabelIds]);
2557
+ if (missingLabelIds.length > 0) {
2558
+ return googleApiError(c, 400, `Invalid label IDs: ${missingLabelIds.join(", ")}`, "invalidArgument", "INVALID_ARGUMENT");
2559
+ }
2560
+ const updated = markMessageModified(
2561
+ gs,
2562
+ message,
2563
+ applyLabelMutation(message.label_ids, addLabelIds, removeLabelIds)
2564
+ );
2565
+ return c.json(formatMessageResource(gs, updated, "full"));
2566
+ });
2567
+ app.post("/gmail/v1/users/:userId/messages/:id/trash", (c) => {
2568
+ const authEmail = requireGmailUser(c);
2569
+ if (authEmail instanceof Response) return authEmail;
2570
+ const message = getMessageById(gs, authEmail, c.req.param("id"));
2571
+ if (!message) {
2572
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2573
+ }
2574
+ return c.json(formatMessageResource(gs, markMessageModified(gs, message, trashLabelIds(message.label_ids)), "full"));
2575
+ });
2576
+ app.post("/gmail/v1/users/:userId/messages/:id/untrash", (c) => {
2577
+ const authEmail = requireGmailUser(c);
2578
+ if (authEmail instanceof Response) return authEmail;
2579
+ const message = getMessageById(gs, authEmail, c.req.param("id"));
2580
+ if (!message) {
2581
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2582
+ }
2583
+ return c.json(formatMessageResource(gs, markMessageModified(gs, message, untrashLabelIds(message.label_ids)), "full"));
2584
+ });
2585
+ app.delete("/gmail/v1/users/:userId/messages/:id", (c) => {
2586
+ const authEmail = requireGmailUser(c);
2587
+ if (authEmail instanceof Response) return authEmail;
2588
+ const message = getMessageById(gs, authEmail, c.req.param("id"));
2589
+ if (!message) {
2590
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
2591
+ }
2592
+ deleteMessage(gs, message);
2593
+ return c.body(null, 204);
2594
+ });
2595
+ }
2596
+ function createErrorHandler(documentationUrl) {
2597
+ return async (c, next) => {
2598
+ if (documentationUrl) {
2599
+ c.set("docsUrl", documentationUrl);
2600
+ }
2601
+ await next();
2602
+ };
2603
+ }
2604
+ var errorHandler = createErrorHandler();
2605
+ var isDebug = typeof process !== "undefined" && (process.env.DEBUG === "1" || process.env.DEBUG === "true" || process.env.EMULATE_DEBUG === "1");
2606
+ function debug(label, ...args) {
2607
+ if (isDebug) {
2608
+ console.log(`[${label}]`, ...args);
2609
+ }
2610
+ }
2611
+ var __dirname = dirname(fileURLToPath(import.meta.url));
2612
+ var FONTS = {
2613
+ "geist-sans.woff2": readFileSync(join(__dirname, "fonts", "geist-sans.woff2")),
2614
+ "GeistPixel-Square.woff2": readFileSync(join(__dirname, "fonts", "GeistPixel-Square.woff2"))
2615
+ };
2616
+ function escapeHtml(s) {
2617
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2618
+ }
2619
+ function escapeAttr(s) {
2620
+ return escapeHtml(s).replace(/'/g, "&#39;");
2621
+ }
2622
+ var CSS = `
2623
+ @font-face{
2624
+ font-family:'Geist';font-style:normal;font-weight:100 900;font-display:swap;
2625
+ src:url('/_emulate/fonts/geist-sans.woff2') format('woff2');
2626
+ }
2627
+ @font-face{
2628
+ font-family:'Geist Pixel';font-style:normal;font-weight:400;font-display:swap;
2629
+ src:url('/_emulate/fonts/GeistPixel-Square.woff2') format('woff2');
2630
+ }
2631
+ *{box-sizing:border-box;margin:0;padding:0}
2632
+ body{
2633
+ font-family:'Geist',-apple-system,BlinkMacSystemFont,sans-serif;
2634
+ background:#000;color:#33ff00;min-height:100vh;
2635
+ -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
2636
+ }
2637
+ .emu-bar{
2638
+ border-bottom:1px solid #0a3300;padding:10px 20px;
2639
+ display:flex;align-items:center;gap:10px;font-size:.8125rem;color:#1a8c00;
2640
+ }
2641
+ .emu-bar-title{font-weight:600;color:#33ff00;font-family:'Geist Pixel',monospace;}
2642
+ .emu-bar-links{margin-left:auto;display:flex;gap:16px;}
2643
+ .emu-bar-links a{
2644
+ color:#1a8c00;font-size:.75rem;text-decoration:none;transition:color .15s;
2645
+ }
2646
+ .emu-bar-links a:hover{color:#33ff00;}
2647
+ .emu-bar-links a .full{display:inline;}
2648
+ .emu-bar-links a .short{display:none;}
2649
+ @media(max-width:600px){
2650
+ .emu-bar-links a .full{display:none;}
2651
+ .emu-bar-links a .short{display:inline;}
2652
+ }
2653
+
2654
+ .content{
2655
+ display:flex;align-items:center;justify-content:center;
2656
+ min-height:calc(100vh - 42px);padding:24px 16px;
2657
+ }
2658
+ .content-inner{width:100%;max-width:420px;}
2659
+ .card-title{
2660
+ font-family:'Geist Pixel',monospace;
2661
+ font-size:1.125rem;font-weight:600;margin-bottom:4px;color:#33ff00;
2662
+ }
2663
+ .card-subtitle{color:#1a8c00;font-size:.8125rem;margin-bottom:18px;line-height:1.45;}
2664
+ .powered-by{
2665
+ position:fixed;bottom:0;left:0;right:0;
2666
+ text-align:center;padding:12px;font-size:.6875rem;color:#0a3300;
2667
+ font-family:'Geist Pixel',monospace;
2668
+ }
2669
+ .powered-by a{color:#1a8c00;text-decoration:none;transition:color .15s;}
2670
+ .powered-by a:hover{color:#33ff00;}
2671
+
2672
+ .error-title{
2673
+ font-family:'Geist Pixel',monospace;
2674
+ color:#ff4444;font-size:1.125rem;font-weight:600;margin-bottom:8px;
2675
+ }
2676
+ .error-msg{color:#1a8c00;font-size:.875rem;line-height:1.5;}
2677
+ .error-card{text-align:center;}
2678
+
2679
+ .user-form{margin-bottom:8px;}
2680
+ .user-form:last-of-type{margin-bottom:0;}
2681
+ .user-btn{
2682
+ width:100%;display:flex;align-items:center;gap:12px;
2683
+ padding:10px 12px;border:1px solid #0a3300;border-radius:8px;
2684
+ background:#000;color:inherit;cursor:pointer;text-align:left;
2685
+ font:inherit;transition:border-color .15s;
2686
+ }
2687
+ .user-btn:hover{border-color:#33ff00;}
2688
+ .avatar{
2689
+ width:36px;height:36px;border-radius:50%;
2690
+ background:#0a3300;color:#33ff00;font-weight:600;font-size:.875rem;
2691
+ display:flex;align-items:center;justify-content:center;flex-shrink:0;
2692
+ font-family:'Geist Pixel',monospace;
2693
+ }
2694
+ .user-text{min-width:0;}
2695
+ .user-login{font-weight:600;font-size:.875rem;display:block;color:#33ff00;}
2696
+ .user-meta{color:#1a8c00;font-size:.75rem;margin-top:1px;}
2697
+ .user-email{font-size:.6875rem;color:#116600;word-break:break-all;margin-top:1px;}
2698
+
2699
+ .settings-layout{
2700
+ max-width:920px;margin:0 auto;padding:28px 20px;
2701
+ display:flex;gap:28px;
2702
+ }
2703
+ .settings-sidebar{width:200px;flex-shrink:0;}
2704
+ .settings-sidebar a{
2705
+ display:block;padding:6px 10px;border-radius:6px;color:#1a8c00;
2706
+ text-decoration:none;font-size:.8125rem;transition:color .15s;
2707
+ }
2708
+ .settings-sidebar a:hover{color:#33ff00;}
2709
+ .settings-sidebar a.active{color:#33ff00;font-weight:600;}
2710
+ .settings-main{flex:1;min-width:0;}
2711
+
2712
+ .s-card{
2713
+ padding:18px 0;margin-bottom:14px;border-bottom:1px solid #0a3300;
2714
+ }
2715
+ .s-card:last-child{border-bottom:none;}
2716
+ .s-card-header{display:flex;align-items:center;gap:14px;margin-bottom:14px;}
2717
+ .s-icon{
2718
+ width:42px;height:42px;border-radius:8px;
2719
+ background:#0a3300;display:flex;align-items:center;justify-content:center;
2720
+ font-size:1.125rem;font-weight:700;color:#116600;flex-shrink:0;
2721
+ font-family:'Geist Pixel',monospace;
2722
+ }
2723
+ .s-title{
2724
+ font-family:'Geist Pixel',monospace;
2725
+ font-size:1.25rem;font-weight:600;color:#33ff00;
2726
+ }
2727
+ .s-subtitle{font-size:.75rem;color:#1a8c00;margin-top:2px;}
2728
+ .section-heading{
2729
+ font-size:.9375rem;font-weight:600;margin-bottom:10px;color:#33ff00;
2730
+ display:flex;align-items:center;justify-content:space-between;
2731
+ }
2732
+ .perm-list{list-style:none;}
2733
+ .perm-list li{padding:5px 0;font-size:.8125rem;display:flex;align-items:center;gap:6px;color:#1a8c00;}
2734
+ .check{color:#33ff00;}
2735
+ .org-row{
2736
+ display:flex;align-items:center;gap:8px;padding:7px 0;
2737
+ border-bottom:1px solid #0a3300;font-size:.8125rem;
2738
+ }
2739
+ .org-row:last-child{border-bottom:none;}
2740
+ .org-icon{
2741
+ width:22px;height:22px;border-radius:4px;background:#0a3300;
2742
+ display:flex;align-items:center;justify-content:center;
2743
+ font-size:.625rem;font-weight:700;color:#116600;flex-shrink:0;
2744
+ font-family:'Geist Pixel',monospace;
2745
+ }
2746
+ .org-name{font-weight:600;color:#33ff00;}
2747
+ .badge{font-size:.6875rem;padding:1px 7px;border-radius:999px;font-weight:500;}
2748
+ .badge-granted{background:#0a3300;color:#33ff00;}
2749
+ .badge-denied{background:#1a0a0a;color:#ff4444;}
2750
+ .badge-requested{background:#0a3300;color:#1a8c00;}
2751
+ .btn-revoke{
2752
+ display:inline-block;padding:5px 14px;border-radius:6px;
2753
+ border:1px solid #0a3300;background:transparent;color:#ff4444;
2754
+ font-size:.75rem;font-weight:600;cursor:pointer;transition:border-color .15s;
2755
+ }
2756
+ .btn-revoke:hover{border-color:#ff4444;}
2757
+ .info-text{color:#1a8c00;font-size:.75rem;line-height:1.5;margin-top:10px;}
2758
+ .app-link{
2759
+ display:flex;align-items:center;gap:12px;padding:12px;
2760
+ border:1px solid #0a3300;border-radius:8px;background:#000;
2761
+ text-decoration:none;color:inherit;margin-bottom:8px;transition:border-color .15s;
2762
+ }
2763
+ .app-link:hover{border-color:#33ff00;}
2764
+ .app-link-name{font-weight:600;font-size:.875rem;color:#33ff00;}
2765
+ .app-link-scopes{font-size:.6875rem;color:#1a8c00;margin-top:1px;}
2766
+ .empty{color:#1a8c00;text-align:center;padding:28px 0;font-size:.875rem;}
2767
+ `;
2768
+ var POWERED_BY = `<div class="powered-by">Powered by <a href="https://emulate.dev" target="_blank" rel="noopener">emulate</a></div>`;
2769
+ function emuBar(service) {
2770
+ const title = service ? `${escapeHtml(service)} Emulator` : "Emulator";
2771
+ return `<div class="emu-bar">
2772
+ <span class="emu-bar-title">${title}</span>
2773
+ <nav class="emu-bar-links">
2774
+ <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>
2775
+ <a href="https://github.com/vercel-labs/emulate" target="_blank" rel="noopener"><span class="full">Source Code</span><span class="short">Source</span></a>
2776
+ <a href="https://emulate.dev" target="_blank" rel="noopener"><span class="full">Learn More</span><span class="short">Learn</span></a>
2777
+ </nav>
2778
+ </div>`;
2779
+ }
2780
+ function head(title) {
2781
+ return `<!DOCTYPE html>
2782
+ <html lang="en">
2783
+ <head>
2784
+ <meta charset="utf-8"/>
2785
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
2786
+ <title>${escapeHtml(title)} | emulate</title>
2787
+ <style>${CSS}</style>
2788
+ </head>`;
2789
+ }
2790
+ function renderCardPage(title, subtitle, body, service) {
2791
+ return `${head(title)}
2792
+ <body>
2793
+ ${emuBar(service)}
2794
+ <div class="content">
2795
+ <div class="content-inner">
2796
+ <div class="card-title">${escapeHtml(title)}</div>
2797
+ <div class="card-subtitle">${subtitle}</div>
2798
+ ${body}
2799
+ </div>
2800
+ </div>
2801
+ ${POWERED_BY}
2802
+ </body></html>`;
2803
+ }
2804
+ function renderErrorPage(title, message, service) {
2805
+ return `${head(title)}
2806
+ <body>
2807
+ ${emuBar(service)}
2808
+ <div class="content">
2809
+ <div class="content-inner error-card">
2810
+ <div class="error-title">${escapeHtml(title)}</div>
2811
+ <div class="error-msg">${escapeHtml(message)}</div>
2812
+ </div>
2813
+ </div>
2814
+ ${POWERED_BY}
2815
+ </body></html>`;
2816
+ }
2817
+ function renderUserButton(opts) {
2818
+ const hiddens = Object.entries(opts.hiddenFields).map(([k, v]) => `<input type="hidden" name="${escapeAttr(k)}" value="${escapeAttr(v)}"/>`).join("");
2819
+ const nameLine = opts.name ? `<div class="user-meta">${escapeHtml(opts.name)}</div>` : "";
2820
+ const emailLine = opts.email ? `<div class="user-email">${escapeHtml(opts.email)}</div>` : "";
2821
+ return `<form class="user-form" method="post" action="${escapeAttr(opts.formAction)}">
2822
+ ${hiddens}
2823
+ <button type="submit" class="user-btn">
2824
+ <span class="avatar">${escapeHtml(opts.letter)}</span>
2825
+ <span class="user-text">
2826
+ <span class="user-login">${escapeHtml(opts.login)}</span>
2827
+ ${nameLine}${emailLine}
2828
+ </span>
2829
+ </button>
2830
+ </form>`;
2831
+ }
2832
+ function normalizeUri(uri) {
2833
+ try {
2834
+ const u = new URL(uri);
2835
+ return `${u.origin}${u.pathname.replace(/\/+$/, "")}`;
2836
+ } catch {
2837
+ return uri.replace(/\/+$/, "").split("?")[0];
2838
+ }
2839
+ }
2840
+ function matchesRedirectUri(incoming, registered) {
2841
+ const normalized = normalizeUri(incoming);
2842
+ return registered.some((r) => normalizeUri(r) === normalized);
2843
+ }
2844
+ function constantTimeSecretEqual(a, b) {
2845
+ const bufA = Buffer.from(a, "utf-8");
2846
+ const bufB = Buffer.from(b, "utf-8");
2847
+ if (bufA.length !== bufB.length) return false;
2848
+ return timingSafeEqual(bufA, bufB);
2849
+ }
2850
+ function bodyStr(v) {
2851
+ if (typeof v === "string") return v;
2852
+ if (Array.isArray(v) && typeof v[0] === "string") return v[0];
2853
+ return "";
2854
+ }
2855
+ var JWT_SECRET = new TextEncoder().encode("emulate-google-jwt-secret");
2856
+ var PENDING_CODE_TTL_MS = 10 * 60 * 1e3;
2857
+ function getPendingCodes(store) {
2858
+ let map = store.getData("google.oauth.pendingCodes");
2859
+ if (!map) {
2860
+ map = /* @__PURE__ */ new Map();
2861
+ store.setData("google.oauth.pendingCodes", map);
2862
+ }
2863
+ return map;
2864
+ }
2865
+ function getRefreshTokens(store) {
2866
+ let map = store.getData("google.oauth.refreshTokens");
2867
+ if (!map) {
2868
+ map = /* @__PURE__ */ new Map();
2869
+ store.setData("google.oauth.refreshTokens", map);
2870
+ }
2871
+ return map;
2872
+ }
2873
+ function isPendingCodeExpired(p) {
2874
+ return Date.now() - p.created_at > PENDING_CODE_TTL_MS;
2875
+ }
2876
+ var SERVICE_LABEL = "Google";
2877
+ async function createIdToken(user, clientId, nonce, baseUrl) {
2878
+ const builder = new SignJWT({
2879
+ sub: user.uid,
2880
+ email: user.email,
2881
+ email_verified: user.email_verified,
2882
+ name: user.name,
2883
+ given_name: user.given_name,
2884
+ family_name: user.family_name,
2885
+ picture: user.picture,
2886
+ locale: user.locale,
2887
+ ...nonce ? { nonce } : {}
2888
+ }).setProtectedHeader({ alg: "HS256", typ: "JWT" }).setIssuer(baseUrl).setAudience(clientId).setIssuedAt().setExpirationTime("1h");
2889
+ return builder.sign(JWT_SECRET);
2890
+ }
2891
+ function oauthRoutes({ app, store, baseUrl, tokenMap }) {
2892
+ const gs = getGoogleStore(store);
2893
+ app.get("/.well-known/openid-configuration", (c) => {
2894
+ return c.json({
2895
+ issuer: baseUrl,
2896
+ authorization_endpoint: `${baseUrl}/o/oauth2/v2/auth`,
2897
+ token_endpoint: `${baseUrl}/oauth2/token`,
2898
+ userinfo_endpoint: `${baseUrl}/oauth2/v2/userinfo`,
2899
+ revocation_endpoint: `${baseUrl}/oauth2/revoke`,
2900
+ jwks_uri: `${baseUrl}/oauth2/v3/certs`,
2901
+ response_types_supported: ["code"],
2902
+ subject_types_supported: ["public"],
2903
+ id_token_signing_alg_values_supported: ["HS256"],
2904
+ scopes_supported: ["openid", "email", "profile"],
2905
+ token_endpoint_auth_methods_supported: ["client_secret_post", "client_secret_basic"],
2906
+ claims_supported: [
2907
+ "sub",
2908
+ "email",
2909
+ "email_verified",
2910
+ "name",
2911
+ "given_name",
2912
+ "family_name",
2913
+ "picture",
2914
+ "locale"
2915
+ ],
2916
+ code_challenge_methods_supported: ["plain", "S256"]
2917
+ });
2918
+ });
2919
+ app.get("/oauth2/v3/certs", (c) => {
2920
+ return c.json({ keys: [] });
2921
+ });
2922
+ app.get("/o/oauth2/v2/auth", (c) => {
2923
+ const client_id = c.req.query("client_id") ?? "";
2924
+ const redirect_uri = c.req.query("redirect_uri") ?? "";
2925
+ const scope = c.req.query("scope") ?? "";
2926
+ const state = c.req.query("state") ?? "";
2927
+ const nonce = c.req.query("nonce") ?? "";
2928
+ const code_challenge = c.req.query("code_challenge") ?? "";
2929
+ const code_challenge_method = c.req.query("code_challenge_method") ?? "";
2930
+ const clientsConfigured = gs.oauthClients.all().length > 0;
2931
+ let clientName = "";
2932
+ if (clientsConfigured) {
2933
+ const client = gs.oauthClients.findOneBy("client_id", client_id);
2934
+ if (!client) {
2935
+ return c.html(
2936
+ renderErrorPage("Application not found", `The client_id '${client_id}' is not registered.`, SERVICE_LABEL),
2937
+ 400
2938
+ );
2939
+ }
2940
+ if (redirect_uri && !matchesRedirectUri(redirect_uri, client.redirect_uris)) {
2941
+ return c.html(
2942
+ renderErrorPage("Redirect URI mismatch", "The redirect_uri is not registered for this application.", SERVICE_LABEL),
2943
+ 400
2944
+ );
2945
+ }
2946
+ clientName = client.name;
2947
+ }
2948
+ const subtitleText = clientName ? `Sign in to <strong>${escapeHtml(clientName)}</strong> with your Google account.` : "Choose a seeded user to continue.";
2949
+ const users = gs.users.all();
2950
+ const userButtons = users.map((user) => {
2951
+ return renderUserButton({
2952
+ letter: (user.email[0] ?? "?").toUpperCase(),
2953
+ login: user.email,
2954
+ name: user.name,
2955
+ email: user.email,
2956
+ formAction: "/o/oauth2/v2/auth/callback",
2957
+ hiddenFields: {
2958
+ email: user.email,
2959
+ redirect_uri,
2960
+ scope,
2961
+ state,
2962
+ nonce,
2963
+ client_id,
2964
+ code_challenge,
2965
+ code_challenge_method
2966
+ }
2967
+ });
2968
+ }).join("\n");
2969
+ const body = users.length === 0 ? '<p class="empty">No users in the emulator store.</p>' : userButtons;
2970
+ return c.html(renderCardPage("Sign in to Google", subtitleText, body, SERVICE_LABEL));
2971
+ });
2972
+ app.post("/o/oauth2/v2/auth/callback", async (c) => {
2973
+ const body = await c.req.parseBody();
2974
+ const email = bodyStr(body.email);
2975
+ const redirect_uri = bodyStr(body.redirect_uri);
2976
+ const scope = bodyStr(body.scope);
2977
+ const state = bodyStr(body.state);
2978
+ const client_id = bodyStr(body.client_id);
2979
+ const nonce = bodyStr(body.nonce);
2980
+ const code_challenge = bodyStr(body.code_challenge);
2981
+ const code_challenge_method = bodyStr(body.code_challenge_method);
2982
+ const code = randomBytes2(20).toString("hex");
2983
+ getPendingCodes(store).set(code, {
2984
+ email,
2985
+ scope,
2986
+ redirectUri: redirect_uri,
2987
+ clientId: client_id,
2988
+ nonce: nonce || null,
2989
+ codeChallenge: code_challenge || null,
2990
+ codeChallengeMethod: code_challenge_method || null,
2991
+ created_at: Date.now()
2992
+ });
2993
+ debug("google.oauth", `[Google callback] code=${code.slice(0, 8)}... email=${email}`);
2994
+ const url = new URL(redirect_uri);
2995
+ url.searchParams.set("code", code);
2996
+ if (state) url.searchParams.set("state", state);
2997
+ return c.redirect(url.toString(), 302);
2998
+ });
2999
+ app.post("/oauth2/token", async (c) => {
3000
+ const contentType = c.req.header("Content-Type") ?? "";
3001
+ const rawText = await c.req.text();
3002
+ let body;
3003
+ if (contentType.includes("application/json")) {
3004
+ try {
3005
+ body = JSON.parse(rawText);
3006
+ } catch {
3007
+ body = {};
3008
+ }
3009
+ } else {
3010
+ body = Object.fromEntries(new URLSearchParams(rawText));
3011
+ }
3012
+ const code = typeof body.code === "string" ? body.code : "";
3013
+ const redirect_uri = typeof body.redirect_uri === "string" ? body.redirect_uri : "";
3014
+ const grant_type = typeof body.grant_type === "string" ? body.grant_type : "";
3015
+ const code_verifier = typeof body.code_verifier === "string" ? body.code_verifier : void 0;
3016
+ const bodyClientId = typeof body.client_id === "string" ? body.client_id : "";
3017
+ const bodyClientSecret = typeof body.client_secret === "string" ? body.client_secret : "";
3018
+ const clientsConfigured = gs.oauthClients.all().length > 0;
3019
+ if (clientsConfigured) {
3020
+ const client = gs.oauthClients.findOneBy("client_id", bodyClientId);
3021
+ if (!client) {
3022
+ return c.json({ error: "invalid_client", error_description: "The client_id is incorrect." }, 401);
3023
+ }
3024
+ if (!constantTimeSecretEqual(bodyClientSecret, client.client_secret)) {
3025
+ return c.json({ error: "invalid_client", error_description: "The client_secret is incorrect." }, 401);
3026
+ }
3027
+ }
3028
+ if (grant_type === "refresh_token") {
3029
+ const refreshToken2 = typeof body.refresh_token === "string" ? body.refresh_token : "";
3030
+ const record = getRefreshTokens(store).get(refreshToken2);
3031
+ if (!record) {
3032
+ return c.json({ error: "invalid_grant", error_description: "The refresh token is invalid." }, 400);
3033
+ }
3034
+ if (clientsConfigured && record.clientId !== bodyClientId) {
3035
+ return c.json({ error: "invalid_grant", error_description: "The refresh token is invalid." }, 400);
3036
+ }
3037
+ const user2 = gs.users.findOneBy("email", record.email);
3038
+ if (!user2) {
3039
+ return c.json({ error: "invalid_grant", error_description: "User not found." }, 400);
3040
+ }
3041
+ const accessToken2 = "google_" + randomBytes2(20).toString("base64url");
3042
+ const scopes2 = record.scope ? record.scope.split(/\s+/).filter(Boolean) : [];
3043
+ if (tokenMap) {
3044
+ tokenMap.set(accessToken2, { login: user2.email, id: user2.id, scopes: scopes2 });
3045
+ }
3046
+ return c.json({
3047
+ access_token: accessToken2,
3048
+ token_type: "Bearer",
3049
+ expires_in: 3600,
3050
+ scope: record.scope || "openid email profile"
3051
+ });
3052
+ }
3053
+ if (grant_type !== "authorization_code") {
3054
+ return c.json({ error: "unsupported_grant_type", error_description: "Only authorization_code and refresh_token are supported." }, 400);
3055
+ }
3056
+ const pendingMap = getPendingCodes(store);
3057
+ const pending = pendingMap.get(code);
3058
+ if (!pending) {
3059
+ return c.json({ error: "invalid_grant", error_description: "The code is incorrect or expired." }, 400);
3060
+ }
3061
+ if (isPendingCodeExpired(pending)) {
3062
+ pendingMap.delete(code);
3063
+ return c.json({ error: "invalid_grant", error_description: "The code is incorrect or expired." }, 400);
3064
+ }
3065
+ if (pending.codeChallenge != null) {
3066
+ if (code_verifier === void 0) {
3067
+ return c.json({ error: "invalid_grant", error_description: "PKCE verification failed." }, 400);
3068
+ }
3069
+ const method = (pending.codeChallengeMethod ?? "plain").toLowerCase();
3070
+ if (method === "s256") {
3071
+ const expected = createHash("sha256").update(code_verifier).digest("base64url");
3072
+ if (expected !== pending.codeChallenge) {
3073
+ return c.json({ error: "invalid_grant", error_description: "PKCE verification failed." }, 400);
3074
+ }
3075
+ } else if (method === "plain") {
3076
+ if (code_verifier !== pending.codeChallenge) {
3077
+ return c.json({ error: "invalid_grant", error_description: "PKCE verification failed." }, 400);
3078
+ }
3079
+ } else {
3080
+ return c.json({ error: "invalid_grant", error_description: "PKCE verification failed." }, 400);
3081
+ }
3082
+ }
3083
+ pendingMap.delete(code);
3084
+ const user = gs.users.findOneBy("email", pending.email);
3085
+ if (!user) {
3086
+ return c.json({ error: "invalid_grant", error_description: "User not found." }, 400);
3087
+ }
3088
+ const accessToken = "google_" + randomBytes2(20).toString("base64url");
3089
+ const refreshToken = "google_refresh_" + randomBytes2(24).toString("base64url");
3090
+ const scopes = pending.scope ? pending.scope.split(/\s+/).filter(Boolean) : [];
3091
+ if (tokenMap) {
3092
+ tokenMap.set(accessToken, { login: user.email, id: user.id, scopes });
3093
+ }
3094
+ getRefreshTokens(store).set(refreshToken, {
3095
+ email: user.email,
3096
+ scope: pending.scope,
3097
+ clientId: pending.clientId
3098
+ });
3099
+ const idToken = await createIdToken(user, pending.clientId, pending.nonce, baseUrl);
3100
+ debug("google.oauth", `[Google token] issued token for ${user.email}`);
3101
+ return c.json({
3102
+ access_token: accessToken,
3103
+ refresh_token: refreshToken,
3104
+ id_token: idToken,
3105
+ token_type: "Bearer",
3106
+ expires_in: 3600,
3107
+ scope: pending.scope || "openid email profile"
3108
+ });
3109
+ });
3110
+ app.get("/oauth2/v2/userinfo", (c) => {
3111
+ const authUser = c.get("authUser");
3112
+ if (!authUser) {
3113
+ return c.json({ error: "invalid_token", error_description: "Authentication required." }, 401);
3114
+ }
3115
+ const user = gs.users.findOneBy("email", authUser.login);
3116
+ if (!user) {
3117
+ return c.json({ error: "invalid_token", error_description: "User not found." }, 401);
3118
+ }
3119
+ return c.json({
3120
+ sub: user.uid,
3121
+ email: user.email,
3122
+ email_verified: user.email_verified,
3123
+ name: user.name,
3124
+ given_name: user.given_name,
3125
+ family_name: user.family_name,
3126
+ picture: user.picture,
3127
+ locale: user.locale
3128
+ });
3129
+ });
3130
+ app.post("/oauth2/revoke", async (c) => {
3131
+ const contentType = c.req.header("Content-Type") ?? "";
3132
+ const rawText = await c.req.text();
3133
+ let token;
3134
+ if (contentType.includes("application/json")) {
3135
+ try {
3136
+ const parsed = JSON.parse(rawText);
3137
+ token = typeof parsed.token === "string" ? parsed.token : "";
3138
+ } catch {
3139
+ token = "";
3140
+ }
3141
+ } else {
3142
+ const params = new URLSearchParams(rawText);
3143
+ token = params.get("token") ?? "";
3144
+ }
3145
+ if (token && tokenMap) {
3146
+ tokenMap.delete(token);
3147
+ }
3148
+ if (token) {
3149
+ getRefreshTokens(store).delete(token);
3150
+ }
3151
+ return c.body(null, 200);
3152
+ });
3153
+ }
3154
+ function settingsRoutes({ app, store }) {
3155
+ const gs = getGoogleStore(store);
3156
+ app.get("/gmail/v1/users/:userId/settings/filters", (c) => {
3157
+ const authEmail = requireGmailUser(c);
3158
+ if (authEmail instanceof Response) return authEmail;
3159
+ return c.json({
3160
+ filter: listFiltersForUser(gs, authEmail).map((filter) => formatFilterResource(filter))
3161
+ });
3162
+ });
3163
+ app.post("/gmail/v1/users/:userId/settings/filters", async (c) => {
3164
+ const authEmail = requireGmailUser(c);
3165
+ if (authEmail instanceof Response) return authEmail;
3166
+ const body = await parseGoogleBody(c);
3167
+ const criteria = getRecord(body, "criteria") ?? {};
3168
+ const action = getRecord(body, "action") ?? {};
3169
+ const criteriaFrom = getString(criteria, "from") ?? null;
3170
+ const addLabelIds = getStringArray(action, "addLabelIds");
3171
+ const removeLabelIds = getStringArray(action, "removeLabelIds");
3172
+ if (addLabelIds.length === 0 && removeLabelIds.length === 0) {
3173
+ return googleApiError(c, 400, "Filter actions are required.", "invalidArgument", "INVALID_ARGUMENT");
3174
+ }
3175
+ const missingLabelIds = findMissingLabelIds(gs, authEmail, [...addLabelIds, ...removeLabelIds]);
3176
+ if (missingLabelIds.length > 0) {
3177
+ return googleApiError(c, 400, `Invalid label IDs: ${missingLabelIds.join(", ")}`, "invalidArgument", "INVALID_ARGUMENT");
3178
+ }
3179
+ if (findMatchingFilter(gs, {
3180
+ user_email: authEmail,
3181
+ criteria_from: criteriaFrom,
3182
+ add_label_ids: addLabelIds,
3183
+ remove_label_ids: removeLabelIds
3184
+ })) {
3185
+ return googleApiError(c, 400, "Filter already exists", "failedPrecondition", "FAILED_PRECONDITION");
3186
+ }
3187
+ const filter = createFilterRecord(gs, {
3188
+ user_email: authEmail,
3189
+ criteria_from: criteriaFrom,
3190
+ add_label_ids: addLabelIds,
3191
+ remove_label_ids: removeLabelIds
3192
+ });
3193
+ return c.json(formatFilterResource(filter));
3194
+ });
3195
+ app.delete("/gmail/v1/users/:userId/settings/filters/:id", (c) => {
3196
+ const authEmail = requireGmailUser(c);
3197
+ if (authEmail instanceof Response) return authEmail;
3198
+ const filter = getFilterById(gs, authEmail, c.req.param("id"));
3199
+ if (!filter) {
3200
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
3201
+ }
3202
+ gs.filters.delete(filter.id);
3203
+ return c.body(null, 204);
3204
+ });
3205
+ app.get("/gmail/v1/users/:userId/settings/forwardingAddresses", (c) => {
3206
+ const authEmail = requireGmailUser(c);
3207
+ if (authEmail instanceof Response) return authEmail;
3208
+ return c.json({
3209
+ forwardingAddresses: listForwardingAddressesForUser(gs, authEmail).map(
3210
+ (entry) => formatForwardingAddressResource(entry)
3211
+ )
3212
+ });
3213
+ });
3214
+ app.get("/gmail/v1/users/:userId/settings/sendAs", (c) => {
3215
+ const authEmail = requireGmailUser(c);
3216
+ if (authEmail instanceof Response) return authEmail;
3217
+ return c.json({
3218
+ sendAs: listSendAsForUser(gs, authEmail).map((entry) => formatSendAsResource(entry))
3219
+ });
3220
+ });
3221
+ }
3222
+ function threadRoutes({ app, store }) {
3223
+ const gs = getGoogleStore(store);
3224
+ app.get("/gmail/v1/users/:userId/threads", (c) => {
3225
+ const authEmail = requireGmailUser(c);
3226
+ if (authEmail instanceof Response) return authEmail;
3227
+ const url = new URL(c.req.url);
3228
+ const threads = groupThreads(
3229
+ listMessagesForUser(gs, authEmail, {
3230
+ labelIds: url.searchParams.getAll("labelIds"),
3231
+ query: url.searchParams.get("q")?.trim() ?? void 0,
3232
+ includeSpamTrash: parseBooleanParam(url.searchParams.get("includeSpamTrash"))
3233
+ })
3234
+ );
3235
+ const offset = parseOffset(url.searchParams.get("pageToken"));
3236
+ const limit = normalizeLimit(url.searchParams.get("maxResults"), 100, 500);
3237
+ const page = threads.slice(offset, offset + limit);
3238
+ const nextPageToken = offset + limit < threads.length ? String(offset + limit) : void 0;
3239
+ return c.json({
3240
+ threads: page.map((thread) => ({
3241
+ id: thread.id,
3242
+ snippet: thread.snippet,
3243
+ historyId: thread.historyId
3244
+ })),
3245
+ nextPageToken,
3246
+ resultSizeEstimate: threads.length
3247
+ });
3248
+ });
3249
+ app.get("/gmail/v1/users/:userId/threads/:id", (c) => {
3250
+ const authEmail = requireGmailUser(c);
3251
+ if (authEmail instanceof Response) return authEmail;
3252
+ const url = new URL(c.req.url);
3253
+ const messages = getThreadMessages(gs, authEmail, c.req.param("id"), {
3254
+ includeSpamTrash: parseBooleanParam(url.searchParams.get("includeSpamTrash"))
3255
+ });
3256
+ if (messages.length === 0) {
3257
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
3258
+ }
3259
+ return c.json(
3260
+ formatThreadResource(
3261
+ gs,
3262
+ messages,
3263
+ parseFormat(url.searchParams.get("format")),
3264
+ url.searchParams.getAll("metadataHeaders")
3265
+ )
3266
+ );
3267
+ });
3268
+ app.post("/gmail/v1/users/:userId/threads/:id/modify", async (c) => {
3269
+ const authEmail = requireGmailUser(c);
3270
+ if (authEmail instanceof Response) return authEmail;
3271
+ const messages = getThreadMessages(gs, authEmail, c.req.param("id"), { includeSpamTrash: true });
3272
+ if (messages.length === 0) {
3273
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
3274
+ }
3275
+ const body = await parseGoogleBody(c);
3276
+ const addLabelIds = getStringArray(body, "addLabelIds");
3277
+ const removeLabelIds = getStringArray(body, "removeLabelIds");
3278
+ const missingLabelIds = findMissingLabelIds(gs, authEmail, [...addLabelIds, ...removeLabelIds]);
3279
+ if (missingLabelIds.length > 0) {
3280
+ return googleApiError(c, 400, `Invalid label IDs: ${missingLabelIds.join(", ")}`, "invalidArgument", "INVALID_ARGUMENT");
3281
+ }
3282
+ const updated = messages.map(
3283
+ (message) => markMessageModified(
3284
+ gs,
3285
+ message,
3286
+ applyLabelMutation(message.label_ids, addLabelIds, removeLabelIds)
3287
+ )
3288
+ );
3289
+ return c.json(formatThreadResource(gs, updated, "full"));
3290
+ });
3291
+ app.post("/gmail/v1/users/:userId/threads/:id/trash", (c) => {
3292
+ const authEmail = requireGmailUser(c);
3293
+ if (authEmail instanceof Response) return authEmail;
3294
+ const messages = getThreadMessages(gs, authEmail, c.req.param("id"), { includeSpamTrash: true });
3295
+ if (messages.length === 0) {
3296
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
3297
+ }
3298
+ const updated = messages.map((message) => markMessageModified(gs, message, trashLabelIds(message.label_ids)));
3299
+ return c.json(formatThreadResource(gs, updated, "full"));
3300
+ });
3301
+ app.post("/gmail/v1/users/:userId/threads/:id/untrash", (c) => {
3302
+ const authEmail = requireGmailUser(c);
3303
+ if (authEmail instanceof Response) return authEmail;
3304
+ const messages = getThreadMessages(gs, authEmail, c.req.param("id"), { includeSpamTrash: true });
3305
+ if (messages.length === 0) {
3306
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
3307
+ }
3308
+ const updated = messages.map((message) => markMessageModified(gs, message, untrashLabelIds(message.label_ids)));
3309
+ return c.json(formatThreadResource(gs, updated, "full"));
3310
+ });
3311
+ app.delete("/gmail/v1/users/:userId/threads/:id", (c) => {
3312
+ const authEmail = requireGmailUser(c);
3313
+ if (authEmail instanceof Response) return authEmail;
3314
+ const messages = getThreadMessages(gs, authEmail, c.req.param("id"), { includeSpamTrash: true });
3315
+ if (messages.length === 0) {
3316
+ return googleApiError(c, 404, "Requested entity was not found.", "notFound", "NOT_FOUND");
3317
+ }
3318
+ for (const message of messages) {
3319
+ deleteMessage(gs, message);
3320
+ }
3321
+ return c.body(null, 204);
3322
+ });
3323
+ }
3324
+ function seedDefaults(store, _baseUrl) {
3325
+ const gs = getGoogleStore(store);
3326
+ const defaultEmail = "testuser@gmail.com";
3327
+ if (!gs.users.findOneBy("email", defaultEmail)) {
3328
+ gs.users.insert({
3329
+ uid: generateUid("goog"),
3330
+ email: defaultEmail,
3331
+ name: "Test User",
3332
+ given_name: "Test",
3333
+ family_name: "User",
3334
+ picture: null,
3335
+ email_verified: true,
3336
+ locale: "en"
3337
+ });
3338
+ }
3339
+ ensureSystemLabels(gs, defaultEmail);
3340
+ seedCalendars(store, [
3341
+ {
3342
+ id: "primary",
3343
+ user_email: defaultEmail,
3344
+ summary: defaultEmail,
3345
+ primary: true,
3346
+ selected: true,
3347
+ time_zone: "UTC"
3348
+ },
3349
+ {
3350
+ id: "cal_team",
3351
+ user_email: defaultEmail,
3352
+ summary: "Team Calendar",
3353
+ description: "Shared team events",
3354
+ selected: true,
3355
+ time_zone: "UTC"
3356
+ }
3357
+ ], defaultEmail);
3358
+ seedCalendarEvents(store, [
3359
+ {
3360
+ id: "evt_standup",
3361
+ user_email: defaultEmail,
3362
+ calendar_id: "primary",
3363
+ summary: "Daily Standup",
3364
+ description: "Team sync",
3365
+ start_date_time: new Date(Date.now() + 60 * 60 * 1e3).toISOString(),
3366
+ end_date_time: new Date(Date.now() + 90 * 60 * 1e3).toISOString(),
3367
+ attendees: [
3368
+ { email: defaultEmail, display_name: "Test User" },
3369
+ { email: "teammate@example.com", display_name: "Teammate" }
3370
+ ],
3371
+ conference_entry_points: [
3372
+ {
3373
+ entry_point_type: "video",
3374
+ uri: "https://meet.google.com/emulate-standup",
3375
+ label: "Google Meet"
3376
+ }
3377
+ ],
3378
+ hangout_link: "https://meet.google.com/emulate-standup"
3379
+ }
3380
+ ], defaultEmail);
3381
+ seedDriveItems(store, [
3382
+ {
3383
+ id: "drv_root_receipts",
3384
+ user_email: defaultEmail,
3385
+ name: "Receipts",
3386
+ mime_type: "application/vnd.google-apps.folder",
3387
+ parent_ids: ["root"]
3388
+ },
3389
+ {
3390
+ id: "drv_receipt_pdf",
3391
+ user_email: defaultEmail,
3392
+ name: "March Receipt.pdf",
3393
+ mime_type: "application/pdf",
3394
+ parent_ids: ["drv_root_receipts"],
3395
+ data: "receipt-pdf-data"
3396
+ }
3397
+ ], defaultEmail);
3398
+ seedMessages(store, [
3399
+ {
3400
+ id: "msg_welcome",
3401
+ thread_id: "thr_welcome",
3402
+ user_email: defaultEmail,
3403
+ from: "Welcome Team <welcome@example.com>",
3404
+ to: defaultEmail,
3405
+ subject: "Welcome to your local Gmail emulator",
3406
+ snippet: "Your OAuth flow is set up and Gmail message, thread, and label APIs are ready.",
3407
+ 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.",
3408
+ label_ids: ["INBOX", "UNREAD", "CATEGORY_UPDATES"],
3409
+ date: new Date(Date.now() - 60 * 60 * 1e3).toISOString()
3410
+ },
3411
+ {
3412
+ id: "msg_build",
3413
+ thread_id: "thr_build",
3414
+ user_email: defaultEmail,
3415
+ from: "Build Bot <builds@example.com>",
3416
+ to: defaultEmail,
3417
+ subject: "Nightly build finished successfully",
3418
+ snippet: "The latest build completed successfully in 6 minutes.",
3419
+ body_text: "The latest build completed successfully in 6 minutes.\n\nArtifact upload finished and smoke checks passed.",
3420
+ label_ids: ["INBOX", "CATEGORY_UPDATES"],
3421
+ date: new Date(Date.now() - 2 * 60 * 60 * 1e3).toISOString()
3422
+ },
3423
+ {
3424
+ id: "msg_build_reply",
3425
+ thread_id: "thr_build",
3426
+ user_email: defaultEmail,
3427
+ from: defaultEmail,
3428
+ to: "Build Bot <builds@example.com>",
3429
+ subject: "Re: Nightly build finished successfully",
3430
+ snippet: "Thanks, I will review the artifact after lunch.",
3431
+ body_text: "Thanks, I will review the artifact after lunch.",
3432
+ label_ids: ["SENT"],
3433
+ date: new Date(Date.now() - 90 * 60 * 1e3).toISOString(),
3434
+ in_reply_to: "<msg_build@emulate.google.local>",
3435
+ references: "<msg_build@emulate.google.local>"
3436
+ },
3437
+ {
3438
+ id: "msg_draft",
3439
+ thread_id: "thr_draft",
3440
+ user_email: defaultEmail,
3441
+ from: defaultEmail,
3442
+ to: "someone@example.com",
3443
+ subject: "Draft follow-up",
3444
+ snippet: "Checking in on the open question from yesterday.",
3445
+ body_text: "Checking in on the open question from yesterday.",
3446
+ label_ids: ["DRAFT"],
3447
+ date: new Date(Date.now() - 30 * 60 * 1e3).toISOString()
3448
+ }
3449
+ ], defaultEmail);
3450
+ }
3451
+ function seedFromConfig(store, _baseUrl, config) {
3452
+ const gs = getGoogleStore(store);
3453
+ if (config.users) {
3454
+ for (const user of config.users) {
3455
+ const existing = gs.users.findOneBy("email", user.email);
3456
+ if (!existing) {
3457
+ const nameParts = (user.name ?? "").split(/\s+/).filter(Boolean);
3458
+ gs.users.insert({
3459
+ uid: generateUid("goog"),
3460
+ email: user.email,
3461
+ name: user.name ?? user.email.split("@")[0],
3462
+ given_name: user.given_name ?? nameParts[0] ?? "",
3463
+ family_name: user.family_name ?? nameParts.slice(1).join(" "),
3464
+ picture: user.picture ?? null,
3465
+ email_verified: user.email_verified ?? true,
3466
+ locale: user.locale ?? "en"
3467
+ });
3468
+ }
3469
+ ensureSystemLabels(gs, user.email);
3470
+ }
3471
+ }
3472
+ if (config.oauth_clients) {
3473
+ for (const client of config.oauth_clients) {
3474
+ const existing = gs.oauthClients.findOneBy("client_id", client.client_id);
3475
+ if (existing) continue;
3476
+ gs.oauthClients.insert({
3477
+ client_id: client.client_id,
3478
+ client_secret: client.client_secret,
3479
+ name: client.name ?? "Code App (Google)",
3480
+ redirect_uris: client.redirect_uris
3481
+ });
3482
+ }
3483
+ }
3484
+ const fallbackEmail = config.users?.[0]?.email ?? gs.users.all()[0]?.email ?? "testuser@gmail.com";
3485
+ ensureSystemLabels(gs, fallbackEmail);
3486
+ if (config.labels) {
3487
+ seedLabels(store, config.labels, fallbackEmail);
3488
+ }
3489
+ if (config.messages) {
3490
+ seedMessages(store, config.messages, fallbackEmail);
3491
+ }
3492
+ if (config.calendars) {
3493
+ seedCalendars(store, config.calendars, fallbackEmail);
3494
+ }
3495
+ if (config.calendar_events) {
3496
+ seedCalendarEvents(store, config.calendar_events, fallbackEmail);
3497
+ }
3498
+ if (config.drive_items) {
3499
+ seedDriveItems(store, config.drive_items, fallbackEmail);
3500
+ }
3501
+ }
3502
+ function seedLabels(store, labels, fallbackEmail) {
3503
+ const gs = getGoogleStore(store);
3504
+ for (const label of labels) {
3505
+ const userEmail = label.user_email ?? fallbackEmail;
3506
+ ensureSystemLabels(gs, userEmail);
3507
+ const existing = (label.id ? findLabelById(gs, userEmail, label.id) : void 0) ?? findLabelByName(gs, userEmail, label.name);
3508
+ if (existing) continue;
3509
+ createLabelRecord(gs, {
3510
+ gmail_id: label.id,
3511
+ user_email: userEmail,
3512
+ name: label.name,
3513
+ type: label.type ?? "user",
3514
+ message_list_visibility: label.message_list_visibility ?? "show",
3515
+ label_list_visibility: label.label_list_visibility ?? "labelShow",
3516
+ color_background: label.color_background ?? null,
3517
+ color_text: label.color_text ?? null
3518
+ });
3519
+ }
3520
+ }
3521
+ function seedMessages(store, messages, fallbackEmail) {
3522
+ const gs = getGoogleStore(store);
3523
+ for (const message of messages) {
3524
+ const userEmail = message.user_email ?? fallbackEmail;
3525
+ ensureSystemLabels(gs, userEmail);
3526
+ if (message.id && gs.messages.findOneBy("gmail_id", message.id)) continue;
3527
+ createStoredMessage(gs, {
3528
+ gmail_id: message.id,
3529
+ thread_id: message.thread_id,
3530
+ user_email: userEmail,
3531
+ raw: message.raw ?? null,
3532
+ from: message.from,
3533
+ to: message.to,
3534
+ cc: message.cc ?? null,
3535
+ bcc: message.bcc ?? null,
3536
+ reply_to: message.reply_to ?? null,
3537
+ subject: message.subject,
3538
+ snippet: message.snippet,
3539
+ body_text: message.body_text ?? null,
3540
+ body_html: message.body_html ?? null,
3541
+ label_ids: message.label_ids ?? ["INBOX", "UNREAD"],
3542
+ date: message.date,
3543
+ internal_date: message.internal_date,
3544
+ message_id: message.message_id,
3545
+ references: message.references ?? null,
3546
+ in_reply_to: message.in_reply_to ?? null
3547
+ }, {
3548
+ createMissingCustomLabels: true
3549
+ });
3550
+ }
3551
+ }
3552
+ function seedCalendars(store, calendars, fallbackEmail) {
3553
+ const gs = getGoogleStore(store);
3554
+ for (const calendar of calendars) {
3555
+ const userEmail = calendar.user_email ?? fallbackEmail;
3556
+ createCalendarRecord(gs, {
3557
+ google_id: calendar.id,
3558
+ user_email: userEmail,
3559
+ summary: calendar.summary,
3560
+ description: calendar.description ?? null,
3561
+ time_zone: calendar.time_zone ?? "UTC",
3562
+ primary: calendar.primary ?? false,
3563
+ selected: calendar.selected ?? true,
3564
+ access_role: calendar.access_role ?? "owner"
3565
+ });
3566
+ }
3567
+ }
3568
+ function seedCalendarEvents(store, events, fallbackEmail) {
3569
+ const gs = getGoogleStore(store);
3570
+ for (const event of events) {
3571
+ const userEmail = event.user_email ?? fallbackEmail;
3572
+ createCalendarEventRecord(gs, {
3573
+ google_id: event.id,
3574
+ user_email: userEmail,
3575
+ calendar_google_id: event.calendar_id ?? "primary",
3576
+ status: event.status ?? "confirmed",
3577
+ summary: event.summary,
3578
+ description: event.description ?? null,
3579
+ location: event.location ?? null,
3580
+ start_date_time: event.start_date_time ?? null,
3581
+ start_date: event.start_date ?? null,
3582
+ end_date_time: event.end_date_time ?? null,
3583
+ end_date: event.end_date ?? null,
3584
+ attendees: (event.attendees ?? []).map((attendee) => ({
3585
+ email: attendee.email,
3586
+ display_name: attendee.display_name ?? null,
3587
+ response_status: null,
3588
+ organizer: false,
3589
+ self: attendee.email === userEmail
3590
+ })),
3591
+ conference_entry_points: (event.conference_entry_points ?? []).map((entry) => ({
3592
+ entry_point_type: entry.entry_point_type,
3593
+ uri: entry.uri,
3594
+ label: entry.label ?? null
3595
+ })),
3596
+ hangout_link: event.hangout_link ?? null
3597
+ });
3598
+ }
3599
+ }
3600
+ function seedDriveItems(store, items, fallbackEmail) {
3601
+ const gs = getGoogleStore(store);
3602
+ for (const item of items) {
3603
+ const userEmail = item.user_email ?? fallbackEmail;
3604
+ if (item.id && gs.driveItems.findOneBy("google_id", item.id)) continue;
3605
+ createDriveItemRecord(gs, {
3606
+ google_id: item.id,
3607
+ user_email: userEmail,
3608
+ name: item.name,
3609
+ mime_type: item.mime_type,
3610
+ parent_google_ids: item.parent_ids ?? ["root"],
3611
+ size: item.data ? Buffer.byteLength(item.data, "utf8") : null,
3612
+ data: item.data ? Buffer.from(item.data, "utf8").toString("base64url") : null
3613
+ });
3614
+ }
3615
+ }
3616
+ var googlePlugin = {
3617
+ name: "google",
3618
+ register(app, store, webhooks, baseUrl, tokenMap) {
3619
+ const ctx = { app, store, webhooks, baseUrl, tokenMap };
3620
+ oauthRoutes(ctx);
3621
+ calendarRoutes(ctx);
3622
+ driveRoutes(ctx);
3623
+ messageRoutes(ctx);
3624
+ draftRoutes(ctx);
3625
+ historyRoutes(ctx);
3626
+ threadRoutes(ctx);
3627
+ labelRoutes(ctx);
3628
+ settingsRoutes(ctx);
3629
+ },
3630
+ seed(store, baseUrl) {
3631
+ seedDefaults(store, baseUrl);
3632
+ }
3633
+ };
3634
+ var index_default = googlePlugin;
3635
+ export {
3636
+ index_default as default,
3637
+ getGoogleStore,
3638
+ googlePlugin,
3639
+ seedFromConfig
3640
+ };
3641
+ //# sourceMappingURL=dist-6EW7SSOZ.js.map