@divizend/scratch-core 1.0.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 (61) hide show
  1. package/basic/demo.ts +11 -0
  2. package/basic/index.ts +490 -0
  3. package/core/Auth.ts +63 -0
  4. package/core/Currency.ts +16 -0
  5. package/core/Env.ts +186 -0
  6. package/core/Fragment.ts +43 -0
  7. package/core/FragmentServingMode.ts +37 -0
  8. package/core/JsonSchemaValidator.ts +173 -0
  9. package/core/ProjectRoot.ts +76 -0
  10. package/core/Scratch.ts +44 -0
  11. package/core/URI.ts +203 -0
  12. package/core/Universe.ts +406 -0
  13. package/core/index.ts +27 -0
  14. package/gsuite/core/GSuite.ts +237 -0
  15. package/gsuite/core/GSuiteAdmin.ts +81 -0
  16. package/gsuite/core/GSuiteOrgConfig.ts +47 -0
  17. package/gsuite/core/GSuiteUser.ts +115 -0
  18. package/gsuite/core/index.ts +21 -0
  19. package/gsuite/documents/Document.ts +173 -0
  20. package/gsuite/documents/Documents.ts +52 -0
  21. package/gsuite/documents/index.ts +19 -0
  22. package/gsuite/drive/Drive.ts +118 -0
  23. package/gsuite/drive/DriveFile.ts +147 -0
  24. package/gsuite/drive/index.ts +19 -0
  25. package/gsuite/gmail/Gmail.ts +430 -0
  26. package/gsuite/gmail/GmailLabel.ts +55 -0
  27. package/gsuite/gmail/GmailMessage.ts +428 -0
  28. package/gsuite/gmail/GmailMessagePart.ts +298 -0
  29. package/gsuite/gmail/GmailThread.ts +97 -0
  30. package/gsuite/gmail/index.ts +5 -0
  31. package/gsuite/gmail/utils.ts +184 -0
  32. package/gsuite/index.ts +28 -0
  33. package/gsuite/spreadsheets/CellValue.ts +71 -0
  34. package/gsuite/spreadsheets/Sheet.ts +128 -0
  35. package/gsuite/spreadsheets/SheetValues.ts +12 -0
  36. package/gsuite/spreadsheets/Spreadsheet.ts +76 -0
  37. package/gsuite/spreadsheets/Spreadsheets.ts +52 -0
  38. package/gsuite/spreadsheets/index.ts +25 -0
  39. package/gsuite/spreadsheets/utils.ts +52 -0
  40. package/gsuite/utils.ts +104 -0
  41. package/http-server/HttpServer.ts +110 -0
  42. package/http-server/NativeHttpServer.ts +1084 -0
  43. package/http-server/index.ts +3 -0
  44. package/http-server/middlewares/01-cors.ts +33 -0
  45. package/http-server/middlewares/02-static.ts +67 -0
  46. package/http-server/middlewares/03-request-logger.ts +159 -0
  47. package/http-server/middlewares/04-body-parser.ts +54 -0
  48. package/http-server/middlewares/05-no-cache.ts +23 -0
  49. package/http-server/middlewares/06-response-handler.ts +39 -0
  50. package/http-server/middlewares/handler-wrapper.ts +250 -0
  51. package/http-server/middlewares/index.ts +37 -0
  52. package/http-server/middlewares/types.ts +27 -0
  53. package/index.ts +24 -0
  54. package/package.json +37 -0
  55. package/queue/EmailQueue.ts +228 -0
  56. package/queue/RateLimiter.ts +54 -0
  57. package/queue/index.ts +2 -0
  58. package/resend/Resend.ts +190 -0
  59. package/resend/index.ts +11 -0
  60. package/s2/S2.ts +335 -0
  61. package/s2/index.ts +11 -0
@@ -0,0 +1,430 @@
1
+ /**
2
+ * Gmail - Email Management and Processing Service
3
+ *
4
+ * The Gmail class provides comprehensive access to Gmail functionality through
5
+ * the Gmail API v1. It handles email operations including reading, sending,
6
+ * labeling, and thread management with efficient pagination and caching.
7
+ *
8
+ * Key Features:
9
+ * - Email listing with pagination and filtering
10
+ * - Thread-based conversation management
11
+ * - Email composition and sending with attachments
12
+ * - Label management and organization
13
+ * - Efficient data fetching with skip/limit support
14
+ *
15
+ * The class implements smart pagination that only fetches full message content
16
+ * for messages that will actually be displayed, optimizing performance for
17
+ * large email accounts.
18
+ *
19
+ * @class Gmail
20
+ * @version 1.0.0
21
+ * @author Divizend GmbH
22
+ */
23
+
24
+ import { google, gmail_v1 } from "googleapis";
25
+ import { JWT } from "google-auth-library";
26
+ import { GmailLabel, GmailMessage, GSuiteUser, Universe } from "../..";
27
+ import { GmailThread } from "./GmailThread";
28
+ import {
29
+ htmlFromText,
30
+ escapeHtml,
31
+ stripHtmlToText,
32
+ encodeDisplayName,
33
+ chunk76,
34
+ base64Url,
35
+ } from "./utils";
36
+
37
+ /**
38
+ * Configuration for Gmail sending operations
39
+ */
40
+ export type GmailSendConfig = {
41
+ /** Email address to send from */
42
+ fromEmail: string;
43
+ /** Display name for the sender */
44
+ fromName: string;
45
+ /** HTML signature to append to emails */
46
+ signatureHtml: string;
47
+ };
48
+
49
+ /**
50
+ * Parameters for Gmail listing operations
51
+ */
52
+ export type GmailListParams = {
53
+ /** Whether to fetch full message content */
54
+ full?: boolean;
55
+ /** Number of items to skip for pagination */
56
+ skip?: number;
57
+ /** Maximum number of items to return */
58
+ limit?: number;
59
+ };
60
+
61
+ /**
62
+ * Parameters for Gmail sending operations
63
+ */
64
+ export type GmailSendParams = {
65
+ /** Recipient email address */
66
+ to: string;
67
+ /** Email subject line */
68
+ subject: string;
69
+ /** Email body content */
70
+ body: string;
71
+ /** Optional file attachments */
72
+ attachments?: {
73
+ filename: string;
74
+ mimeType: string;
75
+ content: Buffer;
76
+ }[];
77
+ };
78
+
79
+ export class Gmail {
80
+ /** Gmail API v1 client instance */
81
+ public readonly gmail: gmail_v1.Gmail;
82
+
83
+ /**
84
+ * Creates a new Gmail instance
85
+ *
86
+ * @param universe - Reference to the central Universe instance
87
+ * @param auth - JWT authentication for the Gmail user
88
+ */
89
+ constructor(private readonly universe: Universe, private auth: JWT) {
90
+ this.gmail = google.gmail({ version: "v1", auth: this.auth });
91
+ }
92
+
93
+ /**
94
+ * Gets the GSuiteUser instance for the authenticated user
95
+ *
96
+ * This provides access to other Google Workspace services
97
+ * for the same user.
98
+ */
99
+ get user(): GSuiteUser {
100
+ return this.universe.gsuite.user(this.email);
101
+ }
102
+
103
+ /**
104
+ * Gets the email address of the authenticated user
105
+ */
106
+ get email(): string {
107
+ return this.auth.subject!;
108
+ }
109
+
110
+ /**
111
+ * Retrieves all Gmail labels for the authenticated user
112
+ *
113
+ * Labels are used to organize and categorize emails.
114
+ * This method fetches the complete label list from Gmail.
115
+ *
116
+ * @returns Promise<GmailLabel[]> - Array of Gmail labels
117
+ * @throws Error if no labels are found or the API call fails
118
+ */
119
+ async getLabels(): Promise<GmailLabel[]> {
120
+ const response = await this.gmail.users.labels.list({
121
+ userId: "me",
122
+ });
123
+
124
+ if (!response.data.labels) {
125
+ throw new Error("No labels found");
126
+ }
127
+
128
+ return response.data.labels.map((label) => new GmailLabel(this, label));
129
+ }
130
+
131
+ /**
132
+ * Finds the ID of a Gmail label by name
133
+ *
134
+ * This method searches through all available labels to find
135
+ * the one matching the specified name, which is useful for
136
+ * filtering operations.
137
+ *
138
+ * @param label - The name of the label to find
139
+ * @returns Promise<string> - The label ID
140
+ * @throws Error if the label is not found
141
+ */
142
+ async getLabelId(label: string): Promise<string> {
143
+ const labels = await this.getLabels();
144
+ const labelId = labels.find((l) => l.name === label)?.id;
145
+ if (!labelId) {
146
+ throw new Error(`Label ${label} not found`);
147
+ }
148
+ return labelId;
149
+ }
150
+
151
+ /**
152
+ * Generic async generator that handles pagination with skip/limit logic
153
+ *
154
+ * This method provides efficient pagination by only fetching full data
155
+ * for items that will actually be yielded, optimizing performance for
156
+ * large datasets.
157
+ *
158
+ * @param messageGenerator - Base generator for the items
159
+ * @param params - Pagination parameters (skip, limit, full)
160
+ * @param fetchFull - Optional function to fetch full item data
161
+ * @returns AsyncGenerator<T> - Paginated items with optional full data
162
+ */
163
+ private async *paginate<T>(
164
+ messageGenerator: AsyncGenerator<T>,
165
+ params: { skip?: number; limit?: number; full?: boolean },
166
+ fetchFull?: (item: T) => Promise<T>
167
+ ): AsyncGenerator<T> {
168
+ let skipped = 0;
169
+ let yielded = 0;
170
+ const limit = params.limit || Infinity;
171
+ const skip = params.skip || 0;
172
+
173
+ for await (const item of messageGenerator) {
174
+ // Skip items until we reach the skip count
175
+ if (skipped < skip) {
176
+ skipped++;
177
+ continue;
178
+ }
179
+
180
+ // Check if we've reached the limit
181
+ if (yielded >= limit) {
182
+ return;
183
+ }
184
+
185
+ // Only fetch full data if needed and for items we'll actually yield
186
+ let finalItem = item;
187
+ if (params.full && fetchFull) {
188
+ finalItem = await fetchFull(item);
189
+ }
190
+
191
+ yielded++;
192
+ yield finalItem;
193
+ }
194
+ }
195
+
196
+ async *listMessages(
197
+ label?: string,
198
+ params?: GmailListParams
199
+ ): AsyncGenerator<GmailMessage> {
200
+ let nextPageToken: string | null | undefined;
201
+ let labelId = label ? await this.getLabelId(label) : undefined;
202
+
203
+ // Create the base message generator without skip/limit
204
+ const baseMessageGenerator = this.createBaseMessageGenerator(
205
+ labelId,
206
+ nextPageToken
207
+ );
208
+
209
+ // Use the pagination helper
210
+ yield* this.paginate(
211
+ baseMessageGenerator,
212
+ params || {},
213
+ async (msg: GmailMessage) => msg.fetch()
214
+ );
215
+ }
216
+
217
+ /**
218
+ * Creates the base message generator without pagination logic
219
+ */
220
+ private async *createBaseMessageGenerator(
221
+ labelId: string | undefined,
222
+ initialPageToken?: string | null
223
+ ): AsyncGenerator<GmailMessage> {
224
+ let nextPageToken = initialPageToken;
225
+
226
+ while (true) {
227
+ const request: gmail_v1.Params$Resource$Users$Messages$List = {
228
+ userId: "me",
229
+ maxResults: 500,
230
+ };
231
+ if (labelId) {
232
+ request.labelIds = [labelId];
233
+ }
234
+ if (nextPageToken) {
235
+ request.pageToken = nextPageToken;
236
+ }
237
+
238
+ const response = await this.gmail.users.messages.list(request);
239
+
240
+ for (const message of response.data.messages || []) {
241
+ yield new GmailMessage(this, message, false);
242
+ }
243
+
244
+ nextPageToken = response.data.nextPageToken;
245
+ if (!nextPageToken) {
246
+ break;
247
+ }
248
+ }
249
+ }
250
+
251
+ async *listThreads(
252
+ label?: string,
253
+ params?: GmailListParams
254
+ ): AsyncGenerator<GmailThread> {
255
+ let nextPageToken: string | null | undefined;
256
+ let labelId = label ? await this.getLabelId(label) : undefined;
257
+
258
+ // Create the base thread generator without skip/limit
259
+ const baseThreadGenerator = this.createBaseThreadGenerator(
260
+ labelId,
261
+ nextPageToken
262
+ );
263
+
264
+ // Use the pagination helper
265
+ yield* this.paginate(
266
+ baseThreadGenerator,
267
+ params || {},
268
+ async (thread: GmailThread) => thread.fetch()
269
+ );
270
+ }
271
+
272
+ /**
273
+ * Creates the base thread generator without pagination logic
274
+ */
275
+ private async *createBaseThreadGenerator(
276
+ labelId: string | undefined,
277
+ initialPageToken?: string | null
278
+ ): AsyncGenerator<GmailThread> {
279
+ let nextPageToken = initialPageToken;
280
+
281
+ while (true) {
282
+ const request: gmail_v1.Params$Resource$Users$Threads$List = {
283
+ userId: "me",
284
+ maxResults: 500,
285
+ };
286
+ if (labelId) {
287
+ request.labelIds = [labelId];
288
+ }
289
+ if (nextPageToken) {
290
+ request.pageToken = nextPageToken;
291
+ }
292
+
293
+ const response = await this.gmail.users.threads.list(request);
294
+
295
+ for (const thread of response.data.threads || []) {
296
+ yield new GmailThread(this, thread, []);
297
+ }
298
+
299
+ nextPageToken = response.data.nextPageToken;
300
+ if (!nextPageToken) {
301
+ break;
302
+ }
303
+ }
304
+ }
305
+
306
+ async getSendConfig(): Promise<GmailSendConfig> {
307
+ let fromEmail = this.email!;
308
+ let fromName = "";
309
+ let signatureHtml = "";
310
+
311
+ // Try Gmail Send-As for fromEmail, displayName, and signature
312
+ const sendAsList = await this.gmail.users.settings.sendAs.list({
313
+ userId: "me",
314
+ });
315
+ const sendAs =
316
+ sendAsList.data.sendAs?.find((s) => s.isDefault) ||
317
+ sendAsList.data.sendAs?.find((s) => s.sendAsEmail === this.email) ||
318
+ sendAsList.data.sendAs?.[0];
319
+
320
+ if (sendAs?.sendAsEmail) {
321
+ fromEmail = sendAs.sendAsEmail;
322
+ }
323
+ if (sendAs?.displayName && sendAs.displayName.trim()) {
324
+ fromName = sendAs.displayName.trim();
325
+ }
326
+ if (sendAs?.signature) {
327
+ signatureHtml = sendAs.signature;
328
+ }
329
+
330
+ // Fallback: resolve name via Admin Directory
331
+ if (!fromName) {
332
+ const directoryInfo = this.user.directoryData;
333
+ const dirName =
334
+ directoryInfo.name?.fullName ||
335
+ [directoryInfo.name?.givenName, directoryInfo.name?.familyName]
336
+ .filter(Boolean)
337
+ .join(" ");
338
+ if (dirName && dirName.trim()) {
339
+ fromName = dirName.trim();
340
+ }
341
+ }
342
+
343
+ if (!fromName) {
344
+ throw new Error("From name not found");
345
+ }
346
+
347
+ return { fromEmail, fromName, signatureHtml };
348
+ }
349
+
350
+ async send({ to, subject, body, attachments }: GmailSendParams) {
351
+ const sendConfig = await this.getSendConfig();
352
+
353
+ const encodedSubject = `=?UTF-8?B?${Buffer.from(subject, "utf8").toString(
354
+ "base64"
355
+ )}?=`;
356
+
357
+ const emailText = (body + "\n" + sendConfig.fromName).trimEnd();
358
+
359
+ // HTML with bold contract; signature preceded by blank line + `--`
360
+ const htmlWithoutSig = htmlFromText(emailText);
361
+ const htmlSignature = sendConfig.signatureHtml
362
+ ? `<div><br>--<br></div>${sendConfig.signatureHtml}`
363
+ : "";
364
+ const textSignature = sendConfig.signatureHtml
365
+ ? `\n\n--\n${stripHtmlToText(sendConfig.signatureHtml)}`
366
+ : "";
367
+
368
+ const htmlBody = htmlWithoutSig + htmlSignature;
369
+ const textBody = emailText + textSignature;
370
+
371
+ // MIME boundaries
372
+ const boundaryMixed = "mixed_" + Date.now().toString(36);
373
+ const boundaryAlt = "alt_" + Math.random().toString(36).slice(2);
374
+
375
+ // From header (include name if available)
376
+ const fromHeader = sendConfig.fromName
377
+ ? `${encodeDisplayName(sendConfig.fromName)} <${sendConfig.fromEmail}>`
378
+ : `<${sendConfig.fromEmail}>`;
379
+
380
+ // Compose message
381
+ const lines: string[] = [
382
+ `From: ${fromHeader}`,
383
+ `To: <${to}>`,
384
+ `Subject: ${encodedSubject}`,
385
+ "MIME-Version: 1.0",
386
+ `Date: ${new Date().toUTCString()}`,
387
+ `Content-Type: multipart/mixed; boundary="${boundaryMixed}"`,
388
+ "",
389
+ `--${boundaryMixed}`,
390
+ `Content-Type: multipart/alternative; boundary="${boundaryAlt}"`,
391
+ "",
392
+ `--${boundaryAlt}`,
393
+ "Content-Type: text/plain; charset=UTF-8",
394
+ "Content-Transfer-Encoding: 8bit",
395
+ "",
396
+ textBody,
397
+ "",
398
+ `--${boundaryAlt}`,
399
+ "Content-Type: text/html; charset=UTF-8",
400
+ "Content-Transfer-Encoding: 8bit",
401
+ "",
402
+ htmlBody,
403
+ "",
404
+ `--${boundaryAlt}--`,
405
+ "",
406
+ `--${boundaryMixed}`,
407
+ ];
408
+
409
+ for (const attachment of attachments || []) {
410
+ const attachmentChunked = chunk76(attachment.content.toString("base64"));
411
+ lines.push(
412
+ `Content-Type: ${attachment.mimeType}; name="${attachment.filename}"`,
413
+ "Content-Transfer-Encoding: base64",
414
+ `Content-Disposition: attachment; filename="${attachment.filename}"`,
415
+ "",
416
+ attachmentChunked,
417
+ `--${boundaryMixed}--`
418
+ );
419
+ }
420
+
421
+ lines.push("");
422
+
423
+ // Send
424
+ const rawMessage = base64Url(lines.join("\r\n"));
425
+ await this.gmail.users.messages.send({
426
+ userId: "me",
427
+ requestBody: { raw: rawMessage },
428
+ });
429
+ }
430
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * GmailLabel - Email Label Management
3
+ *
4
+ * The GmailLabel class represents a Gmail label (category) that can be used
5
+ * to organize and filter emails. Labels provide a way to categorize messages
6
+ * beyond the standard Gmail folders like Inbox, Sent, and Trash.
7
+ *
8
+ * Labels can be:
9
+ * - System labels (Inbox, Sent, Drafts, etc.)
10
+ * - User-created labels for custom organization
11
+ * - Nested labels for hierarchical organization
12
+ *
13
+ * This class provides a simple interface for accessing label properties
14
+ * and integrating with the broader Gmail management system.
15
+ *
16
+ * @class GmailLabel
17
+ * @version 1.0.0
18
+ * @author Divizend GmbH
19
+ */
20
+
21
+ import { gmail_v1 } from "googleapis";
22
+ import { Gmail } from "../..";
23
+
24
+ export class GmailLabel {
25
+ /**
26
+ * Creates a new GmailLabel instance
27
+ *
28
+ * @param gmail - Reference to the Gmail service instance
29
+ * @param label - Raw Gmail API label data
30
+ */
31
+ constructor(
32
+ private readonly gmail: Gmail,
33
+ public readonly label: gmail_v1.Schema$Label
34
+ ) {}
35
+
36
+ /**
37
+ * Gets the display name of the label
38
+ *
39
+ * This is the human-readable name that appears in the Gmail interface
40
+ * and is used for filtering and organization.
41
+ */
42
+ get name() {
43
+ return this.label.name;
44
+ }
45
+
46
+ /**
47
+ * Gets the unique identifier for the label
48
+ *
49
+ * The label ID is used internally by Gmail for API operations
50
+ * and should not be displayed to users.
51
+ */
52
+ get id() {
53
+ return this.label.id;
54
+ }
55
+ }