@guayaba/workflow-piece-gmail 0.12.3
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/.babelrc +3 -0
- package/.eslintrc.json +37 -0
- package/README.md +5 -0
- package/assets/logo.png +0 -0
- package/package.json +31 -0
- package/src/i18n/ca.json +63 -0
- package/src/i18n/de.json +129 -0
- package/src/i18n/es.json +129 -0
- package/src/i18n/fr.json +129 -0
- package/src/i18n/hi.json +63 -0
- package/src/i18n/id.json +63 -0
- package/src/i18n/ja.json +129 -0
- package/src/i18n/nl.json +129 -0
- package/src/i18n/pt.json +129 -0
- package/src/i18n/ru.json +63 -0
- package/src/i18n/translation.json +129 -0
- package/src/i18n/vi.json +63 -0
- package/src/i18n/zh.json +129 -0
- package/src/index.ts +69 -0
- package/src/lib/actions/create-draft-reply-action.ts +306 -0
- package/src/lib/actions/get-mail-action.ts +44 -0
- package/src/lib/actions/get-thread-action.ts +39 -0
- package/src/lib/actions/reply-to-email-action.ts +220 -0
- package/src/lib/actions/request-approval-in-email.ts +194 -0
- package/src/lib/actions/search-email-action.ts +211 -0
- package/src/lib/actions/send-email-action.ts +205 -0
- package/src/lib/auth.ts +116 -0
- package/src/lib/common/data.ts +268 -0
- package/src/lib/common/models.ts +91 -0
- package/src/lib/common/props.ts +256 -0
- package/src/lib/triggers/new-attachment.ts +198 -0
- package/src/lib/triggers/new-conversation.ts +413 -0
- package/src/lib/triggers/new-email.ts +167 -0
- package/src/lib/triggers/new-label.ts +77 -0
- package/src/lib/triggers/new-labeled-email.ts +192 -0
- package/tsconfig.json +16 -0
- package/tsconfig.lib.json +15 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createTrigger,
|
|
3
|
+
TriggerStrategy,
|
|
4
|
+
FilesService,
|
|
5
|
+
Property,
|
|
6
|
+
} from '@guayaba/workflows-framework';
|
|
7
|
+
import { GmailProps } from '../common/props';
|
|
8
|
+
import { gmailAuth, createGoogleClient } from '../auth';
|
|
9
|
+
import { google } from 'googleapis';
|
|
10
|
+
import { parseStream, convertAttachment } from '../common/data';
|
|
11
|
+
|
|
12
|
+
async function enrichNewConversation({
|
|
13
|
+
gmail,
|
|
14
|
+
threadId,
|
|
15
|
+
files,
|
|
16
|
+
conversationInfo,
|
|
17
|
+
}: {
|
|
18
|
+
gmail: any;
|
|
19
|
+
threadId: string;
|
|
20
|
+
files: FilesService;
|
|
21
|
+
conversationInfo: {
|
|
22
|
+
createdAt: number;
|
|
23
|
+
historyId: string;
|
|
24
|
+
};
|
|
25
|
+
}) {
|
|
26
|
+
const threadResponse = await gmail.users.threads.get({
|
|
27
|
+
userId: 'me',
|
|
28
|
+
id: threadId,
|
|
29
|
+
format: 'full',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const thread = threadResponse.data;
|
|
33
|
+
const messages = thread.messages || [];
|
|
34
|
+
|
|
35
|
+
const firstMessage = messages[0];
|
|
36
|
+
if (!firstMessage?.id) {
|
|
37
|
+
throw new Error('No messages found in thread');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const rawMessageResponse = await gmail.users.messages.get({
|
|
41
|
+
userId: 'me',
|
|
42
|
+
id: firstMessage.id,
|
|
43
|
+
format: 'raw',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const parsedFirstMessage = await parseStream(
|
|
47
|
+
Buffer.from(rawMessageResponse.data.raw as string, 'base64').toString(
|
|
48
|
+
'utf-8'
|
|
49
|
+
)
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const headers = firstMessage.payload?.headers || [];
|
|
53
|
+
const headerMap = headers.reduce(
|
|
54
|
+
(acc: { [key: string]: string }, header: any) => {
|
|
55
|
+
if (header.name && header.value) {
|
|
56
|
+
acc[header.name.toLowerCase()] = header.value;
|
|
57
|
+
}
|
|
58
|
+
return acc;
|
|
59
|
+
},
|
|
60
|
+
{}
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
conversation: {
|
|
65
|
+
threadId: threadId,
|
|
66
|
+
messageCount: messages.length,
|
|
67
|
+
snippet: thread.snippet || '',
|
|
68
|
+
historyId: thread.historyId,
|
|
69
|
+
participants: extractParticipants(messages),
|
|
70
|
+
subject: headerMap['subject'] || '',
|
|
71
|
+
starter: {
|
|
72
|
+
from: headerMap['from'] || '',
|
|
73
|
+
to: headerMap['to'] || '',
|
|
74
|
+
cc: headerMap['cc'] || '',
|
|
75
|
+
bcc: headerMap['bcc'] || '',
|
|
76
|
+
date: headerMap['date'] || '',
|
|
77
|
+
messageId: firstMessage.id,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
firstMessage: {
|
|
81
|
+
...parsedFirstMessage,
|
|
82
|
+
messageId: firstMessage.id,
|
|
83
|
+
attachments: await convertAttachment(
|
|
84
|
+
parsedFirstMessage.attachments,
|
|
85
|
+
files
|
|
86
|
+
),
|
|
87
|
+
},
|
|
88
|
+
conversationInfo: {
|
|
89
|
+
...conversationInfo,
|
|
90
|
+
triggeredAt: Date.now(),
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function extractParticipants(messages: any[]): {
|
|
96
|
+
from: Set<string>;
|
|
97
|
+
to: Set<string>;
|
|
98
|
+
cc: Set<string>;
|
|
99
|
+
} {
|
|
100
|
+
const participants = {
|
|
101
|
+
from: new Set<string>(),
|
|
102
|
+
to: new Set<string>(),
|
|
103
|
+
cc: new Set<string>(),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
messages.forEach((message) => {
|
|
107
|
+
const headers = message.payload?.headers || [];
|
|
108
|
+
headers.forEach((header: any) => {
|
|
109
|
+
if (header.name && header.value) {
|
|
110
|
+
const name = header.name.toLowerCase();
|
|
111
|
+
const value = header.value;
|
|
112
|
+
|
|
113
|
+
if (name === 'from') {
|
|
114
|
+
participants.from.add(value);
|
|
115
|
+
} else if (name === 'to') {
|
|
116
|
+
value
|
|
117
|
+
.split(',')
|
|
118
|
+
.forEach((email: string) => participants.to.add(email.trim()));
|
|
119
|
+
} else if (name === 'cc') {
|
|
120
|
+
value
|
|
121
|
+
.split(',')
|
|
122
|
+
.forEach((email: string) => participants.cc.add(email.trim()));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
from: participants.from,
|
|
130
|
+
to: participants.to,
|
|
131
|
+
cc: participants.cc,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const gmailNewConversationTrigger = createTrigger({
|
|
136
|
+
auth: gmailAuth,
|
|
137
|
+
name: 'new_conversation',
|
|
138
|
+
displayName: 'New Conversation',
|
|
139
|
+
description: 'Triggers when a new email conversation (thread) begins',
|
|
140
|
+
props: {
|
|
141
|
+
from: {
|
|
142
|
+
...GmailProps.from,
|
|
143
|
+
description: 'Filter by sender email (optional)',
|
|
144
|
+
displayName: 'From',
|
|
145
|
+
required: false,
|
|
146
|
+
},
|
|
147
|
+
subject: Property.ShortText({
|
|
148
|
+
displayName: 'Subject Contains',
|
|
149
|
+
description:
|
|
150
|
+
'Only trigger for conversations containing this text in the subject (optional)',
|
|
151
|
+
required: false,
|
|
152
|
+
}),
|
|
153
|
+
maxAgeHours: Property.Number({
|
|
154
|
+
displayName: 'Maximum Age (Hours)',
|
|
155
|
+
description:
|
|
156
|
+
'Only trigger for conversations started within this many hours',
|
|
157
|
+
required: false,
|
|
158
|
+
defaultValue: 24,
|
|
159
|
+
}),
|
|
160
|
+
},
|
|
161
|
+
sampleData: {},
|
|
162
|
+
type: TriggerStrategy.POLLING,
|
|
163
|
+
onEnable: async (context) => {
|
|
164
|
+
const authClient = await createGoogleClient(context.auth);
|
|
165
|
+
const gmail = google.gmail({ version: 'v1', auth: authClient });
|
|
166
|
+
|
|
167
|
+
const profile = await gmail.users.getProfile({ userId: 'me' });
|
|
168
|
+
await context.store.put('lastHistoryId', profile.data.historyId);
|
|
169
|
+
await context.store.put('processedThreads', []);
|
|
170
|
+
},
|
|
171
|
+
onDisable: async (context) => {
|
|
172
|
+
await context.store.delete('lastHistoryId');
|
|
173
|
+
await context.store.delete('processedThreads');
|
|
174
|
+
},
|
|
175
|
+
run: async (context) => {
|
|
176
|
+
const authClient = await createGoogleClient(context.auth);
|
|
177
|
+
const gmail = google.gmail({ version: 'v1', auth: authClient });
|
|
178
|
+
|
|
179
|
+
const lastHistoryId = await context.store.get('lastHistoryId');
|
|
180
|
+
const processedThreads =
|
|
181
|
+
(await context.store.get<string[]>('processedThreads')) || [];
|
|
182
|
+
const maxAge = (context.propsValue.maxAgeHours || 24) * 60 * 60 * 1000;
|
|
183
|
+
const cutoffTime = Date.now() - maxAge;
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const historyResponse = await gmail.users.history.list({
|
|
187
|
+
userId: 'me',
|
|
188
|
+
startHistoryId: lastHistoryId as string,
|
|
189
|
+
historyTypes: ['messageAdded'],
|
|
190
|
+
maxResults: 100,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const newConversations: string[] = [];
|
|
194
|
+
|
|
195
|
+
if (historyResponse.data.history) {
|
|
196
|
+
for (const history of historyResponse.data.history) {
|
|
197
|
+
if (history.messagesAdded) {
|
|
198
|
+
for (const added of history.messagesAdded) {
|
|
199
|
+
const threadId = added.message?.threadId;
|
|
200
|
+
if (threadId && !processedThreads.includes(threadId)) {
|
|
201
|
+
const threadResponse = await gmail.users.threads.get({
|
|
202
|
+
userId: 'me',
|
|
203
|
+
id: threadId,
|
|
204
|
+
format: 'minimal',
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
if (threadResponse.data.messages?.length === 1) {
|
|
208
|
+
newConversations.push(threadId);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const results: any[] = [];
|
|
217
|
+
for (const threadId of newConversations) {
|
|
218
|
+
const threadResponse = await gmail.users.threads.get({
|
|
219
|
+
userId: 'me',
|
|
220
|
+
id: threadId,
|
|
221
|
+
format: 'full',
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const thread = threadResponse.data;
|
|
225
|
+
const firstMessage = thread.messages?.[0];
|
|
226
|
+
if (!firstMessage) continue;
|
|
227
|
+
|
|
228
|
+
const messageDate = parseInt(firstMessage.internalDate || '0');
|
|
229
|
+
if (messageDate < cutoffTime) continue;
|
|
230
|
+
|
|
231
|
+
const headers = firstMessage.payload?.headers || [];
|
|
232
|
+
const headerMap: { [key: string]: string } = {};
|
|
233
|
+
headers.forEach((h: any) => {
|
|
234
|
+
if (h.name && h.value) {
|
|
235
|
+
headerMap[h.name.toLowerCase()] = h.value;
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
if (context.propsValue.from) {
|
|
240
|
+
const from = headerMap['from'] || '';
|
|
241
|
+
if (
|
|
242
|
+
!from.toLowerCase().includes(context.propsValue.from.toLowerCase())
|
|
243
|
+
) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (context.propsValue.subject) {
|
|
249
|
+
const subject = headerMap['subject'] || '';
|
|
250
|
+
if (
|
|
251
|
+
!subject
|
|
252
|
+
.toLowerCase()
|
|
253
|
+
.includes(context.propsValue.subject.toLowerCase())
|
|
254
|
+
) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const rawResponse = await gmail.users.messages.get({
|
|
260
|
+
userId: 'me',
|
|
261
|
+
id: firstMessage.id!,
|
|
262
|
+
format: 'raw',
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const parsedMessage = await parseStream(
|
|
266
|
+
Buffer.from(rawResponse.data.raw as string, 'base64').toString(
|
|
267
|
+
'utf-8'
|
|
268
|
+
)
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
results.push({
|
|
272
|
+
id: `conversation_${threadId}`,
|
|
273
|
+
data: {
|
|
274
|
+
thread: {
|
|
275
|
+
id: threadId,
|
|
276
|
+
snippet: thread.snippet,
|
|
277
|
+
messageCount: 1,
|
|
278
|
+
},
|
|
279
|
+
message: {
|
|
280
|
+
...parsedMessage,
|
|
281
|
+
id: firstMessage.id,
|
|
282
|
+
threadId: threadId,
|
|
283
|
+
date: new Date(messageDate).toISOString(),
|
|
284
|
+
attachments: await convertAttachment(
|
|
285
|
+
parsedMessage.attachments,
|
|
286
|
+
context.files
|
|
287
|
+
),
|
|
288
|
+
},
|
|
289
|
+
conversation: {
|
|
290
|
+
starter: {
|
|
291
|
+
from: headerMap['from'],
|
|
292
|
+
to: headerMap['to'],
|
|
293
|
+
subject: headerMap['subject'],
|
|
294
|
+
date: headerMap['date'],
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
processedThreads.push(threadId);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (historyResponse.data.historyId) {
|
|
304
|
+
await context.store.put(
|
|
305
|
+
'lastHistoryId',
|
|
306
|
+
historyResponse.data.historyId
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const recentThreads = processedThreads.slice(-1000);
|
|
311
|
+
await context.store.put('processedThreads', recentThreads);
|
|
312
|
+
|
|
313
|
+
return results;
|
|
314
|
+
} catch (error: any) {
|
|
315
|
+
if (error.code === 404) {
|
|
316
|
+
const profile = await gmail.users.getProfile({ userId: 'me' });
|
|
317
|
+
await context.store.put('lastHistoryId', profile.data.historyId);
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
test: async (context) => {
|
|
324
|
+
const authClient = await createGoogleClient(context.auth);
|
|
325
|
+
const gmail = google.gmail({ version: 'v1', auth: authClient });
|
|
326
|
+
|
|
327
|
+
const maxAge = (context.propsValue.maxAgeHours || 24) * 60 * 60 * 1000;
|
|
328
|
+
const cutoffSeconds = Math.floor((Date.now() - maxAge) / 1000);
|
|
329
|
+
|
|
330
|
+
let query = `after:${cutoffSeconds}`;
|
|
331
|
+
if (context.propsValue.from) {
|
|
332
|
+
query += ` from:(${context.propsValue.from})`;
|
|
333
|
+
}
|
|
334
|
+
if (context.propsValue.subject) {
|
|
335
|
+
query += ` subject:(${context.propsValue.subject})`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const threadsResponse = await gmail.users.threads.list({
|
|
339
|
+
userId: 'me',
|
|
340
|
+
q: query,
|
|
341
|
+
maxResults: 5,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const results: any[] = [];
|
|
345
|
+
if (threadsResponse.data.threads) {
|
|
346
|
+
for (const thread of threadsResponse.data.threads) {
|
|
347
|
+
const threadId = thread.id!;
|
|
348
|
+
|
|
349
|
+
const fullThread = await gmail.users.threads.get({
|
|
350
|
+
userId: 'me',
|
|
351
|
+
id: threadId,
|
|
352
|
+
format: 'full',
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
if (fullThread.data.messages?.length === 1) {
|
|
356
|
+
const firstMessage = fullThread.data.messages[0];
|
|
357
|
+
const headers = firstMessage.payload?.headers || [];
|
|
358
|
+
const headerMap: { [key: string]: string } = {};
|
|
359
|
+
headers.forEach((h: any) => {
|
|
360
|
+
if (h.name && h.value) {
|
|
361
|
+
headerMap[h.name.toLowerCase()] = h.value;
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const rawResponse = await gmail.users.messages.get({
|
|
366
|
+
userId: 'me',
|
|
367
|
+
id: firstMessage.id!,
|
|
368
|
+
format: 'raw',
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const parsedMessage = await parseStream(
|
|
372
|
+
Buffer.from(rawResponse.data.raw as string, 'base64').toString(
|
|
373
|
+
'utf-8'
|
|
374
|
+
)
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
results.push({
|
|
378
|
+
id: `test_conversation_${threadId}`,
|
|
379
|
+
data: {
|
|
380
|
+
thread: {
|
|
381
|
+
id: threadId,
|
|
382
|
+
snippet: fullThread.data.snippet,
|
|
383
|
+
messageCount: 1,
|
|
384
|
+
},
|
|
385
|
+
message: {
|
|
386
|
+
...parsedMessage,
|
|
387
|
+
id: firstMessage.id,
|
|
388
|
+
threadId: threadId,
|
|
389
|
+
date: new Date(
|
|
390
|
+
parseInt(firstMessage.internalDate || '0')
|
|
391
|
+
).toISOString(),
|
|
392
|
+
attachments: await convertAttachment(
|
|
393
|
+
parsedMessage.attachments,
|
|
394
|
+
context.files
|
|
395
|
+
),
|
|
396
|
+
},
|
|
397
|
+
conversation: {
|
|
398
|
+
starter: {
|
|
399
|
+
from: headerMap['from'],
|
|
400
|
+
to: headerMap['to'],
|
|
401
|
+
subject: headerMap['subject'],
|
|
402
|
+
date: headerMap['date'],
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return results;
|
|
412
|
+
},
|
|
413
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createTrigger,
|
|
3
|
+
TriggerStrategy,
|
|
4
|
+
FilesService,
|
|
5
|
+
} from '@guayaba/workflows-framework';
|
|
6
|
+
import { GmailLabel } from '../common/models';
|
|
7
|
+
import { GmailProps } from '../common/props';
|
|
8
|
+
import { gmailAuth, createGoogleClient, GmailAuthValue } from '../auth';
|
|
9
|
+
import {
|
|
10
|
+
parseStream,
|
|
11
|
+
convertAttachment,
|
|
12
|
+
getFirstFiveOrAll,
|
|
13
|
+
} from '../common/data';
|
|
14
|
+
import { google } from 'googleapis';
|
|
15
|
+
|
|
16
|
+
export const gmailNewEmailTrigger = createTrigger({
|
|
17
|
+
auth: gmailAuth,
|
|
18
|
+
name: 'gmail_new_email_received',
|
|
19
|
+
displayName: 'New Email',
|
|
20
|
+
description: 'Triggers when new mail is found in your Gmail inbox',
|
|
21
|
+
props: {
|
|
22
|
+
subject: GmailProps.subject,
|
|
23
|
+
from: GmailProps.from,
|
|
24
|
+
to: GmailProps.to,
|
|
25
|
+
label: GmailProps.label,
|
|
26
|
+
category: GmailProps.category,
|
|
27
|
+
},
|
|
28
|
+
sampleData: {},
|
|
29
|
+
type: TriggerStrategy.POLLING,
|
|
30
|
+
async onEnable(context) {
|
|
31
|
+
await context.store.put('lastPoll', Date.now());
|
|
32
|
+
},
|
|
33
|
+
async onDisable(context) {
|
|
34
|
+
return;
|
|
35
|
+
},
|
|
36
|
+
async run(context) {
|
|
37
|
+
const lastFetchEpochMS = (await context.store.get<number>('lastPoll')) ?? 0;
|
|
38
|
+
|
|
39
|
+
const items = await pollRecentMessages({
|
|
40
|
+
auth: context.auth,
|
|
41
|
+
props: context.propsValue,
|
|
42
|
+
files: context.files,
|
|
43
|
+
lastFetchEpochMS: lastFetchEpochMS,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const newLastEpochMilliSeconds = items.reduce(
|
|
47
|
+
(acc, item) => Math.max(acc, item.epochMilliSeconds),
|
|
48
|
+
lastFetchEpochMS
|
|
49
|
+
);
|
|
50
|
+
await context.store.put('lastPoll', newLastEpochMilliSeconds);
|
|
51
|
+
return items
|
|
52
|
+
.filter((f) => f.epochMilliSeconds > lastFetchEpochMS)
|
|
53
|
+
.map((item) => item.data);
|
|
54
|
+
},
|
|
55
|
+
async test(context) {
|
|
56
|
+
const lastFetchEpochMS = (await context.store.get<number>('lastPoll')) ?? 0;
|
|
57
|
+
|
|
58
|
+
const items = await pollRecentMessages({
|
|
59
|
+
auth: context.auth,
|
|
60
|
+
props: context.propsValue,
|
|
61
|
+
files: context.files,
|
|
62
|
+
lastFetchEpochMS: lastFetchEpochMS,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return getFirstFiveOrAll(items.map((item) => item.data));
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
interface PropsValue {
|
|
70
|
+
from: string | undefined;
|
|
71
|
+
to: string | undefined;
|
|
72
|
+
subject: string | undefined;
|
|
73
|
+
label: GmailLabel | undefined;
|
|
74
|
+
category: string | undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function pollRecentMessages({
|
|
78
|
+
auth,
|
|
79
|
+
props,
|
|
80
|
+
files,
|
|
81
|
+
lastFetchEpochMS,
|
|
82
|
+
}: {
|
|
83
|
+
auth: GmailAuthValue;
|
|
84
|
+
props: PropsValue;
|
|
85
|
+
files: FilesService;
|
|
86
|
+
lastFetchEpochMS: number;
|
|
87
|
+
}): Promise<
|
|
88
|
+
{
|
|
89
|
+
epochMilliSeconds: number;
|
|
90
|
+
data: unknown;
|
|
91
|
+
}[]
|
|
92
|
+
> {
|
|
93
|
+
const authClient = await createGoogleClient(auth);
|
|
94
|
+
|
|
95
|
+
const gmail = google.gmail({ version: 'v1', auth: authClient });
|
|
96
|
+
|
|
97
|
+
// construct query
|
|
98
|
+
const query = [];
|
|
99
|
+
const maxResults = lastFetchEpochMS === 0 ? 5 : 20;
|
|
100
|
+
const afterUnixSeconds = Math.floor(lastFetchEpochMS / 1000);
|
|
101
|
+
|
|
102
|
+
if (props.from) query.push(`from:(${props.from})`);
|
|
103
|
+
if (props.to) query.push(`to:(${props.to})`);
|
|
104
|
+
if (props.subject) query.push(`subject:(${props.subject})`);
|
|
105
|
+
if (props.label) query.push(`label:${props.label.name}`);
|
|
106
|
+
if (props.category) query.push(`category:${props.category}`);
|
|
107
|
+
if (afterUnixSeconds != null && afterUnixSeconds > 0)
|
|
108
|
+
query.push(`after:${afterUnixSeconds}`);
|
|
109
|
+
|
|
110
|
+
// List Messages
|
|
111
|
+
const messagesResponse = await gmail.users.messages.list({
|
|
112
|
+
userId: 'me',
|
|
113
|
+
q: query.join(' '),
|
|
114
|
+
maxResults,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Reverse to process oldest-first so partial progress doesn't skip messages
|
|
118
|
+
const messages = (messagesResponse.data.messages || []).slice().reverse();
|
|
119
|
+
|
|
120
|
+
const pollingResponse = [];
|
|
121
|
+
for (const message of messages) {
|
|
122
|
+
try {
|
|
123
|
+
const rawMailResponse = await gmail.users.messages.get({
|
|
124
|
+
userId: 'me',
|
|
125
|
+
id: message.id!,
|
|
126
|
+
format: 'raw',
|
|
127
|
+
});
|
|
128
|
+
const threadResponse = await gmail.users.threads.get({
|
|
129
|
+
userId: 'me',
|
|
130
|
+
id: message.threadId!,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const parsedMailResponse = await parseStream(
|
|
134
|
+
Buffer.from(rawMailResponse.data.raw as string, 'base64').toString(
|
|
135
|
+
'utf-8'
|
|
136
|
+
)
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
pollingResponse.push({
|
|
140
|
+
epochMilliSeconds: Number(rawMailResponse.data.internalDate),
|
|
141
|
+
data: {
|
|
142
|
+
message: {
|
|
143
|
+
...parsedMailResponse,
|
|
144
|
+
attachments: await convertAttachment(
|
|
145
|
+
parsedMailResponse.attachments,
|
|
146
|
+
files
|
|
147
|
+
),
|
|
148
|
+
},
|
|
149
|
+
thread: {
|
|
150
|
+
...threadResponse,
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
} catch (error: any) {
|
|
155
|
+
const isRateLimit =
|
|
156
|
+
error.status === 429 ||
|
|
157
|
+
(error.status === 403 &&
|
|
158
|
+
/quota|rate.?limit/i.test(error.message ?? ''));
|
|
159
|
+
if (isRateLimit) {
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return pollingResponse;
|
|
167
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { createTrigger, TriggerStrategy } from '@guayaba/workflows-framework';
|
|
2
|
+
import { gmailAuth, createGoogleClient } from '../auth';
|
|
3
|
+
import { google } from 'googleapis';
|
|
4
|
+
import { getFirstFiveOrAll } from '../common/data';
|
|
5
|
+
import { isNil } from '@guayaba/workflows-shared';
|
|
6
|
+
|
|
7
|
+
const TRIGGER_KEY = 'labels';
|
|
8
|
+
|
|
9
|
+
export const gmailNewLabelTrigger = createTrigger({
|
|
10
|
+
auth: gmailAuth,
|
|
11
|
+
name: 'new_label',
|
|
12
|
+
displayName: 'New Label',
|
|
13
|
+
description: 'Triggers when a new label is created.',
|
|
14
|
+
props: {},
|
|
15
|
+
sampleData: {},
|
|
16
|
+
type: TriggerStrategy.POLLING,
|
|
17
|
+
async onEnable(context) {
|
|
18
|
+
const authClient = await createGoogleClient(context.auth);
|
|
19
|
+
const gmail = google.gmail({ version: 'v1', auth: authClient });
|
|
20
|
+
|
|
21
|
+
const response = await gmail.users.labels.list({
|
|
22
|
+
userId: 'me',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const labels = response.data.labels || [];
|
|
26
|
+
|
|
27
|
+
const existingLabelIds = labels.map((label) => label.id);
|
|
28
|
+
await context.store.put(TRIGGER_KEY, JSON.stringify(existingLabelIds));
|
|
29
|
+
},
|
|
30
|
+
async onDisable(context) {
|
|
31
|
+
await context.store.delete(TRIGGER_KEY);
|
|
32
|
+
},
|
|
33
|
+
async test(context) {
|
|
34
|
+
const authClient = await createGoogleClient(context.auth);
|
|
35
|
+
const gmail = google.gmail({ version: 'v1', auth: authClient });
|
|
36
|
+
|
|
37
|
+
const response = await gmail.users.labels.list({
|
|
38
|
+
userId: 'me',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const labels = response.data.labels || [];
|
|
42
|
+
|
|
43
|
+
return getFirstFiveOrAll(labels);
|
|
44
|
+
},
|
|
45
|
+
async run(context) {
|
|
46
|
+
const existingIds = (await context.store.get<string>(TRIGGER_KEY)) ?? '[]';
|
|
47
|
+
const parsedExistingIds = JSON.parse(existingIds) as string[];
|
|
48
|
+
|
|
49
|
+
const authClient = await createGoogleClient(context.auth);
|
|
50
|
+
|
|
51
|
+
const gmail = google.gmail({ version: 'v1', auth: authClient });
|
|
52
|
+
|
|
53
|
+
const response = await gmail.users.labels.list({
|
|
54
|
+
userId: 'me',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (isNil(response.data.labels) || response.data.labels.length === 0) {
|
|
58
|
+
await context.store.put(TRIGGER_KEY, '[]');
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const allCurrentIds = response.data.labels.map((label) => label.id);
|
|
63
|
+
|
|
64
|
+
const newLables = response.data.labels.filter((label) => {
|
|
65
|
+
const labelId = label.id ?? undefined;
|
|
66
|
+
return labelId !== undefined && !parsedExistingIds.includes(labelId);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await context.store.put(TRIGGER_KEY, JSON.stringify(allCurrentIds));
|
|
70
|
+
|
|
71
|
+
if (newLables.length === 0) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return newLables;
|
|
76
|
+
},
|
|
77
|
+
});
|