@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,256 @@
|
|
|
1
|
+
import { Property } from '@guayaba/workflows-framework';
|
|
2
|
+
import { GmailRequests } from './data';
|
|
3
|
+
import { GmailLabel } from './models';
|
|
4
|
+
import { gmailAuth, createGoogleClient, GmailAuthValue } from '../auth';
|
|
5
|
+
import { google } from 'googleapis';
|
|
6
|
+
|
|
7
|
+
export const GmailProps = {
|
|
8
|
+
from: Property.ShortText({
|
|
9
|
+
displayName: 'Email sender',
|
|
10
|
+
description:
|
|
11
|
+
'Optional filteration, leave empty to filter based on the email sender',
|
|
12
|
+
required: false,
|
|
13
|
+
defaultValue: '',
|
|
14
|
+
}),
|
|
15
|
+
to: Property.ShortText({
|
|
16
|
+
displayName: 'Email recipient',
|
|
17
|
+
description:
|
|
18
|
+
'Optional filteration, leave empty to filter based on the email recipient',
|
|
19
|
+
required: false,
|
|
20
|
+
defaultValue: '',
|
|
21
|
+
}),
|
|
22
|
+
subject: Property.ShortText({
|
|
23
|
+
displayName: 'Email subject',
|
|
24
|
+
description: 'The email subject',
|
|
25
|
+
required: false,
|
|
26
|
+
defaultValue: '',
|
|
27
|
+
}),
|
|
28
|
+
category: Property.StaticDropdown({
|
|
29
|
+
displayName: 'Category',
|
|
30
|
+
description:
|
|
31
|
+
'Optional filteration, leave unselected to filter based on the email category',
|
|
32
|
+
required: false,
|
|
33
|
+
options: {
|
|
34
|
+
disabled: false,
|
|
35
|
+
options: [
|
|
36
|
+
{ label: 'Primary', value: 'primary' },
|
|
37
|
+
{ label: 'Social', value: 'social' },
|
|
38
|
+
{ label: 'Promotions', value: 'promotions' },
|
|
39
|
+
{ label: 'Updates', value: 'updates' },
|
|
40
|
+
{ label: 'Forums', value: 'forums' },
|
|
41
|
+
{ label: 'Reservations', value: 'reservations' },
|
|
42
|
+
{ label: 'Purchases', value: 'purchases' },
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
46
|
+
label: Property.Dropdown<GmailLabel, false, typeof gmailAuth>({
|
|
47
|
+
auth: gmailAuth,
|
|
48
|
+
displayName: 'Label',
|
|
49
|
+
description:
|
|
50
|
+
'Optional filteration, leave unselected to filter based on the email label',
|
|
51
|
+
required: false,
|
|
52
|
+
defaultValue: '',
|
|
53
|
+
refreshers: [],
|
|
54
|
+
options: async ({ auth }) => {
|
|
55
|
+
if (!auth) {
|
|
56
|
+
return {
|
|
57
|
+
disabled: true,
|
|
58
|
+
options: [],
|
|
59
|
+
placeholder: 'please authenticate first',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const response = await GmailRequests.getLabels(auth);
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
disabled: false,
|
|
67
|
+
options: response.body.labels.map((label) => ({
|
|
68
|
+
label: label.name,
|
|
69
|
+
value: label,
|
|
70
|
+
})),
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
}),
|
|
74
|
+
unread: (required = false) =>
|
|
75
|
+
Property.Checkbox({
|
|
76
|
+
displayName: 'Is unread?',
|
|
77
|
+
description: 'Check if the email is unread or not',
|
|
78
|
+
required,
|
|
79
|
+
defaultValue: false,
|
|
80
|
+
}),
|
|
81
|
+
message: Property.Dropdown({
|
|
82
|
+
displayName: 'Message',
|
|
83
|
+
description:
|
|
84
|
+
'Select a message from the list or enter a message ID manually.',
|
|
85
|
+
required: true,
|
|
86
|
+
auth: gmailAuth,
|
|
87
|
+
refreshers: [],
|
|
88
|
+
options: async ({ auth }) => {
|
|
89
|
+
if (!auth) {
|
|
90
|
+
return {
|
|
91
|
+
disabled: true,
|
|
92
|
+
options: [],
|
|
93
|
+
placeholder: 'Please authenticate first',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const authValue = auth as GmailAuthValue;
|
|
99
|
+
const authClient = await createGoogleClient(authValue);
|
|
100
|
+
|
|
101
|
+
const gmail = google.gmail({ version: 'v1', auth: authClient });
|
|
102
|
+
|
|
103
|
+
const response = await GmailRequests.getRecentMessages(
|
|
104
|
+
authValue,
|
|
105
|
+
20 // Get last 20 messages
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
if (!response.body.messages || response.body.messages.length === 0) {
|
|
109
|
+
return {
|
|
110
|
+
disabled: false,
|
|
111
|
+
options: [],
|
|
112
|
+
placeholder:
|
|
113
|
+
'No recent messages found. You can enter a message ID manually.',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Get message details for better display
|
|
118
|
+
const messageDetails = await Promise.all(
|
|
119
|
+
response.body.messages
|
|
120
|
+
.slice(0, 10)
|
|
121
|
+
.map(async (msg: { id: string; threadId: string }) => {
|
|
122
|
+
try {
|
|
123
|
+
const details = await gmail.users.messages.get({
|
|
124
|
+
metadataHeaders: ['Subject'],
|
|
125
|
+
format: 'metadata',
|
|
126
|
+
id: msg.id,
|
|
127
|
+
userId: 'me',
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const headers = details.data.payload?.headers || [];
|
|
131
|
+
const subject =
|
|
132
|
+
headers.find((h: any) => h.name === 'Subject')?.value ||
|
|
133
|
+
'No Subject';
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
id: msg.id,
|
|
137
|
+
subject:
|
|
138
|
+
subject.length > 50
|
|
139
|
+
? subject.substring(0, 50) + '...'
|
|
140
|
+
: subject,
|
|
141
|
+
};
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.log(error);
|
|
144
|
+
return {
|
|
145
|
+
id: msg.id,
|
|
146
|
+
subject: 'Unable to load details',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
disabled: false,
|
|
154
|
+
options: messageDetails.map((msg) => ({
|
|
155
|
+
label: msg.subject,
|
|
156
|
+
value: msg.id,
|
|
157
|
+
})),
|
|
158
|
+
};
|
|
159
|
+
} catch (error) {
|
|
160
|
+
return {
|
|
161
|
+
disabled: false,
|
|
162
|
+
options: [],
|
|
163
|
+
placeholder:
|
|
164
|
+
'Error loading recent messages. You can enter a message ID manually.',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
thread: Property.Dropdown({
|
|
170
|
+
displayName: 'Thread',
|
|
171
|
+
description: 'Select a thread from the list or enter a thread ID manually',
|
|
172
|
+
required: true,
|
|
173
|
+
refreshers: [],
|
|
174
|
+
auth: gmailAuth,
|
|
175
|
+
options: async ({ auth }) => {
|
|
176
|
+
if (!auth) {
|
|
177
|
+
return {
|
|
178
|
+
disabled: true,
|
|
179
|
+
options: [],
|
|
180
|
+
placeholder: 'Please authenticate first',
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const authValue = auth as GmailAuthValue;
|
|
186
|
+
const authClient = await createGoogleClient(authValue);
|
|
187
|
+
|
|
188
|
+
const gmail = google.gmail({ version: 'v1', auth: authClient });
|
|
189
|
+
|
|
190
|
+
const response = await GmailRequests.getRecentThreads(
|
|
191
|
+
authValue,
|
|
192
|
+
15 // Get last 15 threads
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
if (!response.body.threads || response.body.threads.length === 0) {
|
|
196
|
+
return {
|
|
197
|
+
disabled: false,
|
|
198
|
+
options: [],
|
|
199
|
+
placeholder:
|
|
200
|
+
'No recent threads found. You can enter a thread ID manually.',
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Get thread details for better display
|
|
205
|
+
const threadDetails = await Promise.all(
|
|
206
|
+
response.body.threads
|
|
207
|
+
.slice(0, 10)
|
|
208
|
+
.map(async (thread: { id: string; snippet?: string }) => {
|
|
209
|
+
try {
|
|
210
|
+
const details = await await gmail.users.threads.get({
|
|
211
|
+
metadataHeaders: ['Subject'],
|
|
212
|
+
format: 'metadata',
|
|
213
|
+
id: thread.id,
|
|
214
|
+
userId: 'me',
|
|
215
|
+
});
|
|
216
|
+
// Get the first message to extract subject and participants
|
|
217
|
+
const firstMessage = details.data.messages?.[0];
|
|
218
|
+
const headers = firstMessage?.payload?.headers || [];
|
|
219
|
+
const subject =
|
|
220
|
+
headers.find((h: any) => h.name === 'Subject')?.value ||
|
|
221
|
+
'No Subject';
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
id: thread.id,
|
|
225
|
+
subject:
|
|
226
|
+
subject.length > 50
|
|
227
|
+
? subject.substring(0, 50) + '...'
|
|
228
|
+
: subject,
|
|
229
|
+
};
|
|
230
|
+
} catch (error) {
|
|
231
|
+
return {
|
|
232
|
+
id: thread.id,
|
|
233
|
+
subject: 'Unable to load details',
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
})
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
disabled: false,
|
|
241
|
+
options: threadDetails.map((thread) => ({
|
|
242
|
+
label: thread.subject,
|
|
243
|
+
value: thread.id,
|
|
244
|
+
})),
|
|
245
|
+
};
|
|
246
|
+
} catch (error) {
|
|
247
|
+
return {
|
|
248
|
+
disabled: false,
|
|
249
|
+
options: [],
|
|
250
|
+
placeholder:
|
|
251
|
+
'Error loading recent threads. You can enter a thread ID manually.',
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
}),
|
|
256
|
+
};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createTrigger,
|
|
3
|
+
TriggerStrategy,
|
|
4
|
+
Property,
|
|
5
|
+
FilesService,
|
|
6
|
+
} from '@guayaba/workflows-framework';
|
|
7
|
+
import { GmailProps } from '../common/props';
|
|
8
|
+
import { gmailAuth, createGoogleClient, GmailAuthValue } from '../auth';
|
|
9
|
+
import { google } from 'googleapis';
|
|
10
|
+
import {
|
|
11
|
+
parseStream,
|
|
12
|
+
convertAttachment,
|
|
13
|
+
getFirstFiveOrAll,
|
|
14
|
+
} from '../common/data';
|
|
15
|
+
import { GmailLabel } from '../common/models';
|
|
16
|
+
|
|
17
|
+
type Props = {
|
|
18
|
+
from?: string;
|
|
19
|
+
to?: string;
|
|
20
|
+
subject?: string;
|
|
21
|
+
label?: GmailLabel;
|
|
22
|
+
category?: string;
|
|
23
|
+
filenameExtension?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const gmailNewAttachmentTrigger = createTrigger({
|
|
27
|
+
auth: gmailAuth,
|
|
28
|
+
name: 'new_attachment',
|
|
29
|
+
displayName: 'New Attachment',
|
|
30
|
+
description: 'Triggers when an email with an attachment arrives.',
|
|
31
|
+
props: {
|
|
32
|
+
from: {
|
|
33
|
+
...GmailProps.from,
|
|
34
|
+
description: 'Filter by sender email.',
|
|
35
|
+
displayName: 'From',
|
|
36
|
+
required: false,
|
|
37
|
+
},
|
|
38
|
+
to: {
|
|
39
|
+
...GmailProps.to,
|
|
40
|
+
description: 'Filter by recipient email.',
|
|
41
|
+
displayName: 'To',
|
|
42
|
+
required: false,
|
|
43
|
+
},
|
|
44
|
+
subject: Property.ShortText({
|
|
45
|
+
displayName: 'Subject Contains',
|
|
46
|
+
description:
|
|
47
|
+
'Only trigger for emails containing this text in the subject.',
|
|
48
|
+
required: false,
|
|
49
|
+
}),
|
|
50
|
+
label: {
|
|
51
|
+
...GmailProps.label,
|
|
52
|
+
description: 'Filter by Gmail label.',
|
|
53
|
+
displayName: 'Label',
|
|
54
|
+
required: false,
|
|
55
|
+
},
|
|
56
|
+
category: {
|
|
57
|
+
...GmailProps.category,
|
|
58
|
+
description: 'Filter by Gmail category.',
|
|
59
|
+
displayName: 'Category',
|
|
60
|
+
required: false,
|
|
61
|
+
},
|
|
62
|
+
filenameExtension: Property.ShortText({
|
|
63
|
+
displayName: 'File Extension',
|
|
64
|
+
description:
|
|
65
|
+
'Only trigger for attachments with this file extension (e.g., pdf, jpg, docx).',
|
|
66
|
+
required: false,
|
|
67
|
+
}),
|
|
68
|
+
},
|
|
69
|
+
sampleData: {},
|
|
70
|
+
type: TriggerStrategy.POLLING,
|
|
71
|
+
async onEnable(context) {
|
|
72
|
+
await context.store.put('lastPoll', Date.now());
|
|
73
|
+
},
|
|
74
|
+
async onDisable(context) {
|
|
75
|
+
return;
|
|
76
|
+
},
|
|
77
|
+
async run(context) {
|
|
78
|
+
const lastFetchEpochMS = (await context.store.get<number>('lastPoll')) ?? 0;
|
|
79
|
+
|
|
80
|
+
const items = await pollRecentMessages({
|
|
81
|
+
auth: context.auth,
|
|
82
|
+
props: context.propsValue,
|
|
83
|
+
files: context.files,
|
|
84
|
+
lastFetchEpochMS: lastFetchEpochMS,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const newLastEpochMilliSeconds = items.reduce(
|
|
88
|
+
(acc, item) => Math.max(acc, item.epochMilliSeconds),
|
|
89
|
+
lastFetchEpochMS
|
|
90
|
+
);
|
|
91
|
+
await context.store.put('lastPoll', newLastEpochMilliSeconds);
|
|
92
|
+
return items
|
|
93
|
+
.filter((f) => f.epochMilliSeconds > lastFetchEpochMS)
|
|
94
|
+
.map((item) => item.data);
|
|
95
|
+
},
|
|
96
|
+
async test(context) {
|
|
97
|
+
const lastFetchEpochMS = (await context.store.get<number>('lastPoll')) ?? 0;
|
|
98
|
+
|
|
99
|
+
const items = await pollRecentMessages({
|
|
100
|
+
auth: context.auth,
|
|
101
|
+
props: context.propsValue,
|
|
102
|
+
files: context.files,
|
|
103
|
+
lastFetchEpochMS: lastFetchEpochMS,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return getFirstFiveOrAll(items.map((item) => item.data));
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
async function pollRecentMessages({
|
|
111
|
+
auth,
|
|
112
|
+
props,
|
|
113
|
+
files,
|
|
114
|
+
lastFetchEpochMS,
|
|
115
|
+
}: {
|
|
116
|
+
auth: GmailAuthValue;
|
|
117
|
+
props: Props;
|
|
118
|
+
files: FilesService;
|
|
119
|
+
lastFetchEpochMS: number;
|
|
120
|
+
}): Promise<
|
|
121
|
+
{
|
|
122
|
+
epochMilliSeconds: number;
|
|
123
|
+
data: unknown;
|
|
124
|
+
}[]
|
|
125
|
+
> {
|
|
126
|
+
const authClient = await createGoogleClient(auth);
|
|
127
|
+
|
|
128
|
+
const gmail = google.gmail({ version: 'v1', auth: authClient });
|
|
129
|
+
|
|
130
|
+
// construct query
|
|
131
|
+
const query = ['has:attachment'];
|
|
132
|
+
const maxResults = lastFetchEpochMS === 0 ? 5 : 20;
|
|
133
|
+
const afterUnixSeconds = Math.floor(lastFetchEpochMS / 1000);
|
|
134
|
+
|
|
135
|
+
if (props.from) query.push(`from:(${props.from})`);
|
|
136
|
+
if (props.to) query.push(`to:(${props.to})`);
|
|
137
|
+
if (props.subject) query.push(`subject:(${props.subject})`);
|
|
138
|
+
if (props.label) query.push(`label:${props.label.name}`);
|
|
139
|
+
if (props.category) query.push(`category:${props.category}`);
|
|
140
|
+
if (props.filenameExtension)
|
|
141
|
+
query.push(`filename:${props.filenameExtension}`);
|
|
142
|
+
if (afterUnixSeconds != null && afterUnixSeconds > 0)
|
|
143
|
+
query.push(`after:${afterUnixSeconds}`);
|
|
144
|
+
|
|
145
|
+
// List Messages
|
|
146
|
+
const messagesResponse = await gmail.users.messages.list({
|
|
147
|
+
userId: 'me',
|
|
148
|
+
q: query.join(' '),
|
|
149
|
+
maxResults,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Reverse to process oldest-first so partial progress doesn't skip messages
|
|
153
|
+
const messages = (messagesResponse.data.messages || []).slice().reverse();
|
|
154
|
+
|
|
155
|
+
const pollingResponse = [];
|
|
156
|
+
for (const message of messages) {
|
|
157
|
+
try {
|
|
158
|
+
const rawMailResponse = await gmail.users.messages.get({
|
|
159
|
+
userId: 'me',
|
|
160
|
+
id: message.id!,
|
|
161
|
+
format: 'raw',
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const parsedMailResponse = await parseStream(
|
|
165
|
+
Buffer.from(rawMailResponse.data.raw as string, 'base64').toString(
|
|
166
|
+
'utf-8'
|
|
167
|
+
)
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const { attachments, ...restOfParsedMailResponse } = parsedMailResponse;
|
|
171
|
+
const parsedAttachments = await convertAttachment(attachments, files);
|
|
172
|
+
|
|
173
|
+
for (const attachment of parsedAttachments) {
|
|
174
|
+
pollingResponse.push({
|
|
175
|
+
epochMilliSeconds: Number(rawMailResponse.data.internalDate),
|
|
176
|
+
data: {
|
|
177
|
+
attachment,
|
|
178
|
+
message: {
|
|
179
|
+
id: message.id,
|
|
180
|
+
...restOfParsedMailResponse,
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
} catch (error: any) {
|
|
186
|
+
const isRateLimit =
|
|
187
|
+
error.status === 429 ||
|
|
188
|
+
(error.status === 403 &&
|
|
189
|
+
/quota|rate.?limit/i.test(error.message ?? ''));
|
|
190
|
+
if (isRateLimit) {
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return pollingResponse;
|
|
198
|
+
}
|