@divyacon/piece-gmail 0.0.0-stage → 0.1.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.
- package/LICENSE.MIT-AP +24 -0
- package/README.md +8 -2
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +111 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/actions/ai-get-message-action.d.ts +7 -0
- package/dist/lib/actions/ai-get-message-action.d.ts.map +1 -0
- package/dist/lib/actions/ai-get-message-action.js +24 -0
- package/dist/lib/actions/ai-get-message-action.js.map +1 -0
- package/dist/lib/actions/ai-reply-to-thread-action.d.ts +16 -0
- package/dist/lib/actions/ai-reply-to-thread-action.d.ts.map +1 -0
- package/dist/lib/actions/ai-reply-to-thread-action.js +24 -0
- package/dist/lib/actions/ai-reply-to-thread-action.js.map +1 -0
- package/dist/lib/actions/ai-search-email-action.d.ts +21 -0
- package/dist/lib/actions/ai-search-email-action.d.ts.map +1 -0
- package/dist/lib/actions/ai-search-email-action.js +24 -0
- package/dist/lib/actions/ai-search-email-action.js.map +1 -0
- package/dist/lib/actions/ai-send-email-action.d.ts +18 -0
- package/dist/lib/actions/ai-send-email-action.d.ts.map +1 -0
- package/dist/lib/actions/ai-send-email-action.js +24 -0
- package/dist/lib/actions/ai-send-email-action.js.map +1 -0
- package/dist/lib/actions/create-draft-action.d.ts +16 -0
- package/dist/lib/actions/create-draft-action.d.ts.map +1 -0
- package/dist/lib/actions/create-draft-action.js +137 -0
- package/dist/lib/actions/create-draft-action.js.map +1 -0
- package/dist/lib/actions/create-draft-reply-action.d.ts +17 -0
- package/dist/lib/actions/create-draft-reply-action.d.ts.map +1 -0
- package/dist/lib/actions/create-draft-reply-action.js +269 -0
- package/dist/lib/actions/create-draft-reply-action.js.map +1 -0
- package/dist/lib/actions/delete-draft-action.d.ts +7 -0
- package/dist/lib/actions/delete-draft-action.d.ts.map +1 -0
- package/dist/lib/actions/delete-draft-action.js +62 -0
- package/dist/lib/actions/delete-draft-action.js.map +1 -0
- package/dist/lib/actions/forward-message-action.d.ts +12 -0
- package/dist/lib/actions/forward-message-action.d.ts.map +1 -0
- package/dist/lib/actions/forward-message-action.js +139 -0
- package/dist/lib/actions/forward-message-action.js.map +1 -0
- package/dist/lib/actions/get-attachment-action.d.ts +9 -0
- package/dist/lib/actions/get-attachment-action.d.ts.map +1 -0
- package/dist/lib/actions/get-attachment-action.js +80 -0
- package/dist/lib/actions/get-attachment-action.js.map +1 -0
- package/dist/lib/actions/get-draft-action.d.ts +7 -0
- package/dist/lib/actions/get-draft-action.d.ts.map +1 -0
- package/dist/lib/actions/get-draft-action.js +56 -0
- package/dist/lib/actions/get-draft-action.js.map +1 -0
- package/dist/lib/actions/get-label-action.d.ts +7 -0
- package/dist/lib/actions/get-label-action.d.ts.map +1 -0
- package/dist/lib/actions/get-label-action.js +55 -0
- package/dist/lib/actions/get-label-action.js.map +1 -0
- package/dist/lib/actions/get-mail-action.d.ts +7 -0
- package/dist/lib/actions/get-mail-action.d.ts.map +1 -0
- package/dist/lib/actions/get-mail-action.js +44 -0
- package/dist/lib/actions/get-mail-action.js.map +1 -0
- package/dist/lib/actions/get-profile-action.d.ts +5 -0
- package/dist/lib/actions/get-profile-action.d.ts.map +1 -0
- package/dist/lib/actions/get-profile-action.js +43 -0
- package/dist/lib/actions/get-profile-action.js.map +1 -0
- package/dist/lib/actions/get-thread-action.d.ts +9 -0
- package/dist/lib/actions/get-thread-action.d.ts.map +1 -0
- package/dist/lib/actions/get-thread-action.js +53 -0
- package/dist/lib/actions/get-thread-action.js.map +1 -0
- package/dist/lib/actions/list-drafts-action.d.ts +8 -0
- package/dist/lib/actions/list-drafts-action.d.ts.map +1 -0
- package/dist/lib/actions/list-drafts-action.js +62 -0
- package/dist/lib/actions/list-drafts-action.js.map +1 -0
- package/dist/lib/actions/list-history-action.d.ts +9 -0
- package/dist/lib/actions/list-history-action.d.ts.map +1 -0
- package/dist/lib/actions/list-history-action.js +74 -0
- package/dist/lib/actions/list-history-action.js.map +1 -0
- package/dist/lib/actions/list-labels-action.d.ts +5 -0
- package/dist/lib/actions/list-labels-action.d.ts.map +1 -0
- package/dist/lib/actions/list-labels-action.js +47 -0
- package/dist/lib/actions/list-labels-action.js.map +1 -0
- package/dist/lib/actions/list-threads-action.d.ts +10 -0
- package/dist/lib/actions/list-threads-action.d.ts.map +1 -0
- package/dist/lib/actions/list-threads-action.js +74 -0
- package/dist/lib/actions/list-threads-action.js.map +1 -0
- package/dist/lib/actions/reply-to-email-action.d.ts +16 -0
- package/dist/lib/actions/reply-to-email-action.d.ts.map +1 -0
- package/dist/lib/actions/reply-to-email-action.js +202 -0
- package/dist/lib/actions/reply-to-email-action.js.map +1 -0
- package/dist/lib/actions/request-approval-in-email.d.ts +15 -0
- package/dist/lib/actions/request-approval-in-email.d.ts.map +1 -0
- package/dist/lib/actions/request-approval-in-email.js +174 -0
- package/dist/lib/actions/request-approval-in-email.js.map +1 -0
- package/dist/lib/actions/search-email-action.d.ts +22 -0
- package/dist/lib/actions/search-email-action.d.ts.map +1 -0
- package/dist/lib/actions/search-email-action.js +179 -0
- package/dist/lib/actions/search-email-action.js.map +1 -0
- package/dist/lib/actions/send-draft-action.d.ts +7 -0
- package/dist/lib/actions/send-draft-action.d.ts.map +1 -0
- package/dist/lib/actions/send-draft-action.js +57 -0
- package/dist/lib/actions/send-draft-action.js.map +1 -0
- package/dist/lib/actions/send-email-action.d.ts +18 -0
- package/dist/lib/actions/send-email-action.d.ts.map +1 -0
- package/dist/lib/actions/send-email-action.js +196 -0
- package/dist/lib/actions/send-email-action.js.map +1 -0
- package/dist/lib/actions/stop-watch-action.d.ts +5 -0
- package/dist/lib/actions/stop-watch-action.d.ts.map +1 -0
- package/dist/lib/actions/stop-watch-action.js +41 -0
- package/dist/lib/actions/stop-watch-action.js.map +1 -0
- package/dist/lib/actions/update-draft-action.d.ts +17 -0
- package/dist/lib/actions/update-draft-action.d.ts.map +1 -0
- package/dist/lib/actions/update-draft-action.js +193 -0
- package/dist/lib/actions/update-draft-action.js.map +1 -0
- package/dist/lib/auth.d.ts +12 -0
- package/dist/lib/auth.d.ts.map +1 -0
- package/dist/lib/auth.js +108 -0
- package/dist/lib/auth.js.map +1 -0
- package/dist/lib/common/data.d.ts +132 -0
- package/dist/lib/common/data.d.ts.map +1 -0
- package/dist/lib/common/data.js +209 -0
- package/dist/lib/common/data.js.map +1 -0
- package/dist/lib/common/mime.d.ts +29 -0
- package/dist/lib/common/mime.d.ts.map +1 -0
- package/dist/lib/common/mime.js +59 -0
- package/dist/lib/common/mime.js.map +1 -0
- package/dist/lib/common/models.d.ts +86 -0
- package/dist/lib/common/models.d.ts.map +1 -0
- package/dist/lib/common/models.js +26 -0
- package/dist/lib/common/models.js.map +1 -0
- package/dist/lib/common/props.d.ts +28 -0
- package/dist/lib/common/props.d.ts.map +1 -0
- package/dist/lib/common/props.js +231 -0
- package/dist/lib/common/props.js.map +1 -0
- package/dist/lib/output-schemas.d.ts +29 -0
- package/dist/lib/output-schemas.d.ts.map +1 -0
- package/dist/lib/output-schemas.js +1029 -0
- package/dist/lib/output-schemas.js.map +1 -0
- package/dist/lib/triggers/new-attachment.d.ts +144 -0
- package/dist/lib/triggers/new-attachment.d.ts.map +1 -0
- package/dist/lib/triggers/new-attachment.js +150 -0
- package/dist/lib/triggers/new-attachment.js.map +1 -0
- package/dist/lib/triggers/new-conversation.d.ts +59 -0
- package/dist/lib/triggers/new-conversation.d.ts.map +1 -0
- package/dist/lib/triggers/new-conversation.js +308 -0
- package/dist/lib/triggers/new-conversation.js.map +1 -0
- package/dist/lib/triggers/new-email.d.ts +52 -0
- package/dist/lib/triggers/new-email.d.ts.map +1 -0
- package/dist/lib/triggers/new-email.js +135 -0
- package/dist/lib/triggers/new-email.js.map +1 -0
- package/dist/lib/triggers/new-label.d.ts +15 -0
- package/dist/lib/triggers/new-label.d.ts.map +1 -0
- package/dist/lib/triggers/new-label.js +82 -0
- package/dist/lib/triggers/new-label.js.map +1 -0
- package/dist/lib/triggers/new-labeled-email.d.ts +35 -0
- package/dist/lib/triggers/new-labeled-email.d.ts.map +1 -0
- package/dist/lib/triggers/new-labeled-email.js +147 -0
- package/dist/lib/triggers/new-labeled-email.js.map +1 -0
- package/package.json +53 -3
- package/src/index.ts +107 -0
- package/src/lib/actions/ai-get-message-action.ts +20 -0
- package/src/lib/actions/ai-reply-to-thread-action.ts +20 -0
- package/src/lib/actions/ai-search-email-action.ts +21 -0
- package/src/lib/actions/ai-send-email-action.ts +20 -0
- package/src/lib/actions/create-draft-action.ts +148 -0
- package/src/lib/actions/create-draft-reply-action.ts +314 -0
- package/src/lib/actions/delete-draft-action.ts +57 -0
- package/src/lib/actions/forward-message-action.ts +159 -0
- package/src/lib/actions/get-attachment-action.ts +83 -0
- package/src/lib/actions/get-draft-action.ts +53 -0
- package/src/lib/actions/get-label-action.ts +53 -0
- package/src/lib/actions/get-mail-action.ts +52 -0
- package/src/lib/actions/get-profile-action.ts +39 -0
- package/src/lib/actions/get-thread-action.ts +47 -0
- package/src/lib/actions/list-drafts-action.ts +72 -0
- package/src/lib/actions/list-history-action.ts +83 -0
- package/src/lib/actions/list-labels-action.ts +43 -0
- package/src/lib/actions/list-threads-action.ts +89 -0
- package/src/lib/actions/reply-to-email-action.ts +228 -0
- package/src/lib/actions/request-approval-in-email.ts +197 -0
- package/src/lib/actions/search-email-action.ts +219 -0
- package/src/lib/actions/send-draft-action.ts +54 -0
- package/src/lib/actions/send-email-action.ts +213 -0
- package/src/lib/actions/stop-watch-action.ts +37 -0
- package/src/lib/actions/update-draft-action.ts +211 -0
- package/src/lib/auth.ts +116 -0
- package/src/lib/common/data.ts +268 -0
- package/src/lib/common/mime.ts +82 -0
- package/src/lib/common/models.ts +91 -0
- package/src/lib/common/props.ts +262 -0
- package/src/lib/output-schemas.ts +1061 -0
- package/src/lib/triggers/new-attachment.ts +203 -0
- package/src/lib/triggers/new-conversation.ts +413 -0
- package/src/lib/triggers/new-email.ts +173 -0
- package/src/lib/triggers/new-label.ts +83 -0
- package/src/lib/triggers/new-labeled-email.ts +197 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { ApFile, createAction, Property } from '@agnocon/pieces-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 { gmail as googleGmail } from '@googleapis/gmail';
|
|
7
|
+
import { sendEmailActionOutputSchema } from '../output-schemas';
|
|
8
|
+
|
|
9
|
+
export const gmailSendEmailAction = createAction({
|
|
10
|
+
auth: gmailAuth,
|
|
11
|
+
name: 'send_email',
|
|
12
|
+
description: 'Send an email through a Gmail account',
|
|
13
|
+
audience: 'human',
|
|
14
|
+
aiMetadata: {
|
|
15
|
+
description:
|
|
16
|
+
'Composes and sends a new email from the connected Gmail account to one or more recipients, with optional CC/BCC, attachments, and plain-text or HTML body. Use this to originate a fresh message; to answer an existing thread prefer Reply to Email instead, or pass an original Message-ID to send this message into that existing thread. Set the draft flag to save it as a draft instead of sending. Not idempotent: each call sends (or drafts) a separate message.',
|
|
17
|
+
idempotent: false,
|
|
18
|
+
},
|
|
19
|
+
displayName: 'Send Email',
|
|
20
|
+
props: {
|
|
21
|
+
receiver: Property.Array({
|
|
22
|
+
displayName: 'Receiver Email (To)',
|
|
23
|
+
description: undefined,
|
|
24
|
+
required: true,
|
|
25
|
+
}),
|
|
26
|
+
cc: Property.Array({
|
|
27
|
+
displayName: 'CC Email',
|
|
28
|
+
description: undefined,
|
|
29
|
+
required: false,
|
|
30
|
+
}),
|
|
31
|
+
bcc: Property.Array({
|
|
32
|
+
displayName: 'BCC Email',
|
|
33
|
+
description: undefined,
|
|
34
|
+
required: false,
|
|
35
|
+
}),
|
|
36
|
+
subject: Property.ShortText({
|
|
37
|
+
displayName: 'Subject',
|
|
38
|
+
description: undefined,
|
|
39
|
+
required: true,
|
|
40
|
+
}),
|
|
41
|
+
body_type: Property.StaticDropdown({
|
|
42
|
+
displayName: 'Body Type',
|
|
43
|
+
required: true,
|
|
44
|
+
defaultValue: 'plain_text',
|
|
45
|
+
options: {
|
|
46
|
+
disabled: false,
|
|
47
|
+
options: [
|
|
48
|
+
{
|
|
49
|
+
label: 'plain text',
|
|
50
|
+
value: 'plain_text',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
label: 'html',
|
|
54
|
+
value: 'html',
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
59
|
+
body: Property.ShortText({
|
|
60
|
+
displayName: 'Body',
|
|
61
|
+
description: 'Body for the email you want to send',
|
|
62
|
+
required: true,
|
|
63
|
+
}),
|
|
64
|
+
reply_to: Property.Array({
|
|
65
|
+
displayName: 'Reply-To Email',
|
|
66
|
+
description: 'Email address to set as the "Reply-To" header',
|
|
67
|
+
required: false,
|
|
68
|
+
}),
|
|
69
|
+
sender_name: Property.ShortText({
|
|
70
|
+
displayName: 'Sender Name',
|
|
71
|
+
required: false,
|
|
72
|
+
}),
|
|
73
|
+
from: Property.ShortText({
|
|
74
|
+
displayName: 'Sender Email',
|
|
75
|
+
description:
|
|
76
|
+
"The address must be listed in your GMail account's settings",
|
|
77
|
+
required: false,
|
|
78
|
+
}),
|
|
79
|
+
attachments: Property.Array({
|
|
80
|
+
displayName: 'Attachments',
|
|
81
|
+
required: false,
|
|
82
|
+
properties: {
|
|
83
|
+
file: Property.File({
|
|
84
|
+
displayName: 'File',
|
|
85
|
+
description: 'File to attach to the email you want to send.',
|
|
86
|
+
required: true,
|
|
87
|
+
}),
|
|
88
|
+
name: Property.ShortText({
|
|
89
|
+
displayName: 'Attachment Name',
|
|
90
|
+
description: 'In case you want to change the name of the attachment.',
|
|
91
|
+
required: false,
|
|
92
|
+
}),
|
|
93
|
+
},
|
|
94
|
+
}),
|
|
95
|
+
in_reply_to: Property.ShortText({
|
|
96
|
+
displayName: 'In reply to',
|
|
97
|
+
description: 'Reply to this Message-ID',
|
|
98
|
+
required: false,
|
|
99
|
+
}),
|
|
100
|
+
draft: Property.Checkbox({
|
|
101
|
+
displayName: 'Create draft',
|
|
102
|
+
description: 'Create draft without sending the actual email',
|
|
103
|
+
required: true,
|
|
104
|
+
defaultValue: false,
|
|
105
|
+
}),
|
|
106
|
+
},
|
|
107
|
+
outputSchema: sendEmailActionOutputSchema,
|
|
108
|
+
async run(context) {
|
|
109
|
+
const authClient = await createGoogleClient(context.auth);
|
|
110
|
+
|
|
111
|
+
const gmail = googleGmail({ version: 'v1', auth: authClient });
|
|
112
|
+
|
|
113
|
+
const subjectBase64 = Buffer.from(context.propsValue['subject']).toString(
|
|
114
|
+
'base64'
|
|
115
|
+
);
|
|
116
|
+
const attachments = context.propsValue.attachments as {
|
|
117
|
+
file: ApFile;
|
|
118
|
+
name: string | undefined;
|
|
119
|
+
}[];
|
|
120
|
+
const replyTo = context.propsValue['reply_to']?.filter(
|
|
121
|
+
(email) => email !== ''
|
|
122
|
+
);
|
|
123
|
+
const receiver = context.propsValue['receiver']?.filter(
|
|
124
|
+
(email) => email !== ''
|
|
125
|
+
);
|
|
126
|
+
const cc = context.propsValue['cc']?.filter((email) => email !== '');
|
|
127
|
+
const bcc = context.propsValue['bcc']?.filter((email) => email !== '');
|
|
128
|
+
const mailOptions: Mail.Options = {
|
|
129
|
+
to: receiver.join(', '), // Join all email addresses with a comma
|
|
130
|
+
cc: cc ? cc.join(', ') : undefined,
|
|
131
|
+
bcc: bcc ? bcc.join(', ') : undefined,
|
|
132
|
+
subject: `=?UTF-8?B?${subjectBase64}?=`,
|
|
133
|
+
replyTo: replyTo ? replyTo.join(', ') : '',
|
|
134
|
+
text:
|
|
135
|
+
context.propsValue.body_type === 'plain_text'
|
|
136
|
+
? context.propsValue['body']
|
|
137
|
+
: undefined,
|
|
138
|
+
html:
|
|
139
|
+
context.propsValue.body_type === 'html'
|
|
140
|
+
? context.propsValue['body']
|
|
141
|
+
: undefined,
|
|
142
|
+
attachments: [],
|
|
143
|
+
};
|
|
144
|
+
let threadId = undefined;
|
|
145
|
+
if (context.propsValue.in_reply_to) {
|
|
146
|
+
mailOptions.headers = [
|
|
147
|
+
{
|
|
148
|
+
key: 'References',
|
|
149
|
+
value: context.propsValue.in_reply_to,
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
key: 'In-Reply-To',
|
|
153
|
+
value: context.propsValue.in_reply_to,
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
const messages = await gmail.users.messages.list({
|
|
157
|
+
userId: 'me',
|
|
158
|
+
q: `Rfc822msgid:${context.propsValue.in_reply_to}`,
|
|
159
|
+
});
|
|
160
|
+
threadId = messages.data.messages?.[0].threadId;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const senderEmail =
|
|
164
|
+
context.propsValue.from || (await getUserEmail(context.auth, authClient));
|
|
165
|
+
if (senderEmail) {
|
|
166
|
+
mailOptions.from = context.propsValue.sender_name
|
|
167
|
+
? `${context.propsValue['sender_name']} <${senderEmail}>`
|
|
168
|
+
: senderEmail;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (attachments && attachments.length > 0) {
|
|
172
|
+
const attachmentOption: Attachment[] = attachments.map(
|
|
173
|
+
({ file, name }) => {
|
|
174
|
+
const lookupResult = mime.lookup(
|
|
175
|
+
file.extension ? file.extension : ''
|
|
176
|
+
);
|
|
177
|
+
return {
|
|
178
|
+
filename: name ?? file.filename,
|
|
179
|
+
content: file?.base64,
|
|
180
|
+
contentType: lookupResult ? lookupResult : undefined,
|
|
181
|
+
encoding: 'base64',
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
mailOptions.attachments = attachmentOption;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const mail: any = new MailComposer(mailOptions).compile();
|
|
190
|
+
mail.keepBcc = true;
|
|
191
|
+
const mailBody = await mail.build();
|
|
192
|
+
|
|
193
|
+
const encodedPayload = Buffer.from(mailBody)
|
|
194
|
+
.toString('base64')
|
|
195
|
+
.replace(/\+/g, '-')
|
|
196
|
+
.replace(/\//g, '_');
|
|
197
|
+
|
|
198
|
+
if (context.propsValue.draft) {
|
|
199
|
+
return await gmail.users.drafts.create({
|
|
200
|
+
userId: 'me',
|
|
201
|
+
requestBody: { message: { threadId, raw: encodedPayload } },
|
|
202
|
+
});
|
|
203
|
+
} else {
|
|
204
|
+
return await gmail.users.messages.send({
|
|
205
|
+
userId: 'me',
|
|
206
|
+
requestBody: {
|
|
207
|
+
threadId,
|
|
208
|
+
raw: encodedPayload,
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createAction } from '@agnocon/pieces-framework';
|
|
2
|
+
import { gmailAuth, createGoogleClient } from '../auth';
|
|
3
|
+
import { gmail as googleGmail } from '@googleapis/gmail';
|
|
4
|
+
|
|
5
|
+
export const gmailStopWatchAction = createAction({
|
|
6
|
+
auth: gmailAuth,
|
|
7
|
+
name: 'gmail_stop_watch',
|
|
8
|
+
displayName: 'Stop Watch',
|
|
9
|
+
description: 'Stop push notifications on the mailbox.',
|
|
10
|
+
audience: 'ai',
|
|
11
|
+
aiMetadata: {
|
|
12
|
+
description:
|
|
13
|
+
'Stops any active push (Cloud Pub/Sub) notifications for the connected mailbox. Use this to cancel a previously started watch. Takes no parameters. Idempotent: stopping when no watch is active is a no-op.',
|
|
14
|
+
idempotent: true,
|
|
15
|
+
},
|
|
16
|
+
props: {},
|
|
17
|
+
async run(context) {
|
|
18
|
+
const authClient = await createGoogleClient(context.auth);
|
|
19
|
+
const gmail = googleGmail({ version: 'v1', auth: authClient });
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await gmail.users.stop({ userId: 'me' });
|
|
23
|
+
return { success: true };
|
|
24
|
+
} catch (error: any) {
|
|
25
|
+
if (error.code === 403) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
'Insufficient permissions to stop the watch. Ensure a read scope (e.g. gmail.readonly) is granted.'
|
|
28
|
+
);
|
|
29
|
+
} else if (error.code === 429) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
'Gmail API rate limit exceeded. Please try again later.'
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`Failed to stop watch: ${error.message}`);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
});
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { ApFile, createAction, Property } from '@agnocon/pieces-framework';
|
|
2
|
+
import { gmailAuth, createGoogleClient, getUserEmail } from '../auth';
|
|
3
|
+
import { gmail as googleGmail } from '@googleapis/gmail';
|
|
4
|
+
import { GmailMime, RawMimeAttachment } from '../common/mime';
|
|
5
|
+
import { gmailUpdateDraftActionOutputSchema } from '../output-schemas';
|
|
6
|
+
import { parseStream } from '../common/data';
|
|
7
|
+
import { AddressObject } from 'mailparser';
|
|
8
|
+
|
|
9
|
+
export const gmailUpdateDraftAction = createAction({
|
|
10
|
+
auth: gmailAuth,
|
|
11
|
+
name: 'gmail_update_draft',
|
|
12
|
+
displayName: 'Update Draft',
|
|
13
|
+
description: 'Replace the content of an existing draft email.',
|
|
14
|
+
audience: 'ai',
|
|
15
|
+
aiMetadata: {
|
|
16
|
+
description:
|
|
17
|
+
'Updates an existing draft (subject, body, recipients, attachments) by its draft ID. Subject and body are required and replace the previous values; omitted recipients (To/CC/BCC), attachments, and the thread association are preserved from the existing draft rather than cleared. Obtain the draft ID from List Drafts. Not idempotent in effect: each call rewrites the draft with the supplied content.',
|
|
18
|
+
idempotent: false,
|
|
19
|
+
},
|
|
20
|
+
props: {
|
|
21
|
+
draft_id: Property.ShortText({
|
|
22
|
+
displayName: 'Draft ID',
|
|
23
|
+
description: 'The ID of the draft to update (obtain from List Drafts).',
|
|
24
|
+
required: true,
|
|
25
|
+
}),
|
|
26
|
+
receiver: Property.Array({
|
|
27
|
+
displayName: 'Receiver Email (To)',
|
|
28
|
+
required: false,
|
|
29
|
+
}),
|
|
30
|
+
cc: Property.Array({
|
|
31
|
+
displayName: 'CC Email',
|
|
32
|
+
required: false,
|
|
33
|
+
}),
|
|
34
|
+
bcc: Property.Array({
|
|
35
|
+
displayName: 'BCC Email',
|
|
36
|
+
required: false,
|
|
37
|
+
}),
|
|
38
|
+
subject: Property.ShortText({
|
|
39
|
+
displayName: 'Subject',
|
|
40
|
+
required: true,
|
|
41
|
+
}),
|
|
42
|
+
body_type: Property.StaticDropdown({
|
|
43
|
+
displayName: 'Body Type',
|
|
44
|
+
required: true,
|
|
45
|
+
defaultValue: 'plain_text',
|
|
46
|
+
options: {
|
|
47
|
+
disabled: false,
|
|
48
|
+
options: [
|
|
49
|
+
{ label: 'plain text', value: 'plain_text' },
|
|
50
|
+
{ label: 'html', value: 'html' },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
body: Property.LongText({
|
|
55
|
+
displayName: 'Body',
|
|
56
|
+
description: 'New body for the draft email.',
|
|
57
|
+
required: true,
|
|
58
|
+
}),
|
|
59
|
+
sender_name: Property.ShortText({
|
|
60
|
+
displayName: 'Sender Name',
|
|
61
|
+
required: false,
|
|
62
|
+
}),
|
|
63
|
+
from: Property.ShortText({
|
|
64
|
+
displayName: 'Sender Email',
|
|
65
|
+
description:
|
|
66
|
+
"The address must be listed in your Gmail account's settings.",
|
|
67
|
+
required: false,
|
|
68
|
+
}),
|
|
69
|
+
thread_id: Property.ShortText({
|
|
70
|
+
displayName: 'Thread ID',
|
|
71
|
+
description:
|
|
72
|
+
'Optional thread ID to keep this draft attached to a conversation (obtain from Search Email or Get Thread).',
|
|
73
|
+
required: false,
|
|
74
|
+
}),
|
|
75
|
+
attachments: Property.Array({
|
|
76
|
+
displayName: 'Attachments',
|
|
77
|
+
required: false,
|
|
78
|
+
properties: {
|
|
79
|
+
file: Property.File({
|
|
80
|
+
displayName: 'File',
|
|
81
|
+
description: 'File to attach to the draft.',
|
|
82
|
+
required: true,
|
|
83
|
+
}),
|
|
84
|
+
name: Property.ShortText({
|
|
85
|
+
displayName: 'Attachment Name',
|
|
86
|
+
description: 'Optional name to override the attachment filename.',
|
|
87
|
+
required: false,
|
|
88
|
+
}),
|
|
89
|
+
},
|
|
90
|
+
}),
|
|
91
|
+
},
|
|
92
|
+
outputSchema: gmailUpdateDraftActionOutputSchema,
|
|
93
|
+
async run(context) {
|
|
94
|
+
const authClient = await createGoogleClient(context.auth);
|
|
95
|
+
const gmail = googleGmail({ version: 'v1', auth: authClient });
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
let receiver = (
|
|
99
|
+
context.propsValue.receiver as string[] | undefined
|
|
100
|
+
)?.filter((email) => email !== '');
|
|
101
|
+
let cc = (context.propsValue.cc as string[] | undefined)?.filter(
|
|
102
|
+
(email) => email !== ''
|
|
103
|
+
);
|
|
104
|
+
let bcc = (context.propsValue.bcc as string[] | undefined)?.filter(
|
|
105
|
+
(email) => email !== ''
|
|
106
|
+
);
|
|
107
|
+
let threadId = context.propsValue.thread_id || undefined;
|
|
108
|
+
const newAttachments = context.propsValue.attachments as
|
|
109
|
+
| { file: ApFile; name?: string }[]
|
|
110
|
+
| undefined;
|
|
111
|
+
let preservedAttachments: RawMimeAttachment[] = [];
|
|
112
|
+
|
|
113
|
+
// A draft update replaces the whole message, so recover the recipients,
|
|
114
|
+
// attachments, and thread link from the existing draft when the caller
|
|
115
|
+
// omits them — otherwise editing only the body would silently strip the
|
|
116
|
+
// BCC list or attachments and detach the draft from its conversation.
|
|
117
|
+
// BCC is stripped from `metadata`/`full` responses, so recovering it
|
|
118
|
+
// (and the attachment content) requires the raw MIME of the draft.
|
|
119
|
+
if (
|
|
120
|
+
!receiver?.length ||
|
|
121
|
+
!cc?.length ||
|
|
122
|
+
!bcc?.length ||
|
|
123
|
+
!threadId ||
|
|
124
|
+
!newAttachments?.length
|
|
125
|
+
) {
|
|
126
|
+
const existing = await gmail.users.drafts.get({
|
|
127
|
+
userId: 'me',
|
|
128
|
+
id: context.propsValue.draft_id,
|
|
129
|
+
format: 'raw',
|
|
130
|
+
});
|
|
131
|
+
const message = existing.data.message;
|
|
132
|
+
threadId = threadId ?? message?.threadId ?? undefined;
|
|
133
|
+
|
|
134
|
+
if (message?.raw) {
|
|
135
|
+
const parsed = await parseStream(
|
|
136
|
+
Buffer.from(message.raw, 'base64').toString('utf-8')
|
|
137
|
+
);
|
|
138
|
+
if (!receiver?.length) receiver = addressesOf(parsed.to);
|
|
139
|
+
if (!cc?.length) cc = addressesOf(parsed.cc);
|
|
140
|
+
if (!bcc?.length) bcc = addressesOf(parsed.bcc);
|
|
141
|
+
if (!newAttachments?.length) {
|
|
142
|
+
preservedAttachments = parsed.attachments.map((attachment) => ({
|
|
143
|
+
filename: attachment.filename,
|
|
144
|
+
content: attachment.content,
|
|
145
|
+
contentType: attachment.contentType,
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const senderEmail =
|
|
152
|
+
context.propsValue.from ||
|
|
153
|
+
(await getUserEmail(context.auth, authClient));
|
|
154
|
+
const from = senderEmail
|
|
155
|
+
? context.propsValue.sender_name
|
|
156
|
+
? `${context.propsValue.sender_name} <${senderEmail}>`
|
|
157
|
+
: senderEmail
|
|
158
|
+
: undefined;
|
|
159
|
+
|
|
160
|
+
const raw = await GmailMime.buildRawMessage({
|
|
161
|
+
to: receiver,
|
|
162
|
+
cc,
|
|
163
|
+
bcc,
|
|
164
|
+
from,
|
|
165
|
+
subject: context.propsValue.subject,
|
|
166
|
+
bodyType: context.propsValue.body_type as 'plain_text' | 'html',
|
|
167
|
+
body: context.propsValue.body,
|
|
168
|
+
attachments: newAttachments?.length
|
|
169
|
+
? newAttachments
|
|
170
|
+
: preservedAttachments,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const response = await gmail.users.drafts.update({
|
|
174
|
+
userId: 'me',
|
|
175
|
+
id: context.propsValue.draft_id,
|
|
176
|
+
requestBody: {
|
|
177
|
+
message: {
|
|
178
|
+
raw,
|
|
179
|
+
threadId,
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
return response.data;
|
|
185
|
+
} catch (error: any) {
|
|
186
|
+
if (error.code === 403) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
'Insufficient permissions to update a draft. Ensure the gmail.compose scope is granted.'
|
|
189
|
+
);
|
|
190
|
+
} else if (error.code === 404) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`Draft not found: "${context.propsValue.draft_id}". Use List Drafts to find a valid draft ID.`
|
|
193
|
+
);
|
|
194
|
+
} else if (error.code === 429) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
'Gmail API rate limit exceeded. Please try again later.'
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
throw new Error(`Failed to update draft: ${error.message}`);
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
function addressesOf(
|
|
205
|
+
address: AddressObject | AddressObject[] | undefined
|
|
206
|
+
): string[] {
|
|
207
|
+
return (Array.isArray(address) ? address : address ? [address] : [])
|
|
208
|
+
.flatMap((addressObject) => addressObject.value)
|
|
209
|
+
.map((emailAddress) => emailAddress.address)
|
|
210
|
+
.filter((emailAddress): emailAddress is string => !!emailAddress);
|
|
211
|
+
}
|
package/src/lib/auth.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AppConnectionValueForAuthProperty,
|
|
3
|
+
PieceAuth,
|
|
4
|
+
Property,
|
|
5
|
+
} from '@agnocon/pieces-framework';
|
|
6
|
+
import { AppConnectionType } from '@agnocon/pieces-framework';
|
|
7
|
+
import { oauth2 as googleOauth2 } from '@googleapis/oauth2';
|
|
8
|
+
import { JWT, OAuth2Client } from 'google-auth-library';
|
|
9
|
+
|
|
10
|
+
const gmailServiceAccountScopes = [
|
|
11
|
+
'https://www.googleapis.com/auth/gmail.send',
|
|
12
|
+
'https://www.googleapis.com/auth/gmail.readonly',
|
|
13
|
+
'https://www.googleapis.com/auth/gmail.compose',
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export const gmailScopes = [...gmailServiceAccountScopes, 'email'];
|
|
17
|
+
|
|
18
|
+
export const gmailAuth = [
|
|
19
|
+
PieceAuth.OAuth2({
|
|
20
|
+
description: '',
|
|
21
|
+
authUrl: 'https://accounts.google.com/o/oauth2/auth',
|
|
22
|
+
tokenUrl: 'https://oauth2.googleapis.com/token',
|
|
23
|
+
required: true,
|
|
24
|
+
scope: gmailScopes,
|
|
25
|
+
}),
|
|
26
|
+
PieceAuth.CustomAuth({
|
|
27
|
+
displayName: 'Service Account (Advanced)',
|
|
28
|
+
description:
|
|
29
|
+
'Authenticate via service account from https://console.cloud.google.com/ > IAM & Admin > Service Accounts > Create Service Account > Keys > Add key. <br> <br> You can optionally use domain-wide delegation (https://support.google.com/a/answer/162106?hl=en#zippy=%2Cset-up-domain-wide-delegation-for-a-client) to access Gmail without adding the service account to each mailbox. <br> <br> **Note:** A user email with domain-wide delegation is required for Gmail service account access.',
|
|
30
|
+
required: true,
|
|
31
|
+
props: {
|
|
32
|
+
serviceAccount: Property.LongText({
|
|
33
|
+
displayName: 'Service Account JSON Key',
|
|
34
|
+
required: true,
|
|
35
|
+
}),
|
|
36
|
+
userEmail: Property.ShortText({
|
|
37
|
+
displayName: 'User Email',
|
|
38
|
+
required: true,
|
|
39
|
+
description:
|
|
40
|
+
'Email address of the user to impersonate. Required for Gmail service account access via domain-wide delegation.',
|
|
41
|
+
}),
|
|
42
|
+
},
|
|
43
|
+
validate: async ({ auth }) => {
|
|
44
|
+
try {
|
|
45
|
+
await getAccessToken({
|
|
46
|
+
type: AppConnectionType.CUSTOM_AUTH,
|
|
47
|
+
props: { ...auth },
|
|
48
|
+
});
|
|
49
|
+
} catch (e) {
|
|
50
|
+
return {
|
|
51
|
+
valid: false,
|
|
52
|
+
error: (e as Error).message,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
valid: true,
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
export type GmailAuthValue = AppConnectionValueForAuthProperty<
|
|
63
|
+
typeof gmailAuth
|
|
64
|
+
>;
|
|
65
|
+
|
|
66
|
+
export async function createGoogleClient(
|
|
67
|
+
auth: GmailAuthValue
|
|
68
|
+
): Promise<OAuth2Client> {
|
|
69
|
+
if (auth.type === AppConnectionType.CUSTOM_AUTH) {
|
|
70
|
+
let serviceAccount;
|
|
71
|
+
try {
|
|
72
|
+
serviceAccount = JSON.parse(auth.props.serviceAccount);
|
|
73
|
+
} catch {
|
|
74
|
+
throw new Error(
|
|
75
|
+
'Invalid Service Account JSON Key. Please provide a valid JSON string.'
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return new JWT({
|
|
79
|
+
email: serviceAccount.client_email,
|
|
80
|
+
key: serviceAccount.private_key,
|
|
81
|
+
scopes: gmailServiceAccountScopes,
|
|
82
|
+
subject: auth.props.userEmail?.trim() || undefined,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
const authClient = new OAuth2Client();
|
|
86
|
+
authClient.setCredentials(auth);
|
|
87
|
+
return authClient;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const getAccessToken = async (auth: GmailAuthValue): Promise<string> => {
|
|
91
|
+
if (auth.type === AppConnectionType.CUSTOM_AUTH) {
|
|
92
|
+
const googleClient = await createGoogleClient(auth);
|
|
93
|
+
const response = await googleClient.getAccessToken();
|
|
94
|
+
if (response.token) {
|
|
95
|
+
return response.token;
|
|
96
|
+
} else {
|
|
97
|
+
throw new Error(
|
|
98
|
+
'Could not retrieve access token from service account json'
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return auth.access_token;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export async function getUserEmail(
|
|
106
|
+
auth: GmailAuthValue,
|
|
107
|
+
authClient: OAuth2Client
|
|
108
|
+
): Promise<string | undefined> {
|
|
109
|
+
if (auth.type === AppConnectionType.CUSTOM_AUTH) {
|
|
110
|
+
return auth.props.userEmail?.trim();
|
|
111
|
+
}
|
|
112
|
+
return (
|
|
113
|
+
(await googleOauth2({ version: 'v2', auth: authClient }).userinfo.get())
|
|
114
|
+
.data.email ?? undefined
|
|
115
|
+
);
|
|
116
|
+
}
|