@1kbirds/chidori-mock-gmail 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +85 -0
  2. package/dist/api.d.ts +14 -0
  3. package/dist/api.d.ts.map +1 -0
  4. package/dist/api.js +139 -0
  5. package/dist/client.d.ts +228 -0
  6. package/dist/client.d.ts.map +1 -0
  7. package/dist/client.js +50 -0
  8. package/dist/errors.d.ts +19 -0
  9. package/dist/errors.d.ts.map +1 -0
  10. package/dist/errors.js +25 -0
  11. package/dist/index.d.ts +10 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +9 -0
  14. package/dist/seed.d.ts +6 -0
  15. package/dist/seed.d.ts.map +1 -0
  16. package/dist/seed.js +127 -0
  17. package/dist/service.d.ts +41 -0
  18. package/dist/service.d.ts.map +1 -0
  19. package/dist/service.js +372 -0
  20. package/dist/state.d.ts +22 -0
  21. package/dist/state.d.ts.map +1 -0
  22. package/dist/state.js +139 -0
  23. package/dist/types.d.ts +123 -0
  24. package/dist/types.d.ts.map +1 -0
  25. package/dist/types.js +1 -0
  26. package/dist/ui/GmailMockApp.d.ts +7 -0
  27. package/dist/ui/GmailMockApp.d.ts.map +1 -0
  28. package/dist/ui/GmailMockApp.js +93 -0
  29. package/dist/ui/dev.d.ts +2 -0
  30. package/dist/ui/dev.d.ts.map +1 -0
  31. package/dist/ui/dev.js +11 -0
  32. package/dist/ui/index.d.ts +3 -0
  33. package/dist/ui/index.d.ts.map +1 -0
  34. package/dist/ui/index.js +1 -0
  35. package/dist/ui/styles.css +340 -0
  36. package/package.json +56 -0
  37. package/src/__tests__/service.test.ts +120 -0
  38. package/src/api.ts +157 -0
  39. package/src/client.ts +54 -0
  40. package/src/errors.ts +29 -0
  41. package/src/index.ts +12 -0
  42. package/src/seed.ts +143 -0
  43. package/src/service.ts +405 -0
  44. package/src/state.ts +159 -0
  45. package/src/types.ts +149 -0
  46. package/src/ui/GmailMockApp.tsx +236 -0
  47. package/src/ui/dev.tsx +16 -0
  48. package/src/ui/index.ts +2 -0
  49. package/src/ui/styles.css +340 -0
package/dist/seed.js ADDED
@@ -0,0 +1,127 @@
1
+ import { encodeBase64Url } from "./service";
2
+ import { defaultLabels } from "./state";
3
+ export const demoUser = {
4
+ id: "me",
5
+ emailAddress: "ada@example.com",
6
+ displayName: "Ada Lovelace",
7
+ };
8
+ function message(input) {
9
+ const threadId = input.threadId ?? input.id;
10
+ return {
11
+ id: input.id,
12
+ threadId,
13
+ labelIds: input.labels,
14
+ snippet: input.body.replace(/\s+/g, " ").slice(0, 160),
15
+ historyId: "1",
16
+ internalDate: String(new Date(input.date).getTime()),
17
+ sizeEstimate: input.body.length,
18
+ attachments: input.attachments,
19
+ payload: {
20
+ partId: "",
21
+ mimeType: "text/plain",
22
+ filename: "",
23
+ headers: [
24
+ { name: "From", value: input.from },
25
+ { name: "To", value: input.to },
26
+ { name: "Subject", value: input.subject },
27
+ { name: "Date", value: new Date(input.date).toUTCString() },
28
+ ],
29
+ body: {
30
+ size: input.body.length,
31
+ data: encodeBase64Url(input.body),
32
+ },
33
+ parts: input.attachments?.map((attachment, index) => ({
34
+ partId: String(index + 1),
35
+ mimeType: attachment.mimeType,
36
+ filename: attachment.filename,
37
+ headers: [{ name: "Content-Disposition", value: `attachment; filename="${attachment.filename}"` }],
38
+ body: { attachmentId: attachment.id, size: attachment.size, data: attachment.data },
39
+ })),
40
+ },
41
+ };
42
+ }
43
+ export const demoMessages = [
44
+ message({
45
+ id: "msg_schedule_request",
46
+ labels: ["INBOX", "UNREAD", "IMPORTANT", "CATEGORY_PRIMARY"],
47
+ from: "morgan@example.com",
48
+ to: "ada@example.com",
49
+ subject: "Can we meet this week?",
50
+ body: "Hi Ada, can we meet this week to review the launch timeline? Tuesday or Wednesday afternoon would work for me.",
51
+ date: "2026-06-01T16:10:00.000Z",
52
+ }),
53
+ message({
54
+ id: "msg_launch_brief",
55
+ labels: ["INBOX", "CATEGORY_PRIMARY"],
56
+ from: "grace@example.com",
57
+ to: "ada@example.com",
58
+ subject: "Launch brief attached",
59
+ body: "Attached is the current launch brief. Please review the owner list before our planning session.",
60
+ date: "2026-06-01T15:40:00.000Z",
61
+ attachments: [{ id: "att_launch_brief", filename: "launch-brief.txt", mimeType: "text/plain", size: 128, data: encodeBase64Url("Launch brief fixture") }],
62
+ }),
63
+ message({
64
+ id: "msg_reply_thread_1",
65
+ threadId: "thread_customer",
66
+ labels: ["INBOX", "CATEGORY_PRIMARY"],
67
+ from: "customer@example.com",
68
+ to: "ada@example.com",
69
+ subject: "Re: Proposal questions",
70
+ body: "Thanks for the proposal. Could you clarify implementation timing and support coverage?",
71
+ date: "2026-05-31T18:00:00.000Z",
72
+ }),
73
+ message({
74
+ id: "msg_reply_thread_2",
75
+ threadId: "thread_customer",
76
+ labels: ["SENT"],
77
+ from: "ada@example.com",
78
+ to: "customer@example.com",
79
+ subject: "Re: Proposal questions",
80
+ body: "Happy to clarify. Implementation typically takes two weeks and support coverage is included.",
81
+ date: "2026-05-31T19:00:00.000Z",
82
+ }),
83
+ message({
84
+ id: "msg_newsletter",
85
+ labels: ["TRASH"],
86
+ from: "newsletter@example.com",
87
+ to: "ada@example.com",
88
+ subject: "Weekly digest",
89
+ body: "A weekly digest that has been moved to trash.",
90
+ date: "2026-05-30T12:00:00.000Z",
91
+ }),
92
+ ];
93
+ export const demoDrafts = [
94
+ {
95
+ id: "draft_followup",
96
+ message: message({
97
+ id: "msg_draft_followup",
98
+ labels: ["DRAFT"],
99
+ from: "ada@example.com",
100
+ to: "morgan@example.com",
101
+ subject: "Re: Can we meet this week?",
102
+ body: "Hi Morgan, Tuesday afternoon works. I can send a calendar invite.",
103
+ date: "2026-06-01T16:20:00.000Z",
104
+ }),
105
+ },
106
+ ];
107
+ export const demoSeed = {
108
+ users: [demoUser],
109
+ currentUserId: demoUser.id,
110
+ labels: [
111
+ ...defaultLabels(),
112
+ {
113
+ id: "Label_customers",
114
+ name: "Customers",
115
+ type: "user",
116
+ messageListVisibility: "show",
117
+ labelListVisibility: "labelShow",
118
+ color: { textColor: "#ffffff", backgroundColor: "#0b8043" },
119
+ messagesTotal: 0,
120
+ messagesUnread: 0,
121
+ threadsTotal: 0,
122
+ threadsUnread: 0,
123
+ },
124
+ ],
125
+ messages: demoMessages,
126
+ drafts: demoDrafts,
127
+ };
@@ -0,0 +1,41 @@
1
+ import { type GmailMockStore } from "./state";
2
+ import type { GmailDraft, GmailLabel, GmailListResponse, GmailMessage, GmailMockOptions, GmailModifyRequest, GmailProfile, GmailSearchOptions, GmailSendRequest, GmailSnapshot, GmailThread } from "./types";
3
+ export declare class GmailMock {
4
+ readonly store: GmailMockStore;
5
+ private readonly initialSnapshot;
6
+ constructor(options?: GmailMockOptions);
7
+ subscribe(listener: () => void): () => void;
8
+ snapshot(): GmailSnapshot;
9
+ reset(seed?: GmailSnapshot): void;
10
+ getProfile(): GmailProfile;
11
+ listLabels(): GmailListResponse<GmailLabel>;
12
+ getLabel(id: string): GmailLabel;
13
+ createLabel(input: Partial<GmailLabel>): GmailLabel;
14
+ updateLabel(id: string, input: Partial<GmailLabel>): GmailLabel;
15
+ deleteLabel(id: string): void;
16
+ listMessages(options?: GmailSearchOptions): GmailListResponse<Pick<GmailMessage, "id" | "threadId">>;
17
+ getMessage(id: string): GmailMessage;
18
+ insertMessage(input: Partial<GmailMessage> | GmailSendRequest): GmailMessage;
19
+ send(input: GmailSendRequest): GmailMessage;
20
+ modifyMessage(id: string, request: GmailModifyRequest): GmailMessage;
21
+ trashMessage(id: string): GmailMessage;
22
+ untrashMessage(id: string): GmailMessage;
23
+ deleteMessage(id: string): void;
24
+ listThreads(options?: GmailSearchOptions): GmailListResponse<Pick<GmailThread, "id" | "historyId" | "snippet">>;
25
+ getThread(id: string): GmailThread;
26
+ modifyThread(id: string, request: GmailModifyRequest): GmailThread;
27
+ trashThread(id: string): GmailThread;
28
+ listDrafts(): GmailListResponse<Pick<GmailDraft, "id" | "message">>;
29
+ getDraft(id: string): GmailDraft;
30
+ createDraft(input: GmailSendRequest): GmailDraft;
31
+ updateDraft(id: string, input: GmailSendRequest): GmailDraft;
32
+ sendDraft(id: string): GmailMessage;
33
+ deleteDraft(id: string): void;
34
+ private buildMessage;
35
+ private visibleMessages;
36
+ private requireMessage;
37
+ private currentUser;
38
+ }
39
+ export declare function encodeBase64Url(value: string): string;
40
+ export declare function decodeBase64Url(value: string): string;
41
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AACA,OAAO,EAAkE,KAAK,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9G,OAAO,KAAK,EACV,UAAU,EACV,UAAU,EACV,iBAAiB,EACjB,YAAY,EAEZ,gBAAgB,EAChB,kBAAkB,EAClB,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,EAChB,aAAa,EACb,WAAW,EACZ,MAAM,SAAS,CAAC;AAEjB,qBAAa,SAAS;IACpB,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAC/B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAgB;gBAEpC,OAAO,GAAE,gBAAqB;IAK1C,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAK3C,QAAQ,IAAI,aAAa;IAIzB,KAAK,CAAC,IAAI,CAAC,EAAE,aAAa,GAAG,IAAI;IAIjC,UAAU,IAAI,YAAY;IAU1B,UAAU,IAAI,iBAAiB,CAAC,UAAU,CAAC;IAO3C,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU;IAMhC,WAAW,CAAC,KAAK,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,UAAU;IAsBnD,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,UAAU;IAS/D,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAW7B,YAAY,CAAC,OAAO,GAAE,kBAAuB,GAAG,iBAAiB,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,GAAG,UAAU,CAAC,CAAC;IAKxG,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY;IAIpC,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,gBAAgB,GAAG,YAAY;IAU5E,IAAI,CAAC,KAAK,EAAE,gBAAgB,GAAG,YAAY;IAW3C,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,kBAAkB,GAAG,YAAY;IAQpE,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY;IAQtC,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY;IAQxC,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAU/B,WAAW,CAAC,OAAO,GAAE,kBAAuB,GAAG,iBAAiB,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,GAAG,WAAW,GAAG,SAAS,CAAC,CAAC;IASnH,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,WAAW;IAQlC,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,kBAAkB,GAAG,WAAW;IAQlE,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,WAAW;IAMpC,UAAU,IAAI,iBAAiB,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,GAAG,SAAS,CAAC,CAAC;IAKnE,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU;IAMhC,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,UAAU;IAShD,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,GAAG,UAAU;IAU5D,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY;IASnC,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAQ7B,OAAO,CAAC,YAAY;IA0CpB,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,cAAc;IAMtB,OAAO,CAAC,WAAW;CAKpB;AAiFD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOrD;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKrD"}
@@ -0,0 +1,372 @@
1
+ import { badRequest, notFound } from "./errors";
2
+ import { applySnapshot, bumpHistory, clone, createStore, emit, snapshot } from "./state";
3
+ export class GmailMock {
4
+ store;
5
+ initialSnapshot;
6
+ constructor(options = {}) {
7
+ this.store = createStore(options);
8
+ this.initialSnapshot = snapshot(this.store);
9
+ }
10
+ subscribe(listener) {
11
+ this.store.listeners.add(listener);
12
+ return () => this.store.listeners.delete(listener);
13
+ }
14
+ snapshot() {
15
+ return snapshot(this.store);
16
+ }
17
+ reset(seed) {
18
+ applySnapshot(this.store, seed ?? this.initialSnapshot);
19
+ }
20
+ getProfile() {
21
+ const user = this.currentUser();
22
+ return {
23
+ emailAddress: user.emailAddress,
24
+ messagesTotal: this.visibleMessages({ includeSpamTrash: true }).length,
25
+ threadsTotal: this.listThreads({ includeSpamTrash: true }).threads?.length ?? 0,
26
+ historyId: String(this.store.historyId),
27
+ };
28
+ }
29
+ listLabels() {
30
+ return {
31
+ resultSizeEstimate: this.store.labels.size,
32
+ labels: Array.from(this.store.labels.values()).map(clone),
33
+ };
34
+ }
35
+ getLabel(id) {
36
+ const label = this.store.labels.get(id);
37
+ if (!label)
38
+ throw notFound(`Label not found: ${id}`);
39
+ return clone(label);
40
+ }
41
+ createLabel(input) {
42
+ if (!input.name)
43
+ throw badRequest("Missing required field: name");
44
+ const id = input.id ?? `Label_${this.store.nextId("label")}`;
45
+ if (this.store.labels.has(id))
46
+ throw badRequest(`Label already exists: ${id}`);
47
+ const label = {
48
+ id,
49
+ name: input.name,
50
+ type: "user",
51
+ messageListVisibility: input.messageListVisibility ?? "show",
52
+ labelListVisibility: input.labelListVisibility ?? "labelShow",
53
+ color: input.color,
54
+ messagesTotal: 0,
55
+ messagesUnread: 0,
56
+ threadsTotal: 0,
57
+ threadsUnread: 0,
58
+ };
59
+ this.store.labels.set(id, label);
60
+ bumpHistory(this.store);
61
+ emit(this.store);
62
+ return clone(label);
63
+ }
64
+ updateLabel(id, input) {
65
+ const existing = this.getLabel(id);
66
+ const next = { ...existing, ...input, id, type: existing.type };
67
+ this.store.labels.set(id, next);
68
+ bumpHistory(this.store);
69
+ emit(this.store);
70
+ return clone(next);
71
+ }
72
+ deleteLabel(id) {
73
+ const existing = this.getLabel(id);
74
+ if (existing.type === "system")
75
+ throw badRequest("System labels cannot be deleted");
76
+ this.store.labels.delete(id);
77
+ for (const message of this.store.messages.values()) {
78
+ message.labelIds = message.labelIds.filter((labelId) => labelId !== id);
79
+ }
80
+ bumpHistory(this.store);
81
+ emit(this.store);
82
+ }
83
+ listMessages(options = {}) {
84
+ const messages = this.visibleMessages(options).map((message) => ({ id: message.id, threadId: message.threadId }));
85
+ return { resultSizeEstimate: messages.length, messages };
86
+ }
87
+ getMessage(id) {
88
+ return clone(this.requireMessage(id));
89
+ }
90
+ insertMessage(input) {
91
+ const message = this.buildMessage(input, {
92
+ labels: normalizeLabels(input.labelIds ?? ["INBOX", "UNREAD", "CATEGORY_PRIMARY"]),
93
+ threadId: input.threadId,
94
+ });
95
+ this.store.messages.set(message.id, message);
96
+ emit(this.store);
97
+ return clone(message);
98
+ }
99
+ send(input) {
100
+ const message = this.buildMessage(input, {
101
+ labels: ["SENT"],
102
+ threadId: input.message?.threadId,
103
+ fromCurrentUser: true,
104
+ });
105
+ this.store.messages.set(message.id, message);
106
+ emit(this.store);
107
+ return clone(message);
108
+ }
109
+ modifyMessage(id, request) {
110
+ const message = this.requireMessage(id);
111
+ message.labelIds = modifyLabels(message.labelIds, request);
112
+ message.historyId = bumpHistory(this.store);
113
+ emit(this.store);
114
+ return clone(message);
115
+ }
116
+ trashMessage(id) {
117
+ const message = this.requireMessage(id);
118
+ message.labelIds = modifyLabels(message.labelIds, { addLabelIds: ["TRASH"], removeLabelIds: ["INBOX", "SPAM"] });
119
+ message.historyId = bumpHistory(this.store);
120
+ emit(this.store);
121
+ return clone(message);
122
+ }
123
+ untrashMessage(id) {
124
+ const message = this.requireMessage(id);
125
+ message.labelIds = modifyLabels(message.labelIds, { addLabelIds: ["INBOX"], removeLabelIds: ["TRASH"] });
126
+ message.historyId = bumpHistory(this.store);
127
+ emit(this.store);
128
+ return clone(message);
129
+ }
130
+ deleteMessage(id) {
131
+ this.requireMessage(id);
132
+ this.store.messages.delete(id);
133
+ for (const [draftId, draft] of this.store.drafts) {
134
+ if (draft.message.id === id)
135
+ this.store.drafts.delete(draftId);
136
+ }
137
+ bumpHistory(this.store);
138
+ emit(this.store);
139
+ }
140
+ listThreads(options = {}) {
141
+ const threads = groupThreads(this.visibleMessages(options)).map((thread) => ({
142
+ id: thread.id,
143
+ historyId: thread.historyId,
144
+ snippet: thread.snippet,
145
+ }));
146
+ return { resultSizeEstimate: threads.length, threads };
147
+ }
148
+ getThread(id) {
149
+ const messages = Array.from(this.store.messages.values())
150
+ .filter((message) => message.threadId === id)
151
+ .sort((a, b) => Number(a.internalDate) - Number(b.internalDate));
152
+ if (messages.length === 0)
153
+ throw notFound(`Thread not found: ${id}`);
154
+ return clone(groupThreads(messages)[0]);
155
+ }
156
+ modifyThread(id, request) {
157
+ const thread = this.getThread(id);
158
+ for (const message of thread.messages) {
159
+ this.modifyMessage(message.id, request);
160
+ }
161
+ return this.getThread(id);
162
+ }
163
+ trashThread(id) {
164
+ const thread = this.getThread(id);
165
+ for (const message of thread.messages)
166
+ this.trashMessage(message.id);
167
+ return this.getThread(id);
168
+ }
169
+ listDrafts() {
170
+ const drafts = Array.from(this.store.drafts.values()).map((draft) => ({ id: draft.id, message: clone(draft.message) }));
171
+ return { resultSizeEstimate: drafts.length, drafts };
172
+ }
173
+ getDraft(id) {
174
+ const draft = this.store.drafts.get(id);
175
+ if (!draft)
176
+ throw notFound(`Draft not found: ${id}`);
177
+ return clone(draft);
178
+ }
179
+ createDraft(input) {
180
+ const message = this.buildMessage(input, { labels: ["DRAFT"], threadId: input.message?.threadId, fromCurrentUser: true });
181
+ const draft = { id: this.store.nextId("draft"), message };
182
+ this.store.drafts.set(draft.id, draft);
183
+ this.store.messages.set(message.id, message);
184
+ emit(this.store);
185
+ return clone(draft);
186
+ }
187
+ updateDraft(id, input) {
188
+ const existing = this.getDraft(id);
189
+ const message = this.buildMessage(input, { labels: ["DRAFT"], threadId: input.message?.threadId ?? existing.message.threadId, fromCurrentUser: true, id: existing.message.id });
190
+ const draft = { id, message };
191
+ this.store.drafts.set(id, draft);
192
+ this.store.messages.set(message.id, message);
193
+ emit(this.store);
194
+ return clone(draft);
195
+ }
196
+ sendDraft(id) {
197
+ const draft = this.getDraft(id);
198
+ this.store.drafts.delete(id);
199
+ const message = { ...draft.message, labelIds: ["SENT"], historyId: bumpHistory(this.store), internalDate: String(this.store.now().getTime()) };
200
+ this.store.messages.set(message.id, message);
201
+ emit(this.store);
202
+ return clone(message);
203
+ }
204
+ deleteDraft(id) {
205
+ const draft = this.getDraft(id);
206
+ this.store.drafts.delete(id);
207
+ this.store.messages.delete(draft.message.id);
208
+ bumpHistory(this.store);
209
+ emit(this.store);
210
+ }
211
+ buildMessage(input, options) {
212
+ const now = this.store.now();
213
+ const user = this.currentUser();
214
+ const id = options.id ?? input.id ?? this.store.nextId("msg");
215
+ const threadId = options.threadId ?? id;
216
+ const headers = buildHeaders(input, user.emailAddress, options.fromCurrentUser);
217
+ const bodyText = extractBody(input);
218
+ const payload = input.payload ?? {
219
+ partId: "",
220
+ mimeType: "text/plain",
221
+ filename: "",
222
+ headers,
223
+ body: {
224
+ size: bodyText.length,
225
+ data: encodeBase64Url(bodyText),
226
+ },
227
+ parts: (input.attachments ?? []).map((attachment, index) => ({
228
+ partId: String(index + 1),
229
+ mimeType: attachment.mimeType,
230
+ filename: attachment.filename,
231
+ headers: [
232
+ { name: "Content-Type", value: attachment.mimeType },
233
+ { name: "Content-Disposition", value: `attachment; filename="${attachment.filename}"` },
234
+ ],
235
+ body: { attachmentId: attachment.id, size: attachment.size, data: attachment.data },
236
+ })),
237
+ };
238
+ const message = {
239
+ id,
240
+ threadId,
241
+ labelIds: normalizeLabels(options.labels),
242
+ snippet: makeSnippet(bodyText || headerValue(headers, "Subject")),
243
+ historyId: bumpHistory(this.store),
244
+ internalDate: input.internalDate ?? String(now.getTime()),
245
+ payload,
246
+ sizeEstimate: input.sizeEstimate ?? bodyText.length + JSON.stringify(headers).length,
247
+ raw: input.raw ?? input.raw,
248
+ attachments: input.attachments ?? input.attachments,
249
+ };
250
+ return message;
251
+ }
252
+ visibleMessages(options) {
253
+ return Array.from(this.store.messages.values())
254
+ .filter((message) => options.includeSpamTrash || (!message.labelIds.includes("TRASH") && !message.labelIds.includes("SPAM")))
255
+ .filter((message) => !options.labelIds?.length || options.labelIds.every((labelId) => message.labelIds.includes(labelId)))
256
+ .filter((message) => matchesQuery(message, options.q))
257
+ .sort((a, b) => Number(b.internalDate) - Number(a.internalDate))
258
+ .slice(0, options.maxResults ?? undefined)
259
+ .map(clone);
260
+ }
261
+ requireMessage(id) {
262
+ const message = this.store.messages.get(id);
263
+ if (!message)
264
+ throw notFound(`Message not found: ${id}`);
265
+ return message;
266
+ }
267
+ currentUser() {
268
+ const user = this.store.users.get(this.store.currentUserId);
269
+ if (!user)
270
+ throw notFound(`User not found: ${this.store.currentUserId}`);
271
+ return user;
272
+ }
273
+ }
274
+ function normalizeLabels(labelIds) {
275
+ return Array.from(new Set(labelIds));
276
+ }
277
+ function modifyLabels(current, request) {
278
+ const next = new Set(current);
279
+ for (const labelId of request.removeLabelIds ?? [])
280
+ next.delete(labelId);
281
+ for (const labelId of request.addLabelIds ?? [])
282
+ next.add(labelId);
283
+ return Array.from(next);
284
+ }
285
+ function groupThreads(messages) {
286
+ const byThread = new Map();
287
+ for (const message of messages) {
288
+ if (!byThread.has(message.threadId))
289
+ byThread.set(message.threadId, []);
290
+ byThread.get(message.threadId).push(message);
291
+ }
292
+ return Array.from(byThread.entries()).map(([id, threadMessages]) => {
293
+ const sorted = threadMessages.sort((a, b) => Number(a.internalDate) - Number(b.internalDate));
294
+ const latest = sorted[sorted.length - 1];
295
+ return {
296
+ id,
297
+ historyId: latest.historyId,
298
+ snippet: latest.snippet,
299
+ messages: sorted.map(clone),
300
+ };
301
+ });
302
+ }
303
+ function buildHeaders(input, currentEmail, fromCurrentUser) {
304
+ const existing = input.payload?.headers;
305
+ if (existing?.length)
306
+ return existing.map(clone);
307
+ const send = input;
308
+ return [
309
+ { name: "From", value: fromCurrentUser ? currentEmail : send.to?.[0] ?? "sender@example.com" },
310
+ { name: "To", value: send.to?.join(", ") ?? currentEmail },
311
+ ...(send.cc?.length ? [{ name: "Cc", value: send.cc.join(", ") }] : []),
312
+ { name: "Subject", value: send.subject ?? "(no subject)" },
313
+ { name: "Date", value: new Date().toUTCString() },
314
+ ];
315
+ }
316
+ function extractBody(input) {
317
+ const sendBody = input.body;
318
+ if (sendBody !== undefined)
319
+ return sendBody;
320
+ const data = input.payload?.body.data;
321
+ return data ? decodeBase64Url(data) : "";
322
+ }
323
+ function headerValue(headers, name) {
324
+ return headers.find((header) => header.name.toLowerCase() === name.toLowerCase())?.value ?? "";
325
+ }
326
+ function makeSnippet(value) {
327
+ return value.replace(/\s+/g, " ").trim().slice(0, 160);
328
+ }
329
+ function matchesQuery(message, query) {
330
+ if (!query)
331
+ return true;
332
+ const terms = query.match(/(?:[^\s"]+|"[^"]*")+/g) ?? [];
333
+ return terms.every((term) => matchesTerm(message, term.replace(/^"|"$/g, "")));
334
+ }
335
+ function matchesTerm(message, term) {
336
+ const headers = message.payload.headers;
337
+ const lower = term.toLowerCase();
338
+ if (lower.startsWith("from:"))
339
+ return headerValue(headers, "From").toLowerCase().includes(lower.slice(5));
340
+ if (lower.startsWith("to:"))
341
+ return headerValue(headers, "To").toLowerCase().includes(lower.slice(3));
342
+ if (lower.startsWith("subject:"))
343
+ return headerValue(headers, "Subject").toLowerCase().includes(lower.slice(8));
344
+ if (lower.startsWith("label:"))
345
+ return message.labelIds.some((label) => label.toLowerCase() === lower.slice(6));
346
+ if (lower === "is:unread")
347
+ return message.labelIds.includes("UNREAD");
348
+ if (lower === "is:read")
349
+ return !message.labelIds.includes("UNREAD");
350
+ if (lower === "in:inbox")
351
+ return message.labelIds.includes("INBOX");
352
+ if (lower === "in:sent")
353
+ return message.labelIds.includes("SENT");
354
+ if (lower === "in:trash")
355
+ return message.labelIds.includes("TRASH");
356
+ const haystack = [message.snippet, decodeBase64Url(message.payload.body.data ?? ""), ...headers.map((header) => header.value)].join(" ").toLowerCase();
357
+ return haystack.includes(lower);
358
+ }
359
+ export function encodeBase64Url(value) {
360
+ const bytes = new TextEncoder().encode(value);
361
+ let binary = "";
362
+ for (const byte of bytes) {
363
+ binary += String.fromCharCode(byte);
364
+ }
365
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
366
+ }
367
+ export function decodeBase64Url(value) {
368
+ const padded = value.replaceAll("-", "+").replaceAll("_", "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
369
+ const binary = atob(padded);
370
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
371
+ return new TextDecoder().decode(bytes);
372
+ }
@@ -0,0 +1,22 @@
1
+ import type { GmailDraft, GmailLabel, GmailMessage, GmailMockOptions, GmailSeed, GmailSnapshot, GmailUser } from "./types";
2
+ export interface GmailMockStore {
3
+ users: Map<string, GmailUser>;
4
+ currentUserId: string;
5
+ labels: Map<string, GmailLabel>;
6
+ messages: Map<string, GmailMessage>;
7
+ drafts: Map<string, GmailDraft>;
8
+ historyId: number;
9
+ now(): Date;
10
+ nextId(prefix: string): string;
11
+ listeners: Set<() => void>;
12
+ }
13
+ export declare function createStore(options?: GmailMockOptions): GmailMockStore;
14
+ export declare function defaultLabels(): GmailLabel[];
15
+ export declare function snapshot(store: GmailMockStore): GmailSnapshot;
16
+ export declare function applySnapshot(store: GmailMockStore, next: GmailSnapshot): void;
17
+ export declare function applySeed(store: GmailMockStore, seed: GmailSeed): void;
18
+ export declare function bumpHistory(store: GmailMockStore): string;
19
+ export declare function emit(store: GmailMockStore): void;
20
+ export declare function recomputeLabelStats(store: GmailMockStore): void;
21
+ export declare function clone<T>(value: T): T;
22
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,gBAAgB,EAAE,SAAS,EAAE,aAAa,EAAE,SAAS,EAAiB,MAAM,SAAS,CAAC;AAE1I,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAChC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACpC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,IAAI,IAAI,CAAC;IACZ,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,SAAS,EAAE,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC;CAC5B;AAoBD,wBAAgB,WAAW,CAAC,OAAO,GAAE,gBAAqB,GAAG,cAAc,CA2B1E;AAED,wBAAgB,aAAa,IAAI,UAAU,EAAE,CAY5C;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,cAAc,GAAG,aAAa,CAU7D;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,aAAa,GAAG,IAAI,CAS9E;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,SAAS,GAAG,IAAI,CAStE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAGzD;AAED,wBAAgB,IAAI,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI,CAKhD;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI,CAiC/D;AAED,wBAAgB,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAEpC"}