@automate.ax/integration-contracts 0.55.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 (136) hide show
  1. package/dist/airtable/api.d.ts +19 -0
  2. package/dist/airtable/api.js +44 -0
  3. package/dist/airtable/index.d.ts +353 -0
  4. package/dist/airtable/index.js +32 -0
  5. package/dist/airtable/schemas.d.ts +716 -0
  6. package/dist/airtable/schemas.js +184 -0
  7. package/dist/asana/api.d.ts +70 -0
  8. package/dist/asana/api.js +119 -0
  9. package/dist/asana/index.d.ts +384 -0
  10. package/dist/asana/index.js +38 -0
  11. package/dist/asana/schemas.d.ts +1060 -0
  12. package/dist/asana/schemas.js +512 -0
  13. package/dist/brevo/api.d.ts +31 -0
  14. package/dist/brevo/api.js +64 -0
  15. package/dist/brevo/index.d.ts +822 -0
  16. package/dist/brevo/index.js +51 -0
  17. package/dist/brevo/schemas.d.ts +1217 -0
  18. package/dist/brevo/schemas.js +332 -0
  19. package/dist/github/index.d.ts +37 -0
  20. package/dist/github/index.js +106 -0
  21. package/dist/github/schemas.d.ts +8014 -0
  22. package/dist/github/schemas.js +113 -0
  23. package/dist/gmail/gmail.d.ts +172 -0
  24. package/dist/gmail/gmail.js +405 -0
  25. package/dist/gmail/index.d.ts +262 -0
  26. package/dist/gmail/index.js +23 -0
  27. package/dist/gmail/schemas.d.ts +444 -0
  28. package/dist/gmail/schemas.js +242 -0
  29. package/dist/google-calendar/event-schemas.d.ts +117 -0
  30. package/dist/google-calendar/event-schemas.js +13 -0
  31. package/dist/google-calendar/google-calendar.d.ts +147 -0
  32. package/dist/google-calendar/google-calendar.js +583 -0
  33. package/dist/google-calendar/index.d.ts +365 -0
  34. package/dist/google-calendar/index.js +24 -0
  35. package/dist/google-calendar/schemas.d.ts +592 -0
  36. package/dist/google-calendar/schemas.js +379 -0
  37. package/dist/google-forms/event-schemas.d.ts +105 -0
  38. package/dist/google-forms/event-schemas.js +20 -0
  39. package/dist/google-forms/google-forms.d.ts +243 -0
  40. package/dist/google-forms/google-forms.js +581 -0
  41. package/dist/google-forms/index.d.ts +141 -0
  42. package/dist/google-forms/index.js +20 -0
  43. package/dist/google-forms/schemas.d.ts +618 -0
  44. package/dist/google-forms/schemas.js +383 -0
  45. package/dist/linear/api.d.ts +63 -0
  46. package/dist/linear/api.js +88 -0
  47. package/dist/linear/index.d.ts +1338 -0
  48. package/dist/linear/index.js +55 -0
  49. package/dist/linear/schemas.d.ts +1795 -0
  50. package/dist/linear/schemas.js +760 -0
  51. package/dist/outlook/graph.d.ts +34 -0
  52. package/dist/outlook/graph.js +98 -0
  53. package/dist/outlook/index.d.ts +115 -0
  54. package/dist/outlook/index.js +11 -0
  55. package/dist/outlook/schemas.d.ts +529 -0
  56. package/dist/outlook/schemas.js +312 -0
  57. package/dist/resend/index.d.ts +707 -0
  58. package/dist/resend/index.js +54 -0
  59. package/dist/resend/schemas.d.ts +1033 -0
  60. package/dist/resend/schemas.js +377 -0
  61. package/dist/slack/index.d.ts +151 -0
  62. package/dist/slack/index.js +22 -0
  63. package/dist/slack/schemas.d.ts +641 -0
  64. package/dist/slack/schemas.js +581 -0
  65. package/dist/teams/graph.d.ts +33 -0
  66. package/dist/teams/graph.js +95 -0
  67. package/dist/teams/index.d.ts +164 -0
  68. package/dist/teams/index.js +21 -0
  69. package/dist/teams/schemas.d.ts +675 -0
  70. package/dist/teams/schemas.js +365 -0
  71. package/dist/trello/api.d.ts +69 -0
  72. package/dist/trello/api.js +140 -0
  73. package/dist/trello/event-schemas.d.ts +1101 -0
  74. package/dist/trello/event-schemas.js +233 -0
  75. package/dist/trello/index.d.ts +721 -0
  76. package/dist/trello/index.js +35 -0
  77. package/dist/trello/schemas.d.ts +283 -0
  78. package/dist/trello/schemas.js +401 -0
  79. package/dist/triggers.d.ts +26 -0
  80. package/dist/triggers.js +33 -0
  81. package/dist/vercel/index.d.ts +1065 -0
  82. package/dist/vercel/index.js +114 -0
  83. package/dist/vercel/schemas.d.ts +1518 -0
  84. package/dist/vercel/schemas.js +335 -0
  85. package/dist/whatsapp/api.d.ts +62 -0
  86. package/dist/whatsapp/api.js +121 -0
  87. package/dist/whatsapp/index.d.ts +614 -0
  88. package/dist/whatsapp/index.js +35 -0
  89. package/dist/whatsapp/schemas.d.ts +926 -0
  90. package/dist/whatsapp/schemas.js +326 -0
  91. package/package.json +169 -0
  92. package/src/airtable/api.ts +66 -0
  93. package/src/airtable/index.ts +41 -0
  94. package/src/airtable/schemas.ts +231 -0
  95. package/src/asana/api.ts +171 -0
  96. package/src/asana/index.ts +49 -0
  97. package/src/asana/schemas.ts +655 -0
  98. package/src/brevo/api.ts +89 -0
  99. package/src/brevo/index.ts +66 -0
  100. package/src/brevo/schemas.ts +449 -0
  101. package/src/github/index.ts +217 -0
  102. package/src/github/schemas.ts +435 -0
  103. package/src/gmail/gmail.ts +577 -0
  104. package/src/gmail/index.ts +26 -0
  105. package/src/gmail/schemas.ts +327 -0
  106. package/src/google-calendar/event-schemas.ts +16 -0
  107. package/src/google-calendar/google-calendar.ts +727 -0
  108. package/src/google-calendar/index.ts +27 -0
  109. package/src/google-calendar/schemas.ts +529 -0
  110. package/src/google-forms/event-schemas.ts +26 -0
  111. package/src/google-forms/google-forms.ts +683 -0
  112. package/src/google-forms/index.ts +23 -0
  113. package/src/google-forms/schemas.ts +499 -0
  114. package/src/linear/api.ts +141 -0
  115. package/src/linear/index.ts +71 -0
  116. package/src/linear/schemas.ts +1060 -0
  117. package/src/outlook/graph.ts +132 -0
  118. package/src/outlook/index.ts +14 -0
  119. package/src/outlook/schemas.ts +388 -0
  120. package/src/resend/index.ts +70 -0
  121. package/src/resend/schemas.ts +462 -0
  122. package/src/slack/index.ts +29 -0
  123. package/src/slack/schemas.ts +737 -0
  124. package/src/teams/graph.ts +130 -0
  125. package/src/teams/index.ts +27 -0
  126. package/src/teams/schemas.ts +463 -0
  127. package/src/trello/api.ts +190 -0
  128. package/src/trello/event-schemas.ts +290 -0
  129. package/src/trello/index.ts +45 -0
  130. package/src/trello/schemas.ts +507 -0
  131. package/src/triggers.ts +68 -0
  132. package/src/vercel/index.ts +147 -0
  133. package/src/vercel/schemas.ts +594 -0
  134. package/src/whatsapp/api.ts +171 -0
  135. package/src/whatsapp/index.ts +46 -0
  136. package/src/whatsapp/schemas.ts +341 -0
@@ -0,0 +1,577 @@
1
+ import { auth, gmail, type gmail_v1 } from "@googleapis/gmail"
2
+ import { compile } from "html-to-text"
3
+ import MailComposer from "nodemailer/lib/mail-composer"
4
+ import PostalMime, {
5
+ addressParser,
6
+ type Address,
7
+ type Attachment,
8
+ } from "postal-mime"
9
+ import { z } from "zod"
10
+ import {
11
+ ATTACHMENT_INPUT_SCHEMA,
12
+ DRAFT_IDENTIFIER_SCHEMA,
13
+ LABEL_SCHEMA,
14
+ MAILBOX_SCHEMA,
15
+ MESSAGE_METADATA_SCHEMA,
16
+ MESSAGE_SCHEMA,
17
+ RECIPIENT_SCHEMA,
18
+ REPLY_OPTIONS_SCHEMA,
19
+ } from "./schemas"
20
+
21
+ export const AUTOMATE_AX_EMAIL_HEADER = "X-Sent-From-Automate-Ax"
22
+ const AUTOMATE_EMAIL_HEADERS = {
23
+ "X-Mailer": "Automate.ax",
24
+ [AUTOMATE_AX_EMAIL_HEADER]: "true",
25
+ }
26
+ const HTML_TO_TEXT = compile({ wordwrap: false })
27
+ const GMAIL_SECRET_SCHEMA = z.object({
28
+ accessToken: z.string().min(1),
29
+ })
30
+ const LABEL_LIST_VISIBILITY_SCHEMA = z.enum([
31
+ "labelHide",
32
+ "labelShow",
33
+ "labelShowIfUnread",
34
+ ])
35
+ const MESSAGE_LIST_VISIBILITY_SCHEMA = z.enum(["hide", "show"])
36
+ const LABEL_TYPE_SCHEMA = z.enum(["system", "user"])
37
+
38
+ /** Internal options for constructing one RFC 5322 message. */
39
+ export interface BuildMessageOptions {
40
+ /** Files and inline MIME parts attached to the message. */
41
+ attachments?: z.input<typeof ATTACHMENT_INPUT_SCHEMA>[]
42
+
43
+ /** Blind-copy recipients. */
44
+ bcc?: z.input<typeof RECIPIENT_SCHEMA> | z.input<typeof RECIPIENT_SCHEMA>[]
45
+
46
+ /** Carbon-copy recipients. */
47
+ cc?: z.input<typeof RECIPIENT_SCHEMA> | z.input<typeof RECIPIENT_SCHEMA>[]
48
+
49
+ /** Sender address or configured Gmail send-as alias. */
50
+ from?: z.input<typeof RECIPIENT_SCHEMA>
51
+
52
+ /** Additional RFC 5322 headers. */
53
+ headers?: Record<string, string>
54
+
55
+ /** HTML message body. */
56
+ html?: string
57
+
58
+ /** RFC 5322 Message-ID this message replies to. */
59
+ inReplyTo?: string
60
+
61
+ /** Importance priority encoded into standard message headers. */
62
+ priority?: "high" | "normal" | "low"
63
+
64
+ /** RFC 5322 Message-IDs in the conversation ancestry. */
65
+ references?: string[]
66
+
67
+ /** Addresses that should receive replies. */
68
+ replyTo?:
69
+ | z.input<typeof RECIPIENT_SCHEMA>
70
+ | z.input<typeof RECIPIENT_SCHEMA>[]
71
+
72
+ /** Message subject line. */
73
+ subject?: string
74
+
75
+ /** Plain-text message body. */
76
+ text?: string
77
+
78
+ /** Primary recipients. */
79
+ to?: z.input<typeof RECIPIENT_SCHEMA> | z.input<typeof RECIPIENT_SCHEMA>[]
80
+ }
81
+
82
+ /**
83
+ * Creates the official Gmail client from a resolved Google integration secret.
84
+ *
85
+ * @param secret - Refreshed Google integration secret.
86
+ */
87
+ export function getGmailApi(secret: Record<string, unknown>) {
88
+ const { accessToken } = GMAIL_SECRET_SCHEMA.parse(secret)
89
+ return gmail({
90
+ auth: new auth.OAuth2({
91
+ credentials: { access_token: accessToken },
92
+ }),
93
+ version: "v1",
94
+ })
95
+ }
96
+
97
+ /**
98
+ * Builds the RFC 5322 message Gmail expects in its `raw` field.
99
+ *
100
+ * Missing plain text is derived from HTML.
101
+ *
102
+ * @param options - Message recipients, content, headers, and attachments.
103
+ */
104
+ export async function buildRawMessage(options: BuildMessageOptions) {
105
+ const message = new MailComposer({
106
+ attachments: await Promise.all(
107
+ (options.attachments ?? []).map(async (attachment) => {
108
+ const { contentId, disposition, file, filename } =
109
+ attachment instanceof File ? { file: attachment } : attachment
110
+ return {
111
+ cid: contentId,
112
+ content: Buffer.from(await file.arrayBuffer()),
113
+ contentDisposition: disposition,
114
+ contentType: file.type || undefined,
115
+ filename: filename ?? file.name,
116
+ }
117
+ }),
118
+ ),
119
+ bcc: options.bcc,
120
+ cc: options.cc,
121
+ from: options.from,
122
+ headers: {
123
+ ...options.headers,
124
+ ...AUTOMATE_EMAIL_HEADERS,
125
+ },
126
+ html: options.html,
127
+ inReplyTo: options.inReplyTo,
128
+ priority: options.priority,
129
+ references: options.references,
130
+ replyTo: options.replyTo,
131
+ subject: options.subject ?? "",
132
+ text:
133
+ options.text ??
134
+ (options.html === undefined ? undefined : HTML_TO_TEXT(options.html)),
135
+ to: options.to,
136
+ }).compile()
137
+
138
+ // Gmail derives Bcc delivery recipients from the raw message headers.
139
+ message.keepBcc = true
140
+ return (await message.build()).toString("base64url")
141
+ }
142
+
143
+ /**
144
+ * Resolves a Gmail label name or ID to its canonical ID.
145
+ *
146
+ * @param gmailApi - Authenticated Gmail client.
147
+ * @param labelSearch - Label display name or ID.
148
+ */
149
+ export async function findLabelId(
150
+ gmailApi: gmail_v1.Gmail,
151
+ labelSearch: string,
152
+ ) {
153
+ return (await findLabelIds(gmailApi, [labelSearch]))[0]!
154
+ }
155
+
156
+ /**
157
+ * Resolves Gmail label names or IDs with one label-list request.
158
+ *
159
+ * @param gmailApi - Authenticated Gmail client.
160
+ * @param labelSearches - Label display names or IDs.
161
+ */
162
+ export async function findLabelIds(
163
+ gmailApi: gmail_v1.Gmail,
164
+ labelSearches: string[],
165
+ ) {
166
+ const {
167
+ data: { labels = [] },
168
+ } = await gmailApi.users.labels.list({ userId: "me" })
169
+
170
+ return labelSearches.map((labelSearch) => {
171
+ const normalizedSearch = labelSearch.trim().toLowerCase()
172
+ const labelId =
173
+ labels.find((label) => label.id === labelSearch)?.id ??
174
+ labels.find(
175
+ (label) => label.name?.trim().toLowerCase() === normalizedSearch,
176
+ )?.id
177
+
178
+ if (!labelId) {
179
+ throw new Error(`Gmail label "${labelSearch}" was not found.`)
180
+ }
181
+ return labelId
182
+ })
183
+ }
184
+
185
+ /**
186
+ * Normalizes a raw Gmail API message into the public message representation.
187
+ *
188
+ * @param rawMessage - Gmail message fetched with `format: "raw"`.
189
+ */
190
+ export async function getGmailMessage(
191
+ rawMessage: gmail_v1.Schema$Message,
192
+ ): Promise<z.output<typeof MESSAGE_SCHEMA>> {
193
+ if (!rawMessage.id || !rawMessage.threadId || !rawMessage.raw) {
194
+ throw new Error("Gmail returned an incomplete message.")
195
+ }
196
+
197
+ const parsed = await parseRawMessage(rawMessage.raw)
198
+ const sentAt = parsed.date ? new Date(parsed.date) : undefined
199
+ return {
200
+ attachments: parsed.attachments.map(toAttachment),
201
+ bcc: flattenAddresses(parsed.bcc),
202
+ cc: flattenAddresses(parsed.cc),
203
+ deliveredTo: parsed.deliveredTo,
204
+ from: flattenAddresses(parsed.from).at(0),
205
+ headers: parsed.headers.map(({ originalKey, value }) => ({
206
+ name: originalKey,
207
+ value,
208
+ })),
209
+ historyId: rawMessage.historyId ?? undefined,
210
+ html: parsed.html,
211
+ inReplyTo: parsed.inReplyTo,
212
+ labelIds: rawMessage.labelIds ?? [],
213
+ messageId: rawMessage.id,
214
+ receivedAt: rawMessage.internalDate
215
+ ? new Date(Number(rawMessage.internalDate))
216
+ : null,
217
+ references: parsed.references?.trim().split(/\s+/) ?? [],
218
+ replyTo: flattenAddresses(parsed.replyTo),
219
+ returnPath: parsed.returnPath,
220
+ rfc822MessageId: parsed.messageId,
221
+ sender: flattenAddresses(parsed.sender).at(0),
222
+ ...(sentAt && !Number.isNaN(sentAt.getTime()) ? { sentAt } : {}),
223
+ sizeEstimate: rawMessage.sizeEstimate ?? undefined,
224
+ snippet: rawMessage.snippet ?? undefined,
225
+ subject: parsed.subject ?? "",
226
+ text:
227
+ parsed.text ??
228
+ (parsed.html === undefined ? undefined : HTML_TO_TEXT(parsed.html)),
229
+ threadId: rawMessage.threadId,
230
+ to: flattenAddresses(parsed.to),
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Resolves the headers and recipients needed to create a reply.
236
+ *
237
+ * @param gmailApi - Authenticated Gmail client.
238
+ * @param message - Gmail message receiving the reply.
239
+ * @param replyAll - Whether to include the original To and Cc recipients.
240
+ */
241
+ export async function getReplyOptions(
242
+ gmailApi: gmail_v1.Gmail,
243
+ message: gmail_v1.Schema$Message,
244
+ replyAll: boolean,
245
+ ) {
246
+ if (!message.threadId) throw new Error("Gmail message has no thread ID.")
247
+
248
+ const headers = getHeadersAsMap(message.payload?.headers)
249
+ const inReplyTo = headers.get("message-id")
250
+ const replyAddress = headers.get("reply-to") ?? headers.get("from")
251
+ const subject = headers.get("subject")
252
+ if (!inReplyTo || !replyAddress || !subject) {
253
+ throw new Error("Gmail message is missing required reply headers.")
254
+ }
255
+
256
+ return {
257
+ cc: replyAll
258
+ ? await getReplyAllCc(gmailApi, headers, replyAddress)
259
+ : undefined,
260
+ inReplyTo,
261
+ references: [
262
+ ...(headers.get("references")?.trim().split(/\s+/) ?? []),
263
+ inReplyTo,
264
+ ],
265
+ subject,
266
+ threadId: message.threadId,
267
+ to: replyAddress,
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Parses a raw Gmail message with MIME charset and attachment handling.
273
+ *
274
+ * @param raw - Base64url-encoded RFC 5322 message.
275
+ */
276
+ export async function parseRawMessage(raw: string) {
277
+ return await PostalMime.parse(Buffer.from(raw, "base64url"), {
278
+ attachmentEncoding: "arraybuffer",
279
+ rfc822Attachments: true,
280
+ })
281
+ }
282
+
283
+ /**
284
+ * Converts optional singular or plural recipients to an array.
285
+ *
286
+ * @param recipients - One recipient, many recipients, or no recipients.
287
+ */
288
+ export function normalizeRecipients(
289
+ recipients:
290
+ | z.input<typeof RECIPIENT_SCHEMA>
291
+ | z.input<typeof RECIPIENT_SCHEMA>[]
292
+ | undefined,
293
+ ) {
294
+ return recipients === undefined
295
+ ? []
296
+ : Array.isArray(recipients)
297
+ ? recipients
298
+ : [recipients]
299
+ }
300
+
301
+ /**
302
+ * Fetches the metadata needed for message predicates and replies.
303
+ *
304
+ * @param gmailApi - Authenticated Gmail client.
305
+ * @param messageId - Immutable Gmail message ID.
306
+ */
307
+ export async function getMessageMetadata(
308
+ gmailApi: gmail_v1.Gmail,
309
+ messageId: string,
310
+ ) {
311
+ return (
312
+ await gmailApi.users.messages.get({
313
+ format: "metadata",
314
+ id: messageId,
315
+ userId: "me",
316
+ })
317
+ ).data
318
+ }
319
+
320
+ /**
321
+ * Requires Gmail to identify a created or mutated message.
322
+ *
323
+ * @param message - Gmail message response.
324
+ * @param operation - Operation named in failure output.
325
+ * @throws When Gmail omits the message or thread ID.
326
+ */
327
+ export function toMessageMetadata(
328
+ message: gmail_v1.Schema$Message,
329
+ operation: string,
330
+ ): z.output<typeof MESSAGE_METADATA_SCHEMA> {
331
+ if (!message.id || !message.threadId) {
332
+ throw new Error(`Gmail did not identify the ${operation}.`)
333
+ }
334
+ return {
335
+ historyId: message.historyId ?? undefined,
336
+ labelIds: message.labelIds ?? [],
337
+ messageId: message.id,
338
+ threadId: message.threadId,
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Builds and sends a reply for one resolved Gmail message.
344
+ *
345
+ * @param gmailApi - Authenticated Gmail client.
346
+ * @param original - Message receiving the reply.
347
+ * @param input - Reply body, recipients, headers, and attachments.
348
+ */
349
+ export async function sendReply(
350
+ gmailApi: gmail_v1.Gmail,
351
+ original: gmail_v1.Schema$Message,
352
+ input: z.input<typeof REPLY_OPTIONS_SCHEMA>,
353
+ ) {
354
+ const replyOptions = await getReplyOptions(
355
+ gmailApi,
356
+ original,
357
+ input.replyAll ?? false,
358
+ )
359
+ return toMessageMetadata(
360
+ (
361
+ await gmailApi.users.messages.send({
362
+ requestBody: {
363
+ raw: await buildRawMessage({
364
+ ...getReplyMessageOptions(replyOptions, input),
365
+ bcc: input.bcc,
366
+ from: input.from,
367
+ headers: input.headers,
368
+ html: input.html,
369
+ priority: input.priority,
370
+ replyTo: input.replyTo,
371
+ text: input.text,
372
+ }),
373
+ threadId: replyOptions.threadId,
374
+ },
375
+ userId: "me",
376
+ })
377
+ ).data,
378
+ "reply",
379
+ )
380
+ }
381
+
382
+ /**
383
+ * Combines derived reply headers with caller-supplied recipients.
384
+ *
385
+ * @param reply - Reply fields derived from the original message.
386
+ * @param input - Optional reply overrides and additional recipients.
387
+ */
388
+ export function getReplyMessageOptions(
389
+ reply: Awaited<ReturnType<typeof getReplyOptions>>,
390
+ input: z.input<typeof REPLY_OPTIONS_SCHEMA>,
391
+ ) {
392
+ return {
393
+ attachments: input.attachments,
394
+ cc: [...(reply.cc ?? []), ...normalizeRecipients(input.cc)],
395
+ inReplyTo: reply.inReplyTo,
396
+ references: reply.references,
397
+ subject: reply.subject,
398
+ to: [reply.to, ...normalizeRecipients(input.to)],
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Requires Gmail to identify a created or updated draft and its message.
404
+ *
405
+ * @param draft - Gmail draft response.
406
+ * @param operation - Operation named in failure output.
407
+ * @throws When Gmail omits the draft, message, or thread identity.
408
+ */
409
+ export function toDraftIdentifier(
410
+ draft: gmail_v1.Schema$Draft,
411
+ operation: string,
412
+ ): z.output<typeof DRAFT_IDENTIFIER_SCHEMA> {
413
+ if (!draft.id || !draft.message) {
414
+ throw new Error(`Gmail did not identify the ${operation}.`)
415
+ }
416
+ return {
417
+ draftId: draft.id,
418
+ ...toMessageMetadata(draft.message, `${operation} message`),
419
+ }
420
+ }
421
+
422
+ /**
423
+ * Converts a Gmail label resource to its documented public representation.
424
+ *
425
+ * @param label - Gmail label resource.
426
+ * @throws When Gmail omits required label fields or returns invalid metadata.
427
+ */
428
+ export function toGmailLabel(
429
+ label: gmail_v1.Schema$Label,
430
+ ): z.output<typeof LABEL_SCHEMA> {
431
+ if (!label.id || !label.name) {
432
+ throw new Error("Gmail returned an incomplete label.")
433
+ }
434
+ return {
435
+ ...(label.color?.backgroundColor &&
436
+ label.color.textColor && {
437
+ color: {
438
+ backgroundColor: label.color.backgroundColor,
439
+ textColor: label.color.textColor,
440
+ },
441
+ }),
442
+ labelId: label.id,
443
+ labelListVisibility: label.labelListVisibility
444
+ ? LABEL_LIST_VISIBILITY_SCHEMA.parse(label.labelListVisibility)
445
+ : undefined,
446
+ messageListVisibility:
447
+ label.messageListVisibility === null ||
448
+ label.messageListVisibility === undefined
449
+ ? undefined
450
+ : MESSAGE_LIST_VISIBILITY_SCHEMA.parse(label.messageListVisibility),
451
+ messagesTotal: label.messagesTotal ?? undefined,
452
+ messagesUnread: label.messagesUnread ?? undefined,
453
+ name: label.name,
454
+ threadsTotal: label.threadsTotal ?? undefined,
455
+ threadsUnread: label.threadsUnread ?? undefined,
456
+ type: label.type ? LABEL_TYPE_SCHEMA.parse(label.type) : undefined,
457
+ }
458
+ }
459
+
460
+ /**
461
+ * Converts one parsed PostalMime attachment into a File-backed attachment.
462
+ *
463
+ * @param attachment - Parsed MIME attachment.
464
+ */
465
+ function toAttachment(attachment: Attachment) {
466
+ const mimeType = attachment.mimeType || "application/octet-stream"
467
+ const filename = attachment.filename || "attachment"
468
+ return {
469
+ contentId: attachment.contentId,
470
+ description: attachment.description,
471
+ disposition: attachment.disposition ?? undefined,
472
+ file: new File(
473
+ [
474
+ attachment.content instanceof ArrayBuffer
475
+ ? new Uint8Array(attachment.content)
476
+ : typeof attachment.content === "string"
477
+ ? attachment.content
478
+ : Uint8Array.from(attachment.content),
479
+ ],
480
+ filename,
481
+ { type: mimeType },
482
+ ),
483
+ filename,
484
+ mimeType,
485
+ related: attachment.related ?? false,
486
+ }
487
+ }
488
+
489
+ /**
490
+ * Flattens parsed mailbox groups into code-friendly mailbox values.
491
+ *
492
+ * @param addresses - One parsed address or an address list.
493
+ */
494
+ function flattenAddresses(
495
+ addresses: Address | Address[] | undefined,
496
+ ): z.output<typeof MAILBOX_SCHEMA>[] {
497
+ return (Array.isArray(addresses) ? addresses : addresses ? [addresses] : [])
498
+ .flatMap((address) => address.group ?? [address])
499
+ .filter((address): address is Address & { address: string } =>
500
+ Boolean(address.address),
501
+ )
502
+ .map(({ address, name }) => ({
503
+ address,
504
+ ...(name && { name }),
505
+ }))
506
+ }
507
+
508
+ /**
509
+ * Formats one PostalMime address or group.
510
+ *
511
+ * @param address - Parsed address.
512
+ */
513
+ export function formatAddress(address: Address): string {
514
+ if (address.group) {
515
+ return `${address.name}: ${address.group.map(formatAddress).join(", ")}`
516
+ }
517
+ if (!address.address) return address.name
518
+ return address.name ? `${address.name} <${address.address}>` : address.address
519
+ }
520
+
521
+ /**
522
+ * Converts MIME headers into a lowercase-keyed map.
523
+ *
524
+ * @param headers - Gmail MIME headers.
525
+ */
526
+ function getHeadersAsMap(
527
+ headers: gmail_v1.Schema$MessagePartHeader[] | undefined,
528
+ ) {
529
+ return new Map(
530
+ headers
531
+ ?.filter(
532
+ (
533
+ header,
534
+ ): header is {
535
+ name: string
536
+ value: string
537
+ } => Boolean(header.name && header.value),
538
+ )
539
+ .map((header) => [header.name.toLowerCase(), header.value] as const) ??
540
+ [],
541
+ )
542
+ }
543
+
544
+ /**
545
+ * Builds a deduplicated reply-all Cc list.
546
+ *
547
+ * @param gmailApi - Authenticated Gmail client.
548
+ * @param headers - Original message headers.
549
+ * @param replyAddress - Primary reply recipient.
550
+ */
551
+ async function getReplyAllCc(
552
+ gmailApi: gmail_v1.Gmail,
553
+ headers: Map<string, string>,
554
+ replyAddress: string,
555
+ ) {
556
+ const {
557
+ data: { emailAddress },
558
+ } = await gmailApi.users.getProfile({ userId: "me" })
559
+ const excluded = new Set([
560
+ ...(emailAddress ? [emailAddress] : []),
561
+ ...addressParser(replyAddress, { flatten: true }).flatMap((address) =>
562
+ address.address ? [address.address] : [],
563
+ ),
564
+ ])
565
+ return [
566
+ ...new Map(
567
+ [headers.get("to"), headers.get("cc")]
568
+ .flatMap((header) =>
569
+ header ? addressParser(header, { flatten: true }) : [],
570
+ )
571
+ .filter((address): address is Address & { address: string } =>
572
+ Boolean(address.address && !excluded.has(address.address)),
573
+ )
574
+ .map((address) => [address.address, formatAddress(address)]),
575
+ ).values(),
576
+ ]
577
+ }
@@ -0,0 +1,26 @@
1
+ import { z } from "zod"
2
+ import { LABEL_CHANGE_SCHEMA, MESSAGE_SCHEMA } from "./schemas"
3
+
4
+ export * from "./gmail"
5
+ export * from "./schemas"
6
+
7
+ export const GMAIL_TRIGGER_CONFIG_SCHEMA = z.object({})
8
+
9
+ export const gmailTriggerContracts = {
10
+ "gmail.label.added": {
11
+ configSchema: GMAIL_TRIGGER_CONFIG_SCHEMA,
12
+ eventSchema: LABEL_CHANGE_SCHEMA,
13
+ },
14
+ "gmail.label.removed": {
15
+ configSchema: GMAIL_TRIGGER_CONFIG_SCHEMA,
16
+ eventSchema: LABEL_CHANGE_SCHEMA,
17
+ },
18
+ "gmail.message.received": {
19
+ configSchema: GMAIL_TRIGGER_CONFIG_SCHEMA,
20
+ eventSchema: MESSAGE_SCHEMA,
21
+ },
22
+ "gmail.message.sent": {
23
+ configSchema: GMAIL_TRIGGER_CONFIG_SCHEMA,
24
+ eventSchema: MESSAGE_SCHEMA,
25
+ },
26
+ } as const