@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.
@@ -0,0 +1,194 @@
1
+ import { createAction, Property } from '@guayaba/workflows-framework';
2
+ import {
3
+ gmailAuth,
4
+ createGoogleClient,
5
+ getAccessToken,
6
+ getUserEmail,
7
+ } from '../auth';
8
+ import { google } from 'googleapis';
9
+ import MailComposer from 'nodemailer/lib/mail-composer';
10
+ import Mail from 'nodemailer/lib/mailer';
11
+ import { assertNotNullOrUndefined, ExecutionType } from '@guayaba/workflows-shared';
12
+
13
+ export const requestApprovalInEmail = createAction({
14
+ auth: gmailAuth,
15
+ name: 'request_approval_in_mail',
16
+ displayName: 'Request Approval in Email',
17
+ description:
18
+ 'Send approval request email and then wait until the email is approved or disapproved',
19
+ props: {
20
+ receiver: Property.ShortText({
21
+ displayName: 'Receiver Email (To)',
22
+ description:
23
+ 'The email address of the recipient who will receive the approval request.',
24
+ required: true,
25
+ }),
26
+
27
+ cc: Property.Array({
28
+ displayName: 'CC Email',
29
+ description:
30
+ 'The email addresses of the recipients who will receive a carbon copy of the approval request.',
31
+ required: false,
32
+ }),
33
+ bcc: Property.Array({
34
+ displayName: 'BCC Email',
35
+ description:
36
+ 'The email addresses of the recipients who will receive a blind carbon copy of the approval request.',
37
+ required: false,
38
+ }),
39
+ subject: Property.ShortText({
40
+ displayName: 'Subject',
41
+ description: 'The subject of the approval request email.',
42
+ required: true,
43
+ }),
44
+ body: Property.ShortText({
45
+ displayName: 'Body',
46
+ description: 'Body for the email you want to send',
47
+ required: true,
48
+ }),
49
+ reply_to: Property.Array({
50
+ displayName: 'Reply-To Email',
51
+ description: 'Email address to set as the "Reply-To" header',
52
+ required: false,
53
+ }),
54
+ sender_name: Property.ShortText({
55
+ displayName: 'Sender Name',
56
+ required: false,
57
+ }),
58
+ from: Property.ShortText({
59
+ displayName: 'Sender Email',
60
+ description:
61
+ "The address must be listed in your GMail account's settings",
62
+ required: false,
63
+ }),
64
+ in_reply_to: Property.ShortText({
65
+ displayName: 'In reply to',
66
+ description: 'Reply to this Message-ID',
67
+ required: false,
68
+ }),
69
+ },
70
+ async run(context) {
71
+ if (context.executionType === ExecutionType.BEGIN) {
72
+ try {
73
+ const token = await getAccessToken(context.auth);
74
+
75
+ const { subject, body } = context.propsValue;
76
+
77
+ assertNotNullOrUndefined(token, 'token');
78
+ assertNotNullOrUndefined(context.propsValue.receiver, 'receiver');
79
+ assertNotNullOrUndefined(subject, 'subject');
80
+ assertNotNullOrUndefined(body, 'body');
81
+
82
+ const waitpoint = await context.run.createWaitpoint({
83
+ type: 'WEBHOOK',
84
+ });
85
+
86
+ const approvalLink = waitpoint.buildResumeUrl({
87
+ queryParams: { action: 'approve' },
88
+ });
89
+ const disapprovalLink = waitpoint.buildResumeUrl({
90
+ queryParams: { action: 'disapprove' },
91
+ });
92
+
93
+ const htmlBody = `
94
+ <div>
95
+ <p>${body}</p>
96
+ <br />
97
+ <p>
98
+ <a href="${approvalLink}" style="display: inline-block; padding: 10px 20px; margin-right: 10px; background-color: #2acc50; color: white; text-decoration: none; border-radius: 4px;">Approve</a>
99
+ <a href="${disapprovalLink}" style="display: inline-block; padding: 10px 20px; background-color: #e4172b; color: white; text-decoration: none; border-radius: 4px;">Disapprove</a>
100
+ </p>
101
+ </div>
102
+ `;
103
+
104
+ const authClient = await createGoogleClient(context.auth);
105
+
106
+ const gmail = google.gmail({ version: 'v1', auth: authClient });
107
+
108
+ const subjectBase64 = Buffer.from(
109
+ context.propsValue['subject']
110
+ ).toString('base64');
111
+
112
+ const replyTo = context.propsValue['reply_to']?.filter(
113
+ (email) => email !== ''
114
+ );
115
+ const receiverEmail = context.propsValue.receiver;
116
+ const cc = context.propsValue['cc']?.filter((email) => email !== '');
117
+ const bcc = context.propsValue['bcc']?.filter((email) => email !== '');
118
+ const mailOptions: Mail.Options = {
119
+ to: receiverEmail,
120
+ cc: cc ? cc.join(', ') : undefined,
121
+ bcc: bcc ? bcc.join(', ') : undefined,
122
+ subject: `=?UTF-8?B?${subjectBase64}?=`,
123
+ replyTo: replyTo ? replyTo.join(', ') : '',
124
+ // text:
125
+ // context.propsValue.body_type === 'plain_text'
126
+ // ? context.propsValue['body']
127
+ // : undefined,
128
+ html: htmlBody,
129
+ attachments: [],
130
+ };
131
+
132
+ const senderEmail =
133
+ context.propsValue.from ||
134
+ (await getUserEmail(context.auth, authClient));
135
+ if (senderEmail) {
136
+ mailOptions.from = context.propsValue.sender_name
137
+ ? `${context.propsValue['sender_name']} <${senderEmail}>`
138
+ : senderEmail;
139
+ }
140
+ let threadId = undefined;
141
+ if (context.propsValue.in_reply_to) {
142
+ mailOptions.headers = [
143
+ {
144
+ key: 'References',
145
+ value: context.propsValue.in_reply_to,
146
+ },
147
+ {
148
+ key: 'In-Reply-To',
149
+ value: context.propsValue.in_reply_to,
150
+ },
151
+ ];
152
+ const messages = await gmail.users.messages.list({
153
+ userId: 'me',
154
+ q: `Rfc822msgid:${context.propsValue.in_reply_to}`,
155
+ });
156
+ threadId = messages.data.messages?.[0].threadId;
157
+ }
158
+ const mail: any = new MailComposer(mailOptions).compile();
159
+ mail.keepBcc = true;
160
+ const mailBody = await mail.build();
161
+
162
+ const encodedPayload = Buffer.from(mailBody)
163
+ .toString('base64')
164
+ .replace(/\+/g, '-')
165
+ .replace(/\//g, '_');
166
+ await gmail.users.messages.send({
167
+ userId: 'me',
168
+ requestBody: {
169
+ threadId,
170
+ raw: encodedPayload,
171
+ },
172
+ });
173
+ context.run.waitForWaitpoint(waitpoint.id);
174
+
175
+ return {
176
+ approved: false, // default approval is false
177
+ };
178
+ } catch (error) {
179
+ console.error(
180
+ '[RequestApprovalEmail] Error during BEGIN execution:',
181
+ error
182
+ );
183
+ throw error;
184
+ }
185
+ } else {
186
+ const action = context.resumePayload.queryParams['action'];
187
+ const approved = action === 'approve';
188
+
189
+ return {
190
+ approved,
191
+ };
192
+ }
193
+ },
194
+ });
@@ -0,0 +1,211 @@
1
+ import { createAction, Property } from '@guayaba/workflows-framework';
2
+ import { gmailAuth, createGoogleClient } from '../auth';
3
+ import { google } from 'googleapis';
4
+ import { convertAttachment, parseStream } from '../common/data';
5
+ import { GmailProps } from '../common/props';
6
+ import { GmailLabel } from '../common/models';
7
+
8
+ export const gmailSearchMailAction = createAction({
9
+ auth: gmailAuth,
10
+ name: 'gmail_search_mail',
11
+ displayName: 'Find Email',
12
+ description:
13
+ 'Find emails using advanced search criteria. If no filters are provided, the latest emails are returned.',
14
+ props: {
15
+ from: GmailProps.from,
16
+ to: GmailProps.to,
17
+ subject: GmailProps.subject,
18
+ content: Property.ShortText({
19
+ displayName: 'Email Content',
20
+ description: 'Search for specific text within email body',
21
+ required: false,
22
+ }),
23
+ has_attachment: Property.Checkbox({
24
+ displayName: 'Has Attachment',
25
+ description: 'Only find emails with attachments',
26
+ required: false,
27
+ defaultValue: false,
28
+ }),
29
+ attachment_name: Property.ShortText({
30
+ displayName: 'Attachment Name',
31
+ description: 'Search for emails with specific attachment filename',
32
+ required: false,
33
+ }),
34
+ label: GmailProps.label,
35
+ category: GmailProps.category,
36
+ after_date: Property.DateTime({
37
+ displayName: 'After Date',
38
+ description: 'Find emails sent after this date',
39
+ required: false,
40
+ }),
41
+ before_date: Property.DateTime({
42
+ displayName: 'Before Date',
43
+ description: 'Find emails sent before this date',
44
+ required: false,
45
+ }),
46
+
47
+ include_spam_trash: Property.Checkbox({
48
+ displayName: 'Include Spam & Trash',
49
+ description:
50
+ 'Include emails from Spam and Trash folders in search results',
51
+ required: false,
52
+ defaultValue: false,
53
+ }),
54
+ max_results: Property.Number({
55
+ displayName: 'Max Results',
56
+ description: 'Maximum number of emails to return (1-500)',
57
+ required: false,
58
+ defaultValue: 10,
59
+ }),
60
+ },
61
+ async run(context) {
62
+ const authClient = await createGoogleClient(context.auth);
63
+
64
+ const gmail = google.gmail({ version: 'v1', auth: authClient });
65
+
66
+ const queryParts: string[] = [];
67
+
68
+ if (context.propsValue.from?.trim()) {
69
+ queryParts.push(`from:(${context.propsValue.from.trim()})`);
70
+ }
71
+ if (context.propsValue.to?.trim()) {
72
+ queryParts.push(`to:(${context.propsValue.to.trim()})`);
73
+ }
74
+ if (context.propsValue.subject?.trim()) {
75
+ queryParts.push(`subject:(${context.propsValue.subject.trim()})`);
76
+ }
77
+ if (context.propsValue.content?.trim()) {
78
+ queryParts.push(`"${context.propsValue.content.trim()}"`);
79
+ }
80
+
81
+ if (context.propsValue.has_attachment) {
82
+ queryParts.push('has:attachment');
83
+ }
84
+ if (context.propsValue.attachment_name?.trim()) {
85
+ queryParts.push(
86
+ `filename:(${context.propsValue.attachment_name.trim()})`
87
+ );
88
+ }
89
+
90
+ if (context.propsValue.label) {
91
+ const label = context.propsValue.label as GmailLabel;
92
+ queryParts.push(`label:${label.name}`);
93
+ }
94
+ if (context.propsValue.category?.trim()) {
95
+ queryParts.push(`category:${context.propsValue.category.trim()}`);
96
+ }
97
+
98
+ if (context.propsValue.after_date) {
99
+ const afterDate = new Date(context.propsValue.after_date);
100
+ const afterDateStr = afterDate
101
+ .toISOString()
102
+ .split('T')[0]
103
+ .replace(/-/g, '/');
104
+ queryParts.push(`after:${afterDateStr}`);
105
+ }
106
+ if (context.propsValue.before_date) {
107
+ const beforeDate = new Date(context.propsValue.before_date);
108
+ const beforeDateStr = beforeDate
109
+ .toISOString()
110
+ .split('T')[0]
111
+ .replace(/-/g, '/');
112
+ queryParts.push(`before:${beforeDateStr}`);
113
+ }
114
+
115
+ const searchQuery = queryParts.join(' ');
116
+
117
+ const maxResults = Math.min(
118
+ Math.max(context.propsValue.max_results || 10, 1),
119
+ 500
120
+ );
121
+
122
+ try {
123
+ const searchResponse = await gmail.users.messages.list({
124
+ userId: 'me',
125
+ ...(searchQuery.trim() ? { q: searchQuery } : {}),
126
+ maxResults: maxResults,
127
+ includeSpamTrash: context.propsValue.include_spam_trash,
128
+ });
129
+
130
+ const messages = searchResponse.data.messages || [];
131
+
132
+ if (messages.length === 0) {
133
+ return {
134
+ found: false,
135
+ results: {
136
+ messages: [],
137
+ count: 0,
138
+ },
139
+ };
140
+ }
141
+
142
+ const detailedMessages = await Promise.all(
143
+ messages.map(async (message) => {
144
+ try {
145
+ const rawMailResponse = await gmail.users.messages.get({
146
+ userId: 'me',
147
+ id: message.id!,
148
+ format: 'raw',
149
+ });
150
+
151
+ const parsedMailResponse = await parseStream(
152
+ Buffer.from(
153
+ rawMailResponse.data.raw as string,
154
+ 'base64'
155
+ ).toString('utf-8')
156
+ );
157
+
158
+ return {
159
+ id: message.id,
160
+ ...parsedMailResponse,
161
+ attachments: await convertAttachment(
162
+ parsedMailResponse.attachments,
163
+ context.files
164
+ ),
165
+ };
166
+ } catch (error) {
167
+ console.error(
168
+ `Failed to get details for message ${message.id}:`,
169
+ error
170
+ );
171
+ return {
172
+ id: message.id,
173
+ threadId: message.threadId,
174
+ error: 'Failed to retrieve message details',
175
+ };
176
+ }
177
+ })
178
+ );
179
+
180
+ return {
181
+ found: true,
182
+ results: {
183
+ messages: detailedMessages,
184
+ count: detailedMessages.length,
185
+ },
186
+ };
187
+ } catch (error: any) {
188
+ // Enhanced error handling
189
+ if (error.code === 400) {
190
+ if (error.message?.includes('Invalid query')) {
191
+ throw new Error(
192
+ `Invalid search query: "${searchQuery}". Please check your search syntax.`
193
+ );
194
+ }
195
+ throw new Error(`Invalid search request: ${error.message}`);
196
+ } else if (error.code === 403) {
197
+ throw new Error(
198
+ 'Insufficient permissions to search emails. Ensure the gmail.readonly scope is granted.'
199
+ );
200
+ } else if (error.code === 429) {
201
+ throw new Error(
202
+ 'Gmail API rate limit exceeded. Please try again later.'
203
+ );
204
+ } else if (error.code === 500) {
205
+ throw new Error('Gmail API server error. Please try again later.');
206
+ }
207
+
208
+ throw new Error(`Failed to search emails: ${error.message}`);
209
+ }
210
+ },
211
+ });
@@ -0,0 +1,205 @@
1
+ import { ApFile, createAction, Property } from '@guayaba/workflows-framework';
2
+ import mime from 'mime-types';
3
+ import MailComposer from 'nodemailer/lib/mail-composer';
4
+ import Mail, { Attachment } from 'nodemailer/lib/mailer';
5
+ import { gmailAuth, createGoogleClient, getUserEmail } from '../auth';
6
+ import { google } from 'googleapis';
7
+
8
+ export const gmailSendEmailAction = createAction({
9
+ auth: gmailAuth,
10
+ name: 'send_email',
11
+ description: 'Send an email through a Gmail account',
12
+ displayName: 'Send Email',
13
+ props: {
14
+ receiver: Property.Array({
15
+ displayName: 'Receiver Email (To)',
16
+ description: undefined,
17
+ required: true,
18
+ }),
19
+ cc: Property.Array({
20
+ displayName: 'CC Email',
21
+ description: undefined,
22
+ required: false,
23
+ }),
24
+ bcc: Property.Array({
25
+ displayName: 'BCC Email',
26
+ description: undefined,
27
+ required: false,
28
+ }),
29
+ subject: Property.ShortText({
30
+ displayName: 'Subject',
31
+ description: undefined,
32
+ required: true,
33
+ }),
34
+ body_type: Property.StaticDropdown({
35
+ displayName: 'Body Type',
36
+ required: true,
37
+ defaultValue: 'plain_text',
38
+ options: {
39
+ disabled: false,
40
+ options: [
41
+ {
42
+ label: 'plain text',
43
+ value: 'plain_text',
44
+ },
45
+ {
46
+ label: 'html',
47
+ value: 'html',
48
+ },
49
+ ],
50
+ },
51
+ }),
52
+ body: Property.ShortText({
53
+ displayName: 'Body',
54
+ description: 'Body for the email you want to send',
55
+ required: true,
56
+ }),
57
+ reply_to: Property.Array({
58
+ displayName: 'Reply-To Email',
59
+ description: 'Email address to set as the "Reply-To" header',
60
+ required: false,
61
+ }),
62
+ sender_name: Property.ShortText({
63
+ displayName: 'Sender Name',
64
+ required: false,
65
+ }),
66
+ from: Property.ShortText({
67
+ displayName: 'Sender Email',
68
+ description:
69
+ "The address must be listed in your GMail account's settings",
70
+ required: false,
71
+ }),
72
+ attachments: Property.Array({
73
+ displayName: 'Attachments',
74
+ required: false,
75
+ properties: {
76
+ file: Property.File({
77
+ displayName: 'File',
78
+ description: 'File to attach to the email you want to send.',
79
+ required: true,
80
+ }),
81
+ name: Property.ShortText({
82
+ displayName: 'Attachment Name',
83
+ description: 'In case you want to change the name of the attachment.',
84
+ required: false,
85
+ }),
86
+ },
87
+ }),
88
+ in_reply_to: Property.ShortText({
89
+ displayName: 'In reply to',
90
+ description: 'Reply to this Message-ID',
91
+ required: false,
92
+ }),
93
+ draft: Property.Checkbox({
94
+ displayName: 'Create draft',
95
+ description: 'Create draft without sending the actual email',
96
+ required: true,
97
+ defaultValue: false,
98
+ }),
99
+ },
100
+ async run(context) {
101
+ const authClient = await createGoogleClient(context.auth);
102
+
103
+ const gmail = google.gmail({ version: 'v1', auth: authClient });
104
+
105
+ const subjectBase64 = Buffer.from(context.propsValue['subject']).toString(
106
+ 'base64'
107
+ );
108
+ const attachments = context.propsValue.attachments as {
109
+ file: ApFile;
110
+ name: string | undefined;
111
+ }[];
112
+ const replyTo = context.propsValue['reply_to']?.filter(
113
+ (email) => email !== ''
114
+ );
115
+ const receiver = context.propsValue['receiver']?.filter(
116
+ (email) => email !== ''
117
+ );
118
+ const cc = context.propsValue['cc']?.filter((email) => email !== '');
119
+ const bcc = context.propsValue['bcc']?.filter((email) => email !== '');
120
+ const mailOptions: Mail.Options = {
121
+ to: receiver.join(', '), // Join all email addresses with a comma
122
+ cc: cc ? cc.join(', ') : undefined,
123
+ bcc: bcc ? bcc.join(', ') : undefined,
124
+ subject: `=?UTF-8?B?${subjectBase64}?=`,
125
+ replyTo: replyTo ? replyTo.join(', ') : '',
126
+ text:
127
+ context.propsValue.body_type === 'plain_text'
128
+ ? context.propsValue['body']
129
+ : undefined,
130
+ html:
131
+ context.propsValue.body_type === 'html'
132
+ ? context.propsValue['body']
133
+ : undefined,
134
+ attachments: [],
135
+ };
136
+ let threadId = undefined;
137
+ if (context.propsValue.in_reply_to) {
138
+ mailOptions.headers = [
139
+ {
140
+ key: 'References',
141
+ value: context.propsValue.in_reply_to,
142
+ },
143
+ {
144
+ key: 'In-Reply-To',
145
+ value: context.propsValue.in_reply_to,
146
+ },
147
+ ];
148
+ const messages = await gmail.users.messages.list({
149
+ userId: 'me',
150
+ q: `Rfc822msgid:${context.propsValue.in_reply_to}`,
151
+ });
152
+ threadId = messages.data.messages?.[0].threadId;
153
+ }
154
+
155
+ const senderEmail =
156
+ context.propsValue.from || (await getUserEmail(context.auth, authClient));
157
+ if (senderEmail) {
158
+ mailOptions.from = context.propsValue.sender_name
159
+ ? `${context.propsValue['sender_name']} <${senderEmail}>`
160
+ : senderEmail;
161
+ }
162
+
163
+ if (attachments && attachments.length > 0) {
164
+ const attachmentOption: Attachment[] = attachments.map(
165
+ ({ file, name }) => {
166
+ const lookupResult = mime.lookup(
167
+ file.extension ? file.extension : ''
168
+ );
169
+ return {
170
+ filename: name ?? file.filename,
171
+ content: file?.base64,
172
+ contentType: lookupResult ? lookupResult : undefined,
173
+ encoding: 'base64',
174
+ };
175
+ }
176
+ );
177
+
178
+ mailOptions.attachments = attachmentOption;
179
+ }
180
+
181
+ const mail: any = new MailComposer(mailOptions).compile();
182
+ mail.keepBcc = true;
183
+ const mailBody = await mail.build();
184
+
185
+ const encodedPayload = Buffer.from(mailBody)
186
+ .toString('base64')
187
+ .replace(/\+/g, '-')
188
+ .replace(/\//g, '_');
189
+
190
+ if (context.propsValue.draft) {
191
+ return await gmail.users.drafts.create({
192
+ userId: 'me',
193
+ requestBody: { message: { threadId, raw: encodedPayload } },
194
+ });
195
+ } else {
196
+ return await gmail.users.messages.send({
197
+ userId: 'me',
198
+ requestBody: {
199
+ threadId,
200
+ raw: encodedPayload,
201
+ },
202
+ });
203
+ }
204
+ },
205
+ });