@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.
- package/basic/demo.ts +11 -0
- package/basic/index.ts +490 -0
- package/core/Auth.ts +63 -0
- package/core/Currency.ts +16 -0
- package/core/Env.ts +186 -0
- package/core/Fragment.ts +43 -0
- package/core/FragmentServingMode.ts +37 -0
- package/core/JsonSchemaValidator.ts +173 -0
- package/core/ProjectRoot.ts +76 -0
- package/core/Scratch.ts +44 -0
- package/core/URI.ts +203 -0
- package/core/Universe.ts +406 -0
- package/core/index.ts +27 -0
- package/gsuite/core/GSuite.ts +237 -0
- package/gsuite/core/GSuiteAdmin.ts +81 -0
- package/gsuite/core/GSuiteOrgConfig.ts +47 -0
- package/gsuite/core/GSuiteUser.ts +115 -0
- package/gsuite/core/index.ts +21 -0
- package/gsuite/documents/Document.ts +173 -0
- package/gsuite/documents/Documents.ts +52 -0
- package/gsuite/documents/index.ts +19 -0
- package/gsuite/drive/Drive.ts +118 -0
- package/gsuite/drive/DriveFile.ts +147 -0
- package/gsuite/drive/index.ts +19 -0
- package/gsuite/gmail/Gmail.ts +430 -0
- package/gsuite/gmail/GmailLabel.ts +55 -0
- package/gsuite/gmail/GmailMessage.ts +428 -0
- package/gsuite/gmail/GmailMessagePart.ts +298 -0
- package/gsuite/gmail/GmailThread.ts +97 -0
- package/gsuite/gmail/index.ts +5 -0
- package/gsuite/gmail/utils.ts +184 -0
- package/gsuite/index.ts +28 -0
- package/gsuite/spreadsheets/CellValue.ts +71 -0
- package/gsuite/spreadsheets/Sheet.ts +128 -0
- package/gsuite/spreadsheets/SheetValues.ts +12 -0
- package/gsuite/spreadsheets/Spreadsheet.ts +76 -0
- package/gsuite/spreadsheets/Spreadsheets.ts +52 -0
- package/gsuite/spreadsheets/index.ts +25 -0
- package/gsuite/spreadsheets/utils.ts +52 -0
- package/gsuite/utils.ts +104 -0
- package/http-server/HttpServer.ts +110 -0
- package/http-server/NativeHttpServer.ts +1084 -0
- package/http-server/index.ts +3 -0
- package/http-server/middlewares/01-cors.ts +33 -0
- package/http-server/middlewares/02-static.ts +67 -0
- package/http-server/middlewares/03-request-logger.ts +159 -0
- package/http-server/middlewares/04-body-parser.ts +54 -0
- package/http-server/middlewares/05-no-cache.ts +23 -0
- package/http-server/middlewares/06-response-handler.ts +39 -0
- package/http-server/middlewares/handler-wrapper.ts +250 -0
- package/http-server/middlewares/index.ts +37 -0
- package/http-server/middlewares/types.ts +27 -0
- package/index.ts +24 -0
- package/package.json +37 -0
- package/queue/EmailQueue.ts +228 -0
- package/queue/RateLimiter.ts +54 -0
- package/queue/index.ts +2 -0
- package/resend/Resend.ts +190 -0
- package/resend/index.ts +11 -0
- package/s2/S2.ts +335 -0
- package/s2/index.ts +11 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GmailMessagePart - Email Part and Attachment Management
|
|
3
|
+
*
|
|
4
|
+
* The GmailMessagePart class represents individual parts of an email message,
|
|
5
|
+
* including body text, HTML content, and file attachments. It implements the
|
|
6
|
+
* Fragment interface to provide consistent access to different content types.
|
|
7
|
+
*
|
|
8
|
+
* Key Features:
|
|
9
|
+
* - MIME part handling and content extraction
|
|
10
|
+
* - Attachment management and downloading
|
|
11
|
+
* - Content encoding and charset handling
|
|
12
|
+
* - Header parsing and metadata access
|
|
13
|
+
* - Multiple serving modes for different use cases
|
|
14
|
+
*
|
|
15
|
+
* This class handles the complex MIME structure of emails and provides
|
|
16
|
+
* a unified interface for accessing various content types within messages.
|
|
17
|
+
*
|
|
18
|
+
* @class GmailMessagePart
|
|
19
|
+
* @implements Fragment
|
|
20
|
+
* @version 1.0.0
|
|
21
|
+
* @author Divizend GmbH
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { gmail_v1 } from "googleapis";
|
|
25
|
+
import {
|
|
26
|
+
Gmail,
|
|
27
|
+
GmailMessage,
|
|
28
|
+
Fragment,
|
|
29
|
+
FragmentServingMode,
|
|
30
|
+
GmailMessagePartURI,
|
|
31
|
+
URI,
|
|
32
|
+
Universe,
|
|
33
|
+
} from "../..";
|
|
34
|
+
import { qpDecode } from "./utils";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Extended message part data with additional properties
|
|
38
|
+
*
|
|
39
|
+
* Extends the Gmail API message part with convenience properties
|
|
40
|
+
* for headers and body content management.
|
|
41
|
+
*/
|
|
42
|
+
export type GmailMessagePartData = gmail_v1.Schema$MessagePart & {
|
|
43
|
+
/** Map of lowercase header names to values for easy access */
|
|
44
|
+
headersMap?: { [key: string]: string };
|
|
45
|
+
/** Extended body data with optional buffer for content */
|
|
46
|
+
body?: gmail_v1.Schema$MessagePartBody & {
|
|
47
|
+
buffer?: Buffer;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export class GmailMessagePart implements Fragment {
|
|
52
|
+
/**
|
|
53
|
+
* Creates a new GmailMessagePart instance
|
|
54
|
+
*
|
|
55
|
+
* The constructor automatically processes headers into a map for
|
|
56
|
+
* efficient lookup and initializes the part with the provided data.
|
|
57
|
+
*
|
|
58
|
+
* @param gmail - Reference to the Gmail service instance
|
|
59
|
+
* @param part - Raw message part data from Gmail API
|
|
60
|
+
* @param messageId - ID of the parent message
|
|
61
|
+
* @param isFull - Whether the part contains full content
|
|
62
|
+
*/
|
|
63
|
+
constructor(
|
|
64
|
+
private readonly gmail: Gmail,
|
|
65
|
+
public readonly part: GmailMessagePartData,
|
|
66
|
+
public readonly messageId: string,
|
|
67
|
+
public readonly isFull: boolean
|
|
68
|
+
) {
|
|
69
|
+
// Create a map of lowercase header names to values for efficient lookup
|
|
70
|
+
const headersMap: { [key: string]: string } = {};
|
|
71
|
+
for (const header of this.part.headers || []) {
|
|
72
|
+
headersMap[header.name!.toLowerCase()] = header.value!;
|
|
73
|
+
}
|
|
74
|
+
this.part.headersMap = headersMap;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Creates a GmailMessagePart from a URI
|
|
79
|
+
*
|
|
80
|
+
* This factory method parses the URI to extract the email address,
|
|
81
|
+
* message ID, and part ID, then creates the appropriate instances
|
|
82
|
+
* to fetch the message part.
|
|
83
|
+
*
|
|
84
|
+
* @param universe - Reference to the central Universe instance
|
|
85
|
+
* @param uri - URI identifying the message part to retrieve
|
|
86
|
+
* @returns Promise<GmailMessagePart> - The requested message part
|
|
87
|
+
*/
|
|
88
|
+
static async fromURI(
|
|
89
|
+
universe: Universe,
|
|
90
|
+
uri: URI
|
|
91
|
+
): Promise<GmailMessagePart> {
|
|
92
|
+
const gmailMessagePartUri = GmailMessagePartURI.fromURI(uri);
|
|
93
|
+
const gmail = universe.gsuite.user(gmailMessagePartUri.email).gmail();
|
|
94
|
+
return GmailMessagePart.fromMessageIdAndPartId(
|
|
95
|
+
gmail,
|
|
96
|
+
gmailMessagePartUri.messageId,
|
|
97
|
+
gmailMessagePartUri.partId
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Creates a GmailMessagePart from message ID and part ID
|
|
103
|
+
*
|
|
104
|
+
* This factory method fetches the full message first, then
|
|
105
|
+
* locates the specific part within the message structure.
|
|
106
|
+
*
|
|
107
|
+
* @param gmail - Gmail service instance for the user
|
|
108
|
+
* @param messageId - Gmail message identifier
|
|
109
|
+
* @param partId - Gmail message part identifier
|
|
110
|
+
* @returns Promise<GmailMessagePart> - The requested message part
|
|
111
|
+
*/
|
|
112
|
+
static async fromMessageIdAndPartId(
|
|
113
|
+
gmail: Gmail,
|
|
114
|
+
messageId: string,
|
|
115
|
+
partId: string
|
|
116
|
+
): Promise<GmailMessagePart> {
|
|
117
|
+
const message = await GmailMessage.fromMessageId(gmail, messageId);
|
|
118
|
+
return GmailMessagePart.fromMessageAndPartId(gmail, message, partId);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Creates a GmailMessagePart from an existing message and part ID
|
|
123
|
+
*
|
|
124
|
+
* This factory method searches through the message's MIME structure
|
|
125
|
+
* to find the part with the specified ID, handling nested multipart
|
|
126
|
+
* messages recursively.
|
|
127
|
+
*
|
|
128
|
+
* @param gmail - Gmail service instance for the user
|
|
129
|
+
* @param message - Gmail message containing the part
|
|
130
|
+
* @param partId - Gmail message part identifier
|
|
131
|
+
* @returns Promise<GmailMessagePart> - The requested message part
|
|
132
|
+
* @throws Error if the part is not found in the message
|
|
133
|
+
*/
|
|
134
|
+
static async fromMessageAndPartId(
|
|
135
|
+
gmail: Gmail,
|
|
136
|
+
message: GmailMessage,
|
|
137
|
+
partId: string
|
|
138
|
+
): Promise<GmailMessagePart> {
|
|
139
|
+
// Recursive function to find a part by ID in the MIME structure
|
|
140
|
+
const findPart = (part: any) => {
|
|
141
|
+
if (part.partId === partId) {
|
|
142
|
+
return part;
|
|
143
|
+
}
|
|
144
|
+
if (Array.isArray(part.parts)) {
|
|
145
|
+
for (const subPart of part.parts) {
|
|
146
|
+
const body: any = findPart(subPart);
|
|
147
|
+
if (body) {
|
|
148
|
+
return body;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const part = findPart(message.data.payload);
|
|
156
|
+
if (!part) {
|
|
157
|
+
throw new Error(`Part not found: ${partId}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return GmailMessagePart.fromMessageAndPart(gmail, message, part);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Creates a GmailMessagePart from an existing message and part data
|
|
165
|
+
*
|
|
166
|
+
* This factory method creates a prototype part and then fetches
|
|
167
|
+
* the full content if needed.
|
|
168
|
+
*
|
|
169
|
+
* @param gmail - Gmail service instance for the user
|
|
170
|
+
* @param message - Gmail message containing the part
|
|
171
|
+
* @param part - Raw part data from the message
|
|
172
|
+
* @returns Promise<GmailMessagePart> - The full message part
|
|
173
|
+
*/
|
|
174
|
+
static async fromMessageAndPart(
|
|
175
|
+
gmail: Gmail,
|
|
176
|
+
message: GmailMessage,
|
|
177
|
+
part: GmailMessagePartData
|
|
178
|
+
): Promise<GmailMessagePart> {
|
|
179
|
+
const proto = new GmailMessagePart(gmail, part, message.message.id!, false);
|
|
180
|
+
return proto.fetch();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Fetches the full content of the message part
|
|
185
|
+
*
|
|
186
|
+
* This method retrieves the complete part content including
|
|
187
|
+
* body data and attachments. It handles different content
|
|
188
|
+
* encoding methods and attachment downloading.
|
|
189
|
+
*
|
|
190
|
+
* @returns Promise<GmailMessagePart> - Part with full content
|
|
191
|
+
*/
|
|
192
|
+
async fetch(): Promise<GmailMessagePart> {
|
|
193
|
+
if (this.isFull) {
|
|
194
|
+
return this;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let buffer: Buffer | undefined = undefined;
|
|
198
|
+
if (this.part.body?.data) {
|
|
199
|
+
// Decode base64-encoded content
|
|
200
|
+
buffer = Buffer.from(this.part.body.data, "base64");
|
|
201
|
+
} else if (this.part.body?.attachmentId) {
|
|
202
|
+
// Download attachment if it's an attachment
|
|
203
|
+
buffer = Buffer.from(
|
|
204
|
+
(
|
|
205
|
+
await this.gmail.gmail.users.messages.attachments.get({
|
|
206
|
+
userId: "me",
|
|
207
|
+
messageId: this.messageId,
|
|
208
|
+
id: this.part.body.attachmentId,
|
|
209
|
+
})
|
|
210
|
+
).data.data!,
|
|
211
|
+
"base64"
|
|
212
|
+
);
|
|
213
|
+
} else {
|
|
214
|
+
// If no data and no attachment, return an empty buffer
|
|
215
|
+
buffer = Buffer.alloc(0);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return new GmailMessagePart(
|
|
219
|
+
this.gmail,
|
|
220
|
+
{
|
|
221
|
+
...this.part,
|
|
222
|
+
body: {
|
|
223
|
+
...this.part.body,
|
|
224
|
+
buffer: buffer!,
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
this.messageId,
|
|
228
|
+
true
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
get uri() {
|
|
233
|
+
return `gmail://${this.gmail.email}/message/${this.messageId}/part/${this.part.partId}`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async serve(format: FragmentServingMode): Promise<{
|
|
237
|
+
headers: { name: string; value: string }[];
|
|
238
|
+
data: Buffer;
|
|
239
|
+
}> {
|
|
240
|
+
if (!this.isFull) {
|
|
241
|
+
throw new Error("Part is not full");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (format === FragmentServingMode.ORIGINAL) {
|
|
245
|
+
return {
|
|
246
|
+
headers: this.part.headers!.map((header) => ({
|
|
247
|
+
name: header.name!,
|
|
248
|
+
value: header.value!,
|
|
249
|
+
})),
|
|
250
|
+
data: this.part.body?.buffer || Buffer.alloc(0),
|
|
251
|
+
};
|
|
252
|
+
} else if (format === FragmentServingMode.JSON) {
|
|
253
|
+
return {
|
|
254
|
+
headers: [{ name: "Content-Type", value: "application/json" }],
|
|
255
|
+
data: Buffer.from(JSON.stringify(this.part)),
|
|
256
|
+
};
|
|
257
|
+
} else {
|
|
258
|
+
throw new Error(`Unknown serving mode: ${format}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
getHeader(name: string): string | undefined {
|
|
263
|
+
return this.part.headersMap?.[name.toLowerCase()];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
getCharset(): BufferEncoding {
|
|
267
|
+
const ct = this.getHeader("content-type") || "";
|
|
268
|
+
const m = /charset\s*=\s*"?([^";]+)"?/i.exec(ct);
|
|
269
|
+
const cs = (m?.[1] || "utf-8").toLowerCase();
|
|
270
|
+
if (/(utf-8|utf8)/i.test(cs)) return "utf8";
|
|
271
|
+
if (/(iso-8859-1|latin1)/i.test(cs)) return "latin1";
|
|
272
|
+
// default to utf-8
|
|
273
|
+
return "utf8";
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async asText(): Promise<string> {
|
|
277
|
+
if (!this.isFull) {
|
|
278
|
+
throw new Error("Part is not full");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// decode transfer-encoding if necessary (ignore base64, it's already decoded)
|
|
282
|
+
let raw = this.part.body?.buffer!;
|
|
283
|
+
const cte = (
|
|
284
|
+
this.getHeader("content-transfer-encoding") || ""
|
|
285
|
+
).toLowerCase();
|
|
286
|
+
if (cte === "quoted-printable") {
|
|
287
|
+
raw = qpDecode(raw);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// always decode with utf8 first (because some emails wrongly say that they are iso-8859-1, but they are actually utf8)
|
|
291
|
+
const enc = this.getCharset();
|
|
292
|
+
const ret = raw.toString("utf8");
|
|
293
|
+
if (Buffer.from(ret, "utf8").equals(raw)) {
|
|
294
|
+
return ret; // valid utf8
|
|
295
|
+
}
|
|
296
|
+
return raw.toString(enc);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GmailThread - Email Conversation Management
|
|
3
|
+
*
|
|
4
|
+
* The GmailThread class represents a conversation thread in Gmail, which
|
|
5
|
+
* groups related email messages together. Threads provide a way to view
|
|
6
|
+
* the complete conversation history in a single, organized view.
|
|
7
|
+
*
|
|
8
|
+
* Key Features:
|
|
9
|
+
* - Thread metadata and conversation overview
|
|
10
|
+
* - Access to all messages within the thread
|
|
11
|
+
* - Thread subject and snippet extraction
|
|
12
|
+
*
|
|
13
|
+
* This class simplifies working with email conversations by providing
|
|
14
|
+
* a high-level interface to thread properties and message collections.
|
|
15
|
+
*
|
|
16
|
+
* @class GmailThread
|
|
17
|
+
* @version 1.0.0
|
|
18
|
+
* @author Divizend GmbH
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { gmail_v1 } from "googleapis";
|
|
22
|
+
import { Gmail, GmailMessage } from "../..";
|
|
23
|
+
|
|
24
|
+
export class GmailThread {
|
|
25
|
+
/**
|
|
26
|
+
* Creates a new GmailThread instance
|
|
27
|
+
*
|
|
28
|
+
* @param gmail - Reference to the Gmail service instance
|
|
29
|
+
* @param thread - Raw Gmail API thread data
|
|
30
|
+
*/
|
|
31
|
+
constructor(
|
|
32
|
+
private readonly gmail: Gmail,
|
|
33
|
+
public readonly thread: gmail_v1.Schema$Thread,
|
|
34
|
+
public readonly messages: GmailMessage[]
|
|
35
|
+
) {}
|
|
36
|
+
|
|
37
|
+
async fetch(): Promise<GmailThread> {
|
|
38
|
+
if (this.messages.length > 0) {
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const threadFull = await this.gmail.gmail.users.threads.get({
|
|
43
|
+
userId: "me",
|
|
44
|
+
id: this.id!,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (!threadFull.data.messages) {
|
|
48
|
+
throw new Error("Thread has no messages: " + this.id);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const messages = threadFull.data.messages?.map((message) => {
|
|
52
|
+
return new GmailMessage(this.gmail, message, true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return new GmailThread(this.gmail, threadFull.data, messages);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get id() {
|
|
59
|
+
return this.thread.id!;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Gets the conversation snippet/preview
|
|
64
|
+
*
|
|
65
|
+
* The snippet provides a brief preview of the conversation,
|
|
66
|
+
* typically showing the most recent message content or subject.
|
|
67
|
+
*/
|
|
68
|
+
get snippet() {
|
|
69
|
+
return this.thread.snippet;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Gets the history ID for change tracking
|
|
74
|
+
*
|
|
75
|
+
* The history ID is used by Gmail to track changes to the thread
|
|
76
|
+
* and enable efficient synchronization of updates.
|
|
77
|
+
*/
|
|
78
|
+
get historyId() {
|
|
79
|
+
return this.thread.historyId;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Gets the subject from the first message in the thread
|
|
84
|
+
*
|
|
85
|
+
* This provides the original conversation subject, which typically
|
|
86
|
+
* remains consistent throughout the thread's lifecycle.
|
|
87
|
+
*
|
|
88
|
+
* @returns The thread subject, or undefined if no messages exist
|
|
89
|
+
*/
|
|
90
|
+
get subject(): string | undefined {
|
|
91
|
+
if (this.messages.length === 0) {
|
|
92
|
+
return "No messages";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return this.messages[0]!.subject;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gmail Utility Functions
|
|
3
|
+
*
|
|
4
|
+
* This module provides utility functions for Gmail operations including:
|
|
5
|
+
* - Content encoding and decoding (quoted-printable, base64)
|
|
6
|
+
* - HTML processing and sanitization
|
|
7
|
+
* - URL rewriting for embedded content
|
|
8
|
+
* - MIME message formatting and composition
|
|
9
|
+
*
|
|
10
|
+
* These utilities handle the low-level details of email processing,
|
|
11
|
+
* content transformation, and message composition that are common
|
|
12
|
+
* across different Gmail operations.
|
|
13
|
+
*
|
|
14
|
+
* @module GmailUtils
|
|
15
|
+
* @version 1.0.0
|
|
16
|
+
* @author Divizend GmbH
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Decodes quoted-printable encoded content
|
|
21
|
+
*
|
|
22
|
+
* Quoted-printable is a content transfer encoding used in email
|
|
23
|
+
* that represents 8-bit data using only 7-bit printable ASCII characters.
|
|
24
|
+
* This function handles both soft line breaks and hex-encoded bytes.
|
|
25
|
+
*
|
|
26
|
+
* @param buf - Buffer containing quoted-printable encoded data
|
|
27
|
+
* @returns Buffer with decoded content
|
|
28
|
+
*/
|
|
29
|
+
export function qpDecode(buf: Buffer): Buffer {
|
|
30
|
+
// Convert to string for ease; handle soft line breaks and =XX hex
|
|
31
|
+
let s = buf.toString("utf8");
|
|
32
|
+
// remove soft line breaks
|
|
33
|
+
s = s.replace(/=\r?\n/g, "");
|
|
34
|
+
// replace =HH hex bytes
|
|
35
|
+
s = s.replace(/=([A-Fa-f0-9]{2})/g, (_, hex) =>
|
|
36
|
+
String.fromCharCode(parseInt(hex, 16))
|
|
37
|
+
);
|
|
38
|
+
return Buffer.from(s, "utf8");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Escapes special regex characters in a string
|
|
43
|
+
*
|
|
44
|
+
* This function escapes characters that have special meaning in regular
|
|
45
|
+
* expressions, allowing the string to be used safely in regex patterns.
|
|
46
|
+
*
|
|
47
|
+
* @param s - String to escape
|
|
48
|
+
* @returns String with regex special characters escaped
|
|
49
|
+
*/
|
|
50
|
+
export function escapeRegExp(s: string) {
|
|
51
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Rewrites CID URLs in HTML/CSS to fragment URIs
|
|
56
|
+
*
|
|
57
|
+
* Content-ID (CID) URLs are used in multipart emails to reference
|
|
58
|
+
* embedded content like images. This function converts them to
|
|
59
|
+
* fragment URIs that can be served by the system.
|
|
60
|
+
*
|
|
61
|
+
* @param html - HTML content containing CID URLs
|
|
62
|
+
* @param uri - Base URI for the message
|
|
63
|
+
* @param cidToPart - Mapping of CID values to part IDs
|
|
64
|
+
* @returns HTML with CID URLs rewritten to fragment URIs
|
|
65
|
+
*/
|
|
66
|
+
export function rewriteCidUrls(
|
|
67
|
+
html: string,
|
|
68
|
+
uri: string,
|
|
69
|
+
cidToPart: { [key: string]: string }
|
|
70
|
+
): string {
|
|
71
|
+
// To avoid partial overlaps, replace longest CIDs first
|
|
72
|
+
const keys = Object.keys(cidToPart).sort((a, b) => b.length - a.length);
|
|
73
|
+
for (const cid of keys) {
|
|
74
|
+
const partId = cidToPart[cid];
|
|
75
|
+
const url = `/fragment?uri=${encodeURIComponent(uri + "/part/" + partId)}`;
|
|
76
|
+
const pat = new RegExp(
|
|
77
|
+
`cid:(?:%3C|<)?${escapeRegExp(cid)}(?:%3E|>)?`,
|
|
78
|
+
"gi"
|
|
79
|
+
);
|
|
80
|
+
html = html.replace(pat, url);
|
|
81
|
+
}
|
|
82
|
+
return html;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Converts a buffer or string to base64url encoding
|
|
87
|
+
*
|
|
88
|
+
* Base64url is a URL-safe variant of base64 encoding that replaces
|
|
89
|
+
* '+' with '-' and '/' with '_', and removes padding characters.
|
|
90
|
+
* This is commonly used in JWT tokens and other web-safe encodings.
|
|
91
|
+
*
|
|
92
|
+
* @param buf - Buffer or string to encode
|
|
93
|
+
* @returns Base64url encoded string
|
|
94
|
+
*/
|
|
95
|
+
export function base64Url(buf: Buffer | string) {
|
|
96
|
+
return (typeof buf === "string" ? Buffer.from(buf, "utf8") : buf)
|
|
97
|
+
.toString("base64")
|
|
98
|
+
.replace(/\+/g, "-")
|
|
99
|
+
.replace(/\//g, "_")
|
|
100
|
+
.replace(/=+$/g, "");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Chunks a string into 76-character lines
|
|
105
|
+
*
|
|
106
|
+
* This function breaks long strings into lines of 76 characters
|
|
107
|
+
* or less, which is the standard line length for MIME messages.
|
|
108
|
+
* Each line is terminated with CRLF (\r\n).
|
|
109
|
+
*
|
|
110
|
+
* @param s - String to chunk
|
|
111
|
+
* @returns String with line breaks every 76 characters
|
|
112
|
+
*/
|
|
113
|
+
export function chunk76(s: string) {
|
|
114
|
+
return s.replace(/.{1,76}/g, "$&\r\n");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Escapes HTML special characters
|
|
119
|
+
*
|
|
120
|
+
* Converts HTML special characters to their entity equivalents
|
|
121
|
+
* to prevent HTML injection and ensure safe content display.
|
|
122
|
+
*
|
|
123
|
+
* @param s - String containing HTML content
|
|
124
|
+
* @returns String with HTML special characters escaped
|
|
125
|
+
*/
|
|
126
|
+
export function escapeHtml(s: string) {
|
|
127
|
+
return s.replace(
|
|
128
|
+
/[&<>"']/g,
|
|
129
|
+
(c) =>
|
|
130
|
+
({
|
|
131
|
+
"&": "&",
|
|
132
|
+
"<": "<",
|
|
133
|
+
">": ">",
|
|
134
|
+
'"': """,
|
|
135
|
+
"'": "'",
|
|
136
|
+
}[c]!)
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Converts plain text to HTML with line breaks
|
|
142
|
+
*
|
|
143
|
+
* This function wraps plain text in HTML div tags and converts
|
|
144
|
+
* line breaks to HTML br tags for proper display in web browsers.
|
|
145
|
+
*
|
|
146
|
+
* @param s - Plain text string
|
|
147
|
+
* @returns HTML string with line breaks converted to br tags
|
|
148
|
+
*/
|
|
149
|
+
export function htmlFromText(s: string) {
|
|
150
|
+
return `<div>${escapeHtml(s).replace(/\r?\n/g, "<br>")}</div>`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Strips HTML tags to extract plain text
|
|
155
|
+
*
|
|
156
|
+
* Removes all HTML tags and converts common HTML elements like
|
|
157
|
+
* br and p tags to appropriate line breaks for plain text output.
|
|
158
|
+
*
|
|
159
|
+
* @param html - HTML string to convert
|
|
160
|
+
* @returns Plain text with HTML tags removed
|
|
161
|
+
*/
|
|
162
|
+
export function stripHtmlToText(html: string) {
|
|
163
|
+
return html
|
|
164
|
+
.replace(/<br\s*\/?>/gi, "\n")
|
|
165
|
+
.replace(/<\/p>/gi, "\n\n")
|
|
166
|
+
.replace(/<[^>]+>/g, "")
|
|
167
|
+
.trim();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Encodes display names for email headers
|
|
172
|
+
*
|
|
173
|
+
* This function handles display names in email headers, particularly
|
|
174
|
+
* for international characters. It uses base64 encoding for non-ASCII
|
|
175
|
+
* names and proper quoting for ASCII names.
|
|
176
|
+
*
|
|
177
|
+
* @param name - Display name to encode
|
|
178
|
+
* @returns Properly encoded display name for email headers
|
|
179
|
+
*/
|
|
180
|
+
export function encodeDisplayName(name: string) {
|
|
181
|
+
return /[^\x00-\x7F]/.test(name)
|
|
182
|
+
? `=?UTF-8?B?${Buffer.from(name, "utf8").toString("base64")}?=`
|
|
183
|
+
: `"${name.replace(/"/g, '\\"')}"`;
|
|
184
|
+
}
|
package/gsuite/index.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSuite Module - Google Workspace Integration
|
|
3
|
+
*
|
|
4
|
+
* This module provides comprehensive integration with Google Workspace services
|
|
5
|
+
* including Gmail, Google Drive, Google Sheets, Google Docs, and administrative
|
|
6
|
+
* functions.
|
|
7
|
+
*
|
|
8
|
+
* The module is organized into specialized submodules:
|
|
9
|
+
* - core: Organization management and user authentication
|
|
10
|
+
* - gmail: Email processing and management
|
|
11
|
+
* - drive: File storage and management
|
|
12
|
+
* - spreadsheets: Data analysis and spreadsheet operations
|
|
13
|
+
* - documents: Document creation and processing
|
|
14
|
+
*
|
|
15
|
+
* All services support multi-organization deployments with proper isolation
|
|
16
|
+
* and enterprise-grade security through service account authentication.
|
|
17
|
+
*
|
|
18
|
+
* @module GSuite
|
|
19
|
+
* @version 1.0.0
|
|
20
|
+
* @author Divizend GmbH
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export * from "./core";
|
|
24
|
+
export * from "./documents";
|
|
25
|
+
export * from "./drive";
|
|
26
|
+
export * from "./gmail";
|
|
27
|
+
export * from "./spreadsheets";
|
|
28
|
+
export * from "./utils";
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { sheets_v4 } from "googleapis";
|
|
2
|
+
import { jsDateToSheetsSerial } from "./utils";
|
|
3
|
+
|
|
4
|
+
export enum CellFormat {
|
|
5
|
+
Date_de = "Date_de",
|
|
6
|
+
Currency_EUR_de = "Currency_EUR_de",
|
|
7
|
+
RightAligned = "RightAligned",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const CellFormats: { [key in CellFormat]: sheets_v4.Schema$CellFormat } = {
|
|
11
|
+
[CellFormat.Date_de]: {
|
|
12
|
+
numberFormat: {
|
|
13
|
+
type: "DATE",
|
|
14
|
+
pattern: "dd.mm.yyyy",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
[CellFormat.Currency_EUR_de]: {
|
|
18
|
+
numberFormat: {
|
|
19
|
+
type: "CURRENCY",
|
|
20
|
+
pattern: "#,##0.00 €",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
[CellFormat.RightAligned]: {
|
|
24
|
+
horizontalAlignment: "RIGHT",
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function isDateCellFormat(
|
|
29
|
+
format: CellFormat | CellFormat[] | undefined
|
|
30
|
+
): boolean {
|
|
31
|
+
if (Array.isArray(format)) {
|
|
32
|
+
return format.some((f) => isDateCellFormat(f));
|
|
33
|
+
}
|
|
34
|
+
return format === CellFormat.Date_de;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type CellValue =
|
|
38
|
+
| string
|
|
39
|
+
| number
|
|
40
|
+
| { value: any; format?: CellFormat | CellFormat[] };
|
|
41
|
+
|
|
42
|
+
export function transformCellValueForSheets(
|
|
43
|
+
value: CellValue
|
|
44
|
+
): sheets_v4.Schema$CellData {
|
|
45
|
+
const newCell: sheets_v4.Schema$CellData = {};
|
|
46
|
+
if (typeof value === "string") {
|
|
47
|
+
newCell.userEnteredValue = { stringValue: value };
|
|
48
|
+
} else if (typeof value === "number") {
|
|
49
|
+
newCell.userEnteredValue = { numberValue: value };
|
|
50
|
+
} else if (typeof value === "object") {
|
|
51
|
+
if (isDateCellFormat(value.format)) {
|
|
52
|
+
newCell.userEnteredValue = {
|
|
53
|
+
numberValue: jsDateToSheetsSerial(value.value),
|
|
54
|
+
};
|
|
55
|
+
} else if (typeof value.value === "string") {
|
|
56
|
+
newCell.userEnteredValue = { stringValue: value.value };
|
|
57
|
+
} else if (typeof value.value === "number") {
|
|
58
|
+
newCell.userEnteredValue = { numberValue: value.value };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (Array.isArray(value.format)) {
|
|
62
|
+
newCell.userEnteredFormat = Object.assign(
|
|
63
|
+
{},
|
|
64
|
+
...value.format.map((f) => CellFormats[f])
|
|
65
|
+
);
|
|
66
|
+
} else if (value.format) {
|
|
67
|
+
newCell.userEnteredFormat = CellFormats[value.format];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return newCell;
|
|
71
|
+
}
|