@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,306 @@
|
|
|
1
|
+
import { 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
|
+
import { GmailProps } from '../common/props';
|
|
8
|
+
|
|
9
|
+
export const gmailCreateDraftReplyAction = createAction({
|
|
10
|
+
auth: gmailAuth,
|
|
11
|
+
name: 'create_draft_reply',
|
|
12
|
+
description: 'Creates a draft reply to an existing email.',
|
|
13
|
+
displayName: 'Create Draft Reply',
|
|
14
|
+
props: {
|
|
15
|
+
message_id: GmailProps.message,
|
|
16
|
+
reply_type: Property.StaticDropdown({
|
|
17
|
+
displayName: 'Reply Type',
|
|
18
|
+
description:
|
|
19
|
+
'Choose whether to reply to sender only or to all recipients',
|
|
20
|
+
required: true,
|
|
21
|
+
defaultValue: 'reply',
|
|
22
|
+
options: {
|
|
23
|
+
disabled: false,
|
|
24
|
+
options: [
|
|
25
|
+
{
|
|
26
|
+
label: 'Reply (to sender only)',
|
|
27
|
+
value: 'reply',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
label: 'Reply All (to all recipients)',
|
|
31
|
+
value: 'reply_all',
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
body_type: Property.StaticDropdown({
|
|
37
|
+
displayName: 'Body Type',
|
|
38
|
+
required: true,
|
|
39
|
+
defaultValue: 'plain_text',
|
|
40
|
+
options: {
|
|
41
|
+
disabled: false,
|
|
42
|
+
options: [
|
|
43
|
+
{
|
|
44
|
+
label: 'Plain text',
|
|
45
|
+
value: 'plain_text',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
label: 'HTML',
|
|
49
|
+
value: 'html',
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
body: Property.LongText({
|
|
55
|
+
displayName: 'Draft Reply Body',
|
|
56
|
+
description: 'Your draft reply message content',
|
|
57
|
+
required: false,
|
|
58
|
+
}),
|
|
59
|
+
include_original_message: Property.Checkbox({
|
|
60
|
+
displayName: 'Include Original Message',
|
|
61
|
+
description: 'Include the original message content in the draft reply',
|
|
62
|
+
required: true,
|
|
63
|
+
defaultValue: true,
|
|
64
|
+
}),
|
|
65
|
+
sender_name: Property.ShortText({
|
|
66
|
+
displayName: 'Sender Name',
|
|
67
|
+
description: 'Optional sender name to display',
|
|
68
|
+
required: false,
|
|
69
|
+
}),
|
|
70
|
+
attachment: Property.File({
|
|
71
|
+
displayName: 'Attachment',
|
|
72
|
+
description: 'Optional file to attach to your draft reply',
|
|
73
|
+
required: false,
|
|
74
|
+
}),
|
|
75
|
+
attachment_name: Property.ShortText({
|
|
76
|
+
displayName: 'Attachment Name',
|
|
77
|
+
description: 'Custom name for the attachment',
|
|
78
|
+
required: false,
|
|
79
|
+
}),
|
|
80
|
+
},
|
|
81
|
+
async run(context) {
|
|
82
|
+
const authClient = await createGoogleClient(context.auth);
|
|
83
|
+
|
|
84
|
+
const gmail = google.gmail({ version: 'v1', auth: authClient });
|
|
85
|
+
|
|
86
|
+
const originalMessage = await gmail.users.messages.get({
|
|
87
|
+
userId: 'me',
|
|
88
|
+
id: context.propsValue.message_id,
|
|
89
|
+
format: 'full',
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (!originalMessage.data || !originalMessage.data.payload) {
|
|
93
|
+
throw new Error('Could not fetch original message details');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const headers = originalMessage.data.payload.headers || [];
|
|
97
|
+
const headerMap = headers.reduce(
|
|
98
|
+
(acc: { [key: string]: string }, header) => {
|
|
99
|
+
if (header.name && header.value) {
|
|
100
|
+
acc[header.name.toLowerCase()] = header.value;
|
|
101
|
+
}
|
|
102
|
+
return acc;
|
|
103
|
+
},
|
|
104
|
+
{}
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const originalSubject = headerMap['subject'] || '';
|
|
108
|
+
const originalFrom = headerMap['from'] || '';
|
|
109
|
+
const originalTo = headerMap['to'] || '';
|
|
110
|
+
const originalCc = headerMap['cc'] || '';
|
|
111
|
+
const originalReplyTo = headerMap['reply-to'] || '';
|
|
112
|
+
const originalMessageId = headerMap['message-id'] || '';
|
|
113
|
+
const originalReferences = headerMap['references'] || '';
|
|
114
|
+
const originalDate = headerMap['date'] || '';
|
|
115
|
+
|
|
116
|
+
let originalMessageContent = '';
|
|
117
|
+
if (context.propsValue.include_original_message) {
|
|
118
|
+
try {
|
|
119
|
+
const { parseStream } = await import('../common/data');
|
|
120
|
+
|
|
121
|
+
const originalMessageFull = await gmail.users.messages.get({
|
|
122
|
+
userId: 'me',
|
|
123
|
+
id: context.propsValue.message_id,
|
|
124
|
+
format: 'raw',
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (originalMessageFull.data.raw) {
|
|
128
|
+
const rawMessage = Buffer.from(
|
|
129
|
+
originalMessageFull.data.raw,
|
|
130
|
+
'base64'
|
|
131
|
+
).toString('utf-8');
|
|
132
|
+
const parsedMessage = await parseStream(rawMessage);
|
|
133
|
+
|
|
134
|
+
let messageText = parsedMessage.text || '';
|
|
135
|
+
if (!messageText && parsedMessage.html) {
|
|
136
|
+
messageText = parsedMessage.html
|
|
137
|
+
.replace(/<[^>]*>/g, '')
|
|
138
|
+
.replace(/ /g, ' ')
|
|
139
|
+
.replace(/&/g, '&')
|
|
140
|
+
.replace(/</g, '<')
|
|
141
|
+
.replace(/>/g, '>');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (messageText) {
|
|
145
|
+
const quotedLines = messageText
|
|
146
|
+
.split('\n')
|
|
147
|
+
.map((line) => `> ${line.trim()}`);
|
|
148
|
+
const senderInfo = `On ${originalDate}, ${originalFrom} wrote:`;
|
|
149
|
+
originalMessageContent = `${senderInfo}\n${quotedLines.join('\n')}`;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.warn(
|
|
154
|
+
'Could not extract original message content for quoting:',
|
|
155
|
+
error
|
|
156
|
+
);
|
|
157
|
+
originalMessageContent = `On ${originalDate}, ${originalFrom} wrote:\n> [Original message content could not be parsed]`;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const toRecipients: string[] = [];
|
|
162
|
+
const ccRecipients: string[] = [];
|
|
163
|
+
|
|
164
|
+
if (context.propsValue.reply_type === 'reply_all') {
|
|
165
|
+
const senderEmail = originalReplyTo || originalFrom;
|
|
166
|
+
if (senderEmail) {
|
|
167
|
+
toRecipients.push(senderEmail);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const currentUserEmail = await getUserEmail(context.auth, authClient);
|
|
171
|
+
|
|
172
|
+
if (originalTo) {
|
|
173
|
+
const toEmails = originalTo.split(',').map((email) => email.trim());
|
|
174
|
+
toRecipients.push(
|
|
175
|
+
...toEmails.filter((email) => !email.includes(currentUserEmail || ''))
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (originalCc) {
|
|
180
|
+
const ccEmails = originalCc.split(',').map((email) => email.trim());
|
|
181
|
+
ccRecipients.push(
|
|
182
|
+
...ccEmails.filter((email) => !email.includes(currentUserEmail || ''))
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
const senderEmail = originalReplyTo || originalFrom;
|
|
187
|
+
if (senderEmail) {
|
|
188
|
+
toRecipients.push(senderEmail);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let replySubject = originalSubject;
|
|
193
|
+
if (!replySubject.toLowerCase().startsWith('re:')) {
|
|
194
|
+
replySubject = `Re: ${replySubject}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let referencesHeader = originalMessageId;
|
|
198
|
+
if (originalReferences) {
|
|
199
|
+
referencesHeader = `${originalReferences} ${originalMessageId}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const senderEmail = await getUserEmail(context.auth, authClient);
|
|
203
|
+
|
|
204
|
+
let draftBody = context.propsValue.body || '';
|
|
205
|
+
|
|
206
|
+
if (context.propsValue.include_original_message && originalMessageContent) {
|
|
207
|
+
const separator =
|
|
208
|
+
context.propsValue.body_type === 'html'
|
|
209
|
+
? '<br><br>--- Original Message ---<br>'
|
|
210
|
+
: '\n\n--- Original Message ---\n';
|
|
211
|
+
|
|
212
|
+
const quotedContent =
|
|
213
|
+
context.propsValue.body_type === 'html'
|
|
214
|
+
? originalMessageContent.replace(/\n/g, '<br>')
|
|
215
|
+
: originalMessageContent;
|
|
216
|
+
|
|
217
|
+
draftBody = draftBody
|
|
218
|
+
? `${draftBody}${separator}${quotedContent}`
|
|
219
|
+
: quotedContent;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const subjectBase64 = Buffer.from(replySubject).toString('base64');
|
|
223
|
+
const mailOptions: Mail.Options = {
|
|
224
|
+
to: toRecipients.join(', '),
|
|
225
|
+
cc: ccRecipients.length > 0 ? ccRecipients.join(', ') : undefined,
|
|
226
|
+
subject: `=?UTF-8?B?${subjectBase64}?=`,
|
|
227
|
+
text:
|
|
228
|
+
context.propsValue.body_type === 'plain_text' ? draftBody : undefined,
|
|
229
|
+
html: context.propsValue.body_type === 'html' ? draftBody : undefined,
|
|
230
|
+
attachments: [],
|
|
231
|
+
headers: [
|
|
232
|
+
{
|
|
233
|
+
key: 'In-Reply-To',
|
|
234
|
+
value: originalMessageId,
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
key: 'References',
|
|
238
|
+
value: referencesHeader,
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
if (senderEmail) {
|
|
244
|
+
mailOptions.from = context.propsValue.sender_name
|
|
245
|
+
? `${context.propsValue.sender_name} <${senderEmail}>`
|
|
246
|
+
: senderEmail;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (context.propsValue.attachment) {
|
|
250
|
+
const lookupResult = mime.lookup(
|
|
251
|
+
context.propsValue.attachment.extension || ''
|
|
252
|
+
);
|
|
253
|
+
const attachmentOption: Attachment[] = [
|
|
254
|
+
{
|
|
255
|
+
filename:
|
|
256
|
+
context.propsValue.attachment_name ??
|
|
257
|
+
context.propsValue.attachment.filename,
|
|
258
|
+
content: context.propsValue.attachment.base64,
|
|
259
|
+
contentType: lookupResult || undefined,
|
|
260
|
+
encoding: 'base64',
|
|
261
|
+
},
|
|
262
|
+
];
|
|
263
|
+
mailOptions.attachments = attachmentOption;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const mail: any = new MailComposer(mailOptions).compile();
|
|
267
|
+
mail.keepBcc = true;
|
|
268
|
+
const mailBody = await mail.build();
|
|
269
|
+
|
|
270
|
+
const encodedPayload = Buffer.from(mailBody)
|
|
271
|
+
.toString('base64')
|
|
272
|
+
.replace(/\+/g, '-')
|
|
273
|
+
.replace(/\//g, '_');
|
|
274
|
+
|
|
275
|
+
const draft = await gmail.users.drafts.create({
|
|
276
|
+
userId: 'me',
|
|
277
|
+
requestBody: {
|
|
278
|
+
message: {
|
|
279
|
+
threadId: originalMessage.data.threadId || undefined,
|
|
280
|
+
raw: encodedPayload,
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
...draft.data,
|
|
287
|
+
originalMessage: {
|
|
288
|
+
id: context.propsValue.message_id,
|
|
289
|
+
subject: originalSubject,
|
|
290
|
+
from: originalFrom,
|
|
291
|
+
to: originalTo,
|
|
292
|
+
date: originalDate,
|
|
293
|
+
threadId: originalMessage.data.threadId,
|
|
294
|
+
},
|
|
295
|
+
draftDetails: {
|
|
296
|
+
replyType: context.propsValue.reply_type,
|
|
297
|
+
recipients: {
|
|
298
|
+
to: toRecipients,
|
|
299
|
+
cc: ccRecipients,
|
|
300
|
+
},
|
|
301
|
+
subject: replySubject,
|
|
302
|
+
includeOriginal: context.propsValue.include_original_message,
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
},
|
|
306
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
|
|
6
|
+
export const gmailGetEmailAction = createAction({
|
|
7
|
+
auth: gmailAuth,
|
|
8
|
+
name: 'gmail_get_mail',
|
|
9
|
+
description: 'Get an email via Id.',
|
|
10
|
+
displayName: 'Get Email',
|
|
11
|
+
props: {
|
|
12
|
+
message_id: Property.ShortText({
|
|
13
|
+
displayName: 'Message ID',
|
|
14
|
+
description: 'The messageId of the mail to read.',
|
|
15
|
+
required: true,
|
|
16
|
+
}),
|
|
17
|
+
},
|
|
18
|
+
async run(context) {
|
|
19
|
+
const authClient = await createGoogleClient(context.auth);
|
|
20
|
+
|
|
21
|
+
const gmail = google.gmail({ version: 'v1', auth: authClient });
|
|
22
|
+
|
|
23
|
+
const rawMailResponse = await gmail.users.messages.get({
|
|
24
|
+
userId: 'me',
|
|
25
|
+
id: context.propsValue.message_id!,
|
|
26
|
+
format: 'raw',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const parsedMailResponse = await parseStream(
|
|
30
|
+
Buffer.from(rawMailResponse.data.raw as string, 'base64').toString(
|
|
31
|
+
'utf-8'
|
|
32
|
+
)
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
id: context.propsValue.message_id,
|
|
37
|
+
...parsedMailResponse,
|
|
38
|
+
attachments: await convertAttachment(
|
|
39
|
+
parsedMailResponse.attachments,
|
|
40
|
+
context.files
|
|
41
|
+
),
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createAction, Property } from '@guayaba/workflows-framework';
|
|
2
|
+
import { GmailRequests } from '../common/data';
|
|
3
|
+
import { GmailMessageFormat } from '../common/models';
|
|
4
|
+
import { gmailAuth, getAccessToken } from '../auth';
|
|
5
|
+
|
|
6
|
+
export const gmailGetThread = createAction({
|
|
7
|
+
auth: gmailAuth,
|
|
8
|
+
name: 'gmail_get_thread',
|
|
9
|
+
description: 'Get a thread from your Gmail account via Id',
|
|
10
|
+
displayName: 'Get Thread',
|
|
11
|
+
props: {
|
|
12
|
+
thread_id: Property.ShortText({
|
|
13
|
+
displayName: 'Thread ID',
|
|
14
|
+
description: 'The thread Id of the mail to read',
|
|
15
|
+
required: true,
|
|
16
|
+
}),
|
|
17
|
+
format: Property.StaticDropdown<GmailMessageFormat>({
|
|
18
|
+
displayName: 'Format',
|
|
19
|
+
description: 'Format of the mail',
|
|
20
|
+
required: false,
|
|
21
|
+
defaultValue: 'full',
|
|
22
|
+
options: {
|
|
23
|
+
disabled: false,
|
|
24
|
+
options: [
|
|
25
|
+
{ value: GmailMessageFormat.MINIMAL, label: 'Minimal' },
|
|
26
|
+
{ value: GmailMessageFormat.FULL, label: 'Full' },
|
|
27
|
+
{ value: GmailMessageFormat.RAW, label: 'Raw' },
|
|
28
|
+
{ value: GmailMessageFormat.METADATA, label: 'Metadata' },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
},
|
|
33
|
+
run: async ({ auth, propsValue: { format, thread_id } }) =>
|
|
34
|
+
await GmailRequests.getThread({
|
|
35
|
+
access_token: await getAccessToken(auth),
|
|
36
|
+
thread_id,
|
|
37
|
+
format: format ?? GmailMessageFormat.FULL,
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { 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
|
+
import { GmailProps } from '../common/props';
|
|
8
|
+
|
|
9
|
+
export const gmailReplyToEmailAction = createAction({
|
|
10
|
+
auth: gmailAuth,
|
|
11
|
+
name: 'reply_to_email',
|
|
12
|
+
displayName: 'Reply to Email',
|
|
13
|
+
description: 'Reply to an existing email.',
|
|
14
|
+
props: {
|
|
15
|
+
message_id: GmailProps.message,
|
|
16
|
+
reply_type: Property.StaticDropdown({
|
|
17
|
+
displayName: 'Reply Type',
|
|
18
|
+
description:
|
|
19
|
+
'Choose whether to reply to sender only or to all recipients',
|
|
20
|
+
required: true,
|
|
21
|
+
defaultValue: 'reply',
|
|
22
|
+
options: {
|
|
23
|
+
disabled: false,
|
|
24
|
+
options: [
|
|
25
|
+
{
|
|
26
|
+
label: 'Reply (to sender only)',
|
|
27
|
+
value: 'reply',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
label: 'Reply All (to all recipients)',
|
|
31
|
+
value: 'reply_all',
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
body_type: Property.StaticDropdown({
|
|
37
|
+
displayName: 'Body Type',
|
|
38
|
+
required: true,
|
|
39
|
+
defaultValue: 'plain_text',
|
|
40
|
+
options: {
|
|
41
|
+
disabled: false,
|
|
42
|
+
options: [
|
|
43
|
+
{
|
|
44
|
+
label: 'Plain text',
|
|
45
|
+
value: 'plain_text',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
label: 'HTML',
|
|
49
|
+
value: 'html',
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
body: Property.LongText({
|
|
55
|
+
displayName: 'Reply Body',
|
|
56
|
+
description: 'Your reply message content',
|
|
57
|
+
required: true,
|
|
58
|
+
}),
|
|
59
|
+
sender_name: Property.ShortText({
|
|
60
|
+
displayName: 'Sender Name',
|
|
61
|
+
description: 'Optional sender name to display',
|
|
62
|
+
required: false,
|
|
63
|
+
}),
|
|
64
|
+
attachment: Property.File({
|
|
65
|
+
displayName: 'Attachment',
|
|
66
|
+
description: 'Optional file to attach to your reply',
|
|
67
|
+
required: false,
|
|
68
|
+
}),
|
|
69
|
+
attachment_name: Property.ShortText({
|
|
70
|
+
displayName: 'Attachment Name',
|
|
71
|
+
description: 'Custom name for the attachment',
|
|
72
|
+
required: false,
|
|
73
|
+
}),
|
|
74
|
+
},
|
|
75
|
+
async run(context) {
|
|
76
|
+
const authClient = await createGoogleClient(context.auth);
|
|
77
|
+
|
|
78
|
+
const gmail = google.gmail({ version: 'v1', auth: authClient });
|
|
79
|
+
|
|
80
|
+
const originalMessage = await gmail.users.messages.get({
|
|
81
|
+
userId: 'me',
|
|
82
|
+
id: context.propsValue.message_id,
|
|
83
|
+
format: 'full',
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (!originalMessage.data || !originalMessage.data.payload) {
|
|
87
|
+
throw new Error('Could not fetch original message details');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const headers = originalMessage.data.payload.headers || [];
|
|
91
|
+
const headerMap = headers.reduce(
|
|
92
|
+
(acc: { [key: string]: string }, header) => {
|
|
93
|
+
if (header.name && header.value) {
|
|
94
|
+
acc[header.name.toLowerCase()] = header.value;
|
|
95
|
+
}
|
|
96
|
+
return acc;
|
|
97
|
+
},
|
|
98
|
+
{}
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const originalSubject = headerMap['subject'] || '';
|
|
102
|
+
const originalFrom = headerMap['from'] || '';
|
|
103
|
+
const originalTo = headerMap['to'] || '';
|
|
104
|
+
const originalCc = headerMap['cc'] || '';
|
|
105
|
+
const originalReplyTo = headerMap['reply-to'] || '';
|
|
106
|
+
const originalMessageId = headerMap['message-id'] || '';
|
|
107
|
+
const originalReferences = headerMap['references'] || '';
|
|
108
|
+
|
|
109
|
+
const toRecipients: string[] = [];
|
|
110
|
+
const ccRecipients: string[] = [];
|
|
111
|
+
|
|
112
|
+
if (context.propsValue.reply_type === 'reply_all') {
|
|
113
|
+
const senderEmail = originalReplyTo || originalFrom;
|
|
114
|
+
if (senderEmail) {
|
|
115
|
+
toRecipients.push(senderEmail);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const currentUserEmail = await getUserEmail(context.auth, authClient);
|
|
119
|
+
|
|
120
|
+
if (originalTo) {
|
|
121
|
+
const toEmails = originalTo.split(',').map((email) => email.trim());
|
|
122
|
+
toRecipients.push(
|
|
123
|
+
...toEmails.filter((email) => !email.includes(currentUserEmail || ''))
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (originalCc) {
|
|
128
|
+
const ccEmails = originalCc.split(',').map((email) => email.trim());
|
|
129
|
+
ccRecipients.push(
|
|
130
|
+
...ccEmails.filter((email) => !email.includes(currentUserEmail || ''))
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
const senderEmail = originalReplyTo || originalFrom;
|
|
135
|
+
if (senderEmail) {
|
|
136
|
+
toRecipients.push(senderEmail);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let replySubject = originalSubject;
|
|
141
|
+
if (!replySubject.toLowerCase().startsWith('re:')) {
|
|
142
|
+
replySubject = `Re: ${replySubject}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let referencesHeader = originalMessageId;
|
|
146
|
+
if (originalReferences) {
|
|
147
|
+
referencesHeader = `${originalReferences} ${originalMessageId}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const senderEmail = await getUserEmail(context.auth, authClient);
|
|
151
|
+
|
|
152
|
+
const subjectBase64 = Buffer.from(replySubject).toString('base64');
|
|
153
|
+
const mailOptions: Mail.Options = {
|
|
154
|
+
to: toRecipients.join(', '),
|
|
155
|
+
cc: ccRecipients.length > 0 ? ccRecipients.join(', ') : undefined,
|
|
156
|
+
subject: `=?UTF-8?B?${subjectBase64}?=`,
|
|
157
|
+
text:
|
|
158
|
+
context.propsValue.body_type === 'plain_text'
|
|
159
|
+
? context.propsValue.body
|
|
160
|
+
: undefined,
|
|
161
|
+
html:
|
|
162
|
+
context.propsValue.body_type === 'html'
|
|
163
|
+
? context.propsValue.body
|
|
164
|
+
: undefined,
|
|
165
|
+
attachments: [],
|
|
166
|
+
headers: [
|
|
167
|
+
{
|
|
168
|
+
key: 'In-Reply-To',
|
|
169
|
+
value: originalMessageId,
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
key: 'References',
|
|
173
|
+
value: referencesHeader,
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
if (senderEmail) {
|
|
179
|
+
mailOptions.from = context.propsValue.sender_name
|
|
180
|
+
? `${context.propsValue.sender_name} <${senderEmail}>`
|
|
181
|
+
: senderEmail;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (context.propsValue.attachment) {
|
|
185
|
+
const lookupResult = mime.lookup(
|
|
186
|
+
context.propsValue.attachment.extension || ''
|
|
187
|
+
);
|
|
188
|
+
const attachmentOption: Attachment[] = [
|
|
189
|
+
{
|
|
190
|
+
filename:
|
|
191
|
+
context.propsValue.attachment_name ??
|
|
192
|
+
context.propsValue.attachment.filename,
|
|
193
|
+
content: context.propsValue.attachment.base64,
|
|
194
|
+
contentType: lookupResult || undefined,
|
|
195
|
+
encoding: 'base64',
|
|
196
|
+
},
|
|
197
|
+
];
|
|
198
|
+
mailOptions.attachments = attachmentOption;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const mail: any = new MailComposer(mailOptions).compile();
|
|
202
|
+
mail.keepBcc = true;
|
|
203
|
+
const mailBody = await mail.build();
|
|
204
|
+
|
|
205
|
+
const encodedPayload = Buffer.from(mailBody)
|
|
206
|
+
.toString('base64')
|
|
207
|
+
.replace(/\+/g, '-')
|
|
208
|
+
.replace(/\//g, '_');
|
|
209
|
+
|
|
210
|
+
const response = await gmail.users.messages.send({
|
|
211
|
+
userId: 'me',
|
|
212
|
+
requestBody: {
|
|
213
|
+
threadId: originalMessage.data.threadId || undefined,
|
|
214
|
+
raw: encodedPayload,
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
return response.data;
|
|
219
|
+
},
|
|
220
|
+
});
|