n8n-nodes-gmail-custom 0.4.0 → 0.5.1
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.
|
@@ -1,28 +1,5 @@
|
|
|
1
1
|
const crypto = require('crypto');
|
|
2
|
-
|
|
3
|
-
let jwtLib;
|
|
4
|
-
try { jwtLib = require('jsonwebtoken'); } catch (e) {}
|
|
5
|
-
|
|
6
|
-
const TOKEN_CACHE = {};
|
|
7
|
-
|
|
8
|
-
function formatPrivateKey(privateKey) {
|
|
9
|
-
if (privateKey.includes('\\n')) {
|
|
10
|
-
return privateKey.replace(/\\n/g, '\n');
|
|
11
|
-
}
|
|
12
|
-
return privateKey;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function base64UrlEncode(str) {
|
|
16
|
-
return Buffer.from(str)
|
|
17
|
-
.toString('base64')
|
|
18
|
-
.replace(/=/g, '')
|
|
19
|
-
.replace(/\+/g, '-')
|
|
20
|
-
.replace(/\//g, '_');
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function base64urlEscape(base64) {
|
|
24
|
-
return base64.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
|
25
|
-
}
|
|
2
|
+
const { getOrRefreshAccessToken } = require('./utils');
|
|
26
3
|
|
|
27
4
|
function generateBoundary() {
|
|
28
5
|
return '----=_Part_' + crypto.randomBytes(16).toString('hex');
|
|
@@ -166,58 +143,6 @@ async function buildMimeMessage(options) {
|
|
|
166
143
|
.replace(/=+$/, '');
|
|
167
144
|
}
|
|
168
145
|
|
|
169
|
-
async function getOrRefreshAccessToken(ctx, serviceAccountEmail, privateKeyRaw, fromEmail) {
|
|
170
|
-
const cacheKey = `${serviceAccountEmail}:${fromEmail}`;
|
|
171
|
-
let token = TOKEN_CACHE[cacheKey];
|
|
172
|
-
if (token && token.expiresAt > Date.now()) {
|
|
173
|
-
return token.accessToken;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const privateKey = formatPrivateKey(privateKeyRaw);
|
|
177
|
-
const now = Math.floor(Date.now() / 1000);
|
|
178
|
-
|
|
179
|
-
let jwt;
|
|
180
|
-
const payload = {
|
|
181
|
-
iss: serviceAccountEmail,
|
|
182
|
-
scope: 'https://mail.google.com/',
|
|
183
|
-
aud: 'https://oauth2.googleapis.com/token',
|
|
184
|
-
exp: now + 3600,
|
|
185
|
-
iat: now,
|
|
186
|
-
sub: fromEmail,
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
if (jwtLib) {
|
|
190
|
-
jwt = jwtLib.sign(payload, privateKey, {
|
|
191
|
-
algorithm: 'RS256',
|
|
192
|
-
header: { kid: privateKey, typ: 'JWT', alg: 'RS256' },
|
|
193
|
-
});
|
|
194
|
-
} else {
|
|
195
|
-
const header = { alg: 'RS256', typ: 'JWT', kid: privateKey };
|
|
196
|
-
const signatureInput = base64UrlEncode(JSON.stringify(header)) + '.' + base64UrlEncode(JSON.stringify(payload));
|
|
197
|
-
const signer = crypto.createSign('RSA-SHA256');
|
|
198
|
-
signer.update(signatureInput);
|
|
199
|
-
const sig = signer.sign(privateKey);
|
|
200
|
-
jwt = signatureInput + '.' + base64urlEscape(sig.toString('base64'));
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const response = await ctx.helpers.httpRequest({
|
|
204
|
-
method: 'POST',
|
|
205
|
-
url: 'https://oauth2.googleapis.com/token',
|
|
206
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
207
|
-
body: new URLSearchParams({
|
|
208
|
-
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
|
209
|
-
assertion: jwt,
|
|
210
|
-
}).toString(),
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
TOKEN_CACHE[cacheKey] = {
|
|
214
|
-
accessToken: response.access_token,
|
|
215
|
-
expiresAt: Date.now() + 3500 * 1000,
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
return response.access_token;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
146
|
class GmailCustom {
|
|
222
147
|
|
|
223
148
|
constructor() {
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
const { getOrRefreshAccessToken } = require('./utils');
|
|
2
|
+
|
|
3
|
+
class GmailTriggerCustom {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.description = {
|
|
6
|
+
displayName: 'Gmail Trigger Custom',
|
|
7
|
+
name: 'gmailTriggerCustom',
|
|
8
|
+
icon: 'fa:envelope',
|
|
9
|
+
group: ['trigger'],
|
|
10
|
+
version: 1,
|
|
11
|
+
subtitle: 'Gmail Trigger',
|
|
12
|
+
description: 'Fetches emails from Gmail on polling intervals with token caching',
|
|
13
|
+
defaults: { name: 'Gmail Trigger Custom' },
|
|
14
|
+
polling: true,
|
|
15
|
+
inputs: [],
|
|
16
|
+
outputs: ['main'],
|
|
17
|
+
properties: [
|
|
18
|
+
{
|
|
19
|
+
displayName: 'Service Account Email',
|
|
20
|
+
name: 'serviceAccountEmail',
|
|
21
|
+
type: 'string',
|
|
22
|
+
default: '',
|
|
23
|
+
required: true,
|
|
24
|
+
placeholder: 'sa-name@project.iam.gserviceaccount.com',
|
|
25
|
+
description: 'The service account client_email (issuer for JWT)',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
displayName: 'Private Key',
|
|
29
|
+
name: 'privateKey',
|
|
30
|
+
type: 'string',
|
|
31
|
+
typeOptions: { password: true },
|
|
32
|
+
default: '',
|
|
33
|
+
required: true,
|
|
34
|
+
description: 'The service account RSA private key in PEM format',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
displayName: 'Delegated Email',
|
|
38
|
+
name: 'delegatedEmail',
|
|
39
|
+
type: 'string',
|
|
40
|
+
default: '',
|
|
41
|
+
required: true,
|
|
42
|
+
placeholder: 'user@domain.com',
|
|
43
|
+
description: 'The Gmail mailbox to access (user to impersonate via domain-wide delegation)',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
displayName: 'Simplify',
|
|
47
|
+
name: 'simple',
|
|
48
|
+
type: 'boolean',
|
|
49
|
+
default: true,
|
|
50
|
+
description: 'Whether to return a simplified version of the response instead of the raw data',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
displayName: 'Max Emails per Poll',
|
|
54
|
+
name: 'maxResults',
|
|
55
|
+
type: 'number',
|
|
56
|
+
default: 10,
|
|
57
|
+
typeOptions: {
|
|
58
|
+
minValue: 1,
|
|
59
|
+
maxValue: 50,
|
|
60
|
+
},
|
|
61
|
+
description: 'Maximum number of emails to fetch each time the node polls. Remaining emails are picked up in subsequent polls.',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
displayName: 'Mark as Read',
|
|
65
|
+
name: 'markAsRead',
|
|
66
|
+
type: 'boolean',
|
|
67
|
+
default: false,
|
|
68
|
+
description: 'Whether to mark processed emails as read (adds 1 API call per email)',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
displayName: 'Filters',
|
|
72
|
+
name: 'filters',
|
|
73
|
+
type: 'collection',
|
|
74
|
+
placeholder: 'Add Filter',
|
|
75
|
+
default: {},
|
|
76
|
+
options: [
|
|
77
|
+
{
|
|
78
|
+
displayName: 'Include Spam and Trash',
|
|
79
|
+
name: 'includeSpamTrash',
|
|
80
|
+
type: 'boolean',
|
|
81
|
+
default: false,
|
|
82
|
+
description: 'Whether to include messages from SPAM and TRASH in the results',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
displayName: 'Include Drafts',
|
|
86
|
+
name: 'includeDrafts',
|
|
87
|
+
type: 'boolean',
|
|
88
|
+
default: false,
|
|
89
|
+
description: 'Whether to include email drafts in the results',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
displayName: 'Label Names or IDs',
|
|
93
|
+
name: 'labelIds',
|
|
94
|
+
type: 'string',
|
|
95
|
+
default: '',
|
|
96
|
+
description: 'Comma-separated label IDs to filter',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
displayName: 'Search',
|
|
100
|
+
name: 'q',
|
|
101
|
+
type: 'string',
|
|
102
|
+
default: '',
|
|
103
|
+
placeholder: 'has:attachment',
|
|
104
|
+
description: 'Gmail search query. See <a href="https://support.google.com/mail/answer/7190">Gmail search syntax</a>.',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
displayName: 'Read Status',
|
|
108
|
+
name: 'readStatus',
|
|
109
|
+
type: 'options',
|
|
110
|
+
default: 'unread',
|
|
111
|
+
options: [
|
|
112
|
+
{ name: 'Unread and read emails', value: 'both' },
|
|
113
|
+
{ name: 'Unread emails only', value: 'unread' },
|
|
114
|
+
{ name: 'Read emails only', value: 'read' },
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
displayName: 'Sender',
|
|
119
|
+
name: 'sender',
|
|
120
|
+
type: 'string',
|
|
121
|
+
default: '',
|
|
122
|
+
hint: 'Enter an email or part of a sender name',
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async poll() {
|
|
131
|
+
const webhookName = 'default';
|
|
132
|
+
const workflowStaticData = this.getWorkflowStaticData('node') || {};
|
|
133
|
+
if (!workflowStaticData[webhookName]) {
|
|
134
|
+
workflowStaticData[webhookName] = {};
|
|
135
|
+
}
|
|
136
|
+
const nodeStaticData = workflowStaticData[webhookName];
|
|
137
|
+
|
|
138
|
+
const now = Math.floor(Date.now() / 1000);
|
|
139
|
+
|
|
140
|
+
if (this.getMode() !== 'manual') {
|
|
141
|
+
nodeStaticData.lastTimeChecked = nodeStaticData.lastTimeChecked || now;
|
|
142
|
+
}
|
|
143
|
+
const startDate = nodeStaticData.lastTimeChecked || now;
|
|
144
|
+
|
|
145
|
+
const serviceAccountEmail = this.getNodeParameter('serviceAccountEmail', 0) || '';
|
|
146
|
+
const privateKey = this.getNodeParameter('privateKey', 0) || '';
|
|
147
|
+
const delegatedEmail = this.getNodeParameter('delegatedEmail', 0) || '';
|
|
148
|
+
const simple = this.getNodeParameter('simple', 0);
|
|
149
|
+
let maxResults = this.getNodeParameter('maxResults', 0) || 10;
|
|
150
|
+
const markAsRead = this.getNodeParameter('markAsRead', 0);
|
|
151
|
+
const filters = this.getNodeParameter('filters', 0, {});
|
|
152
|
+
|
|
153
|
+
const accessToken = await getOrRefreshAccessToken(this, serviceAccountEmail, privateKey, delegatedEmail);
|
|
154
|
+
|
|
155
|
+
const responseData = [];
|
|
156
|
+
const allFetchedMessages = [];
|
|
157
|
+
|
|
158
|
+
const getEmailDateAsSeconds = (email) => {
|
|
159
|
+
let date;
|
|
160
|
+
if (email.internalDate) {
|
|
161
|
+
date = Math.floor(parseInt(email.internalDate, 10) / 1000);
|
|
162
|
+
}
|
|
163
|
+
if (!date || isNaN(date)) {
|
|
164
|
+
return startDate;
|
|
165
|
+
}
|
|
166
|
+
return date;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const fetchAndProcessMessage = async (messageId, fetchQs) => {
|
|
170
|
+
const fullMessage = await this.helpers.httpRequest({
|
|
171
|
+
method: 'GET',
|
|
172
|
+
url: `https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}`,
|
|
173
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
174
|
+
qs: fetchQs,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
allFetchedMessages.push({
|
|
178
|
+
id: fullMessage.id,
|
|
179
|
+
date: getEmailDateAsSeconds(fullMessage),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (!filters.includeDrafts && fullMessage.labelIds && fullMessage.labelIds.includes('DRAFT')) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (fullMessage.labelIds && fullMessage.labelIds.includes('SENT') && !fullMessage.labelIds.includes('INBOX')) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (markAsRead) {
|
|
190
|
+
try {
|
|
191
|
+
await this.helpers.httpRequest({
|
|
192
|
+
method: 'POST',
|
|
193
|
+
url: `https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}/modify`,
|
|
194
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
195
|
+
body: { removeLabelIds: ['UNREAD'] },
|
|
196
|
+
});
|
|
197
|
+
} catch (e) {}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
responseData.push({ json: fullMessage });
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const qs = {};
|
|
205
|
+
const parts = [];
|
|
206
|
+
|
|
207
|
+
if (!simple) {
|
|
208
|
+
qs.format = 'raw';
|
|
209
|
+
} else {
|
|
210
|
+
qs.format = 'metadata';
|
|
211
|
+
qs.metadataHeaders = ['From', 'To', 'Cc', 'Bcc', 'Subject'];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (this.getMode() !== 'manual') {
|
|
215
|
+
parts.push(`after:${startDate}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (filters.includeSpamTrash) {
|
|
219
|
+
parts.push('in:anywhere');
|
|
220
|
+
} else {
|
|
221
|
+
parts.push('-in:spam -in:trash');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (filters.readStatus === 'unread') {
|
|
225
|
+
parts.push('is:unread');
|
|
226
|
+
} else if (filters.readStatus === 'read') {
|
|
227
|
+
parts.push('is:read');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (filters.sender) {
|
|
231
|
+
parts.push(`from:${filters.sender}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (filters.labelIds) {
|
|
235
|
+
for (const labelId of filters.labelIds.split(',')) {
|
|
236
|
+
parts.push(`label:${labelId.trim()}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (filters.q) {
|
|
241
|
+
parts.push(filters.q);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
parts.push('-in:scheduled');
|
|
245
|
+
|
|
246
|
+
qs.q = parts.join(' ');
|
|
247
|
+
|
|
248
|
+
if (this.getMode() === 'manual') {
|
|
249
|
+
maxResults = 1;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let budget = maxResults;
|
|
253
|
+
|
|
254
|
+
// Process pending messages from previous poll first.
|
|
255
|
+
const pendingIds = nodeStaticData.pendingMessageIds || [];
|
|
256
|
+
if (pendingIds.length > 0) {
|
|
257
|
+
const idsToFetch = pendingIds.slice(0, budget);
|
|
258
|
+
nodeStaticData.pendingMessageIds = pendingIds.slice(budget);
|
|
259
|
+
|
|
260
|
+
const fetchQs = {};
|
|
261
|
+
if (!simple) { fetchQs.format = 'raw'; }
|
|
262
|
+
else { fetchQs.format = 'metadata'; fetchQs.metadataHeaders = ['From', 'To', 'Cc', 'Bcc', 'Subject']; }
|
|
263
|
+
|
|
264
|
+
for (const id of idsToFetch) {
|
|
265
|
+
await fetchAndProcessMessage(id, fetchQs);
|
|
266
|
+
}
|
|
267
|
+
budget -= idsToFetch.length;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Only list new messages if budget remains.
|
|
271
|
+
if (budget > 0) {
|
|
272
|
+
const messagesResponse = await this.helpers.httpRequest({
|
|
273
|
+
method: 'GET',
|
|
274
|
+
url: 'https://gmail.googleapis.com/gmail/v1/users/me/messages',
|
|
275
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
276
|
+
qs,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const messages = messagesResponse.messages || [];
|
|
280
|
+
|
|
281
|
+
if (messages.length > 0) {
|
|
282
|
+
const possibleDuplicates = new Set(nodeStaticData.possibleDuplicates || []);
|
|
283
|
+
const filteredMessages = possibleDuplicates.size > 0
|
|
284
|
+
? messages.filter((m) => !possibleDuplicates.has(m.id))
|
|
285
|
+
: messages;
|
|
286
|
+
|
|
287
|
+
let messagesToProcess = filteredMessages;
|
|
288
|
+
if (filteredMessages.length > budget) {
|
|
289
|
+
messagesToProcess = filteredMessages.slice(0, budget);
|
|
290
|
+
nodeStaticData.pendingMessageIds = filteredMessages.slice(budget).map((m) => m.id);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const fetchQs = {};
|
|
294
|
+
if (!simple) { fetchQs.format = 'raw'; }
|
|
295
|
+
else { fetchQs.format = 'metadata'; fetchQs.metadataHeaders = ['From', 'To', 'Cc', 'Bcc', 'Subject']; }
|
|
296
|
+
|
|
297
|
+
for (const message of messagesToProcess) {
|
|
298
|
+
await fetchAndProcessMessage(message.id, fetchQs);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
} catch (error) {
|
|
303
|
+
if (this.getMode() === 'manual' || !nodeStaticData.lastTimeChecked) {
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
this.logger.error(`Gmail Trigger Custom poll error: ${error.message}`);
|
|
308
|
+
} catch (e) {}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (!allFetchedMessages.length) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const lastEmailDate = allFetchedMessages.reduce(
|
|
316
|
+
(last, m) => (m.date > last ? m.date : last), 0,
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
nodeStaticData.possibleDuplicates = allFetchedMessages.map((m) => m.id);
|
|
320
|
+
if (!nodeStaticData.pendingMessageIds || nodeStaticData.pendingMessageIds.length === 0) {
|
|
321
|
+
nodeStaticData.lastTimeChecked = Math.max(lastEmailDate || startDate, startDate);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return responseData.length > 0 ? [responseData] : null;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
module.exports = {
|
|
329
|
+
nodeTypes: [GmailTriggerCustom],
|
|
330
|
+
GmailTriggerCustom,
|
|
331
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
let jwtLib;
|
|
4
|
+
try { jwtLib = require('jsonwebtoken'); } catch (e) {}
|
|
5
|
+
|
|
6
|
+
const TOKEN_CACHE = {};
|
|
7
|
+
|
|
8
|
+
function formatPrivateKey(privateKey) {
|
|
9
|
+
if (privateKey.includes('\\n')) {
|
|
10
|
+
return privateKey.replace(/\\n/g, '\n');
|
|
11
|
+
}
|
|
12
|
+
return privateKey;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function base64UrlEncode(str) {
|
|
16
|
+
return Buffer.from(str)
|
|
17
|
+
.toString('base64')
|
|
18
|
+
.replace(/=/g, '')
|
|
19
|
+
.replace(/\+/g, '-')
|
|
20
|
+
.replace(/\//g, '_');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function base64urlEscape(base64) {
|
|
24
|
+
return base64.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function getOrRefreshAccessToken(ctx, serviceAccountEmail, privateKeyRaw, delegatedEmail) {
|
|
28
|
+
const cacheKey = `${serviceAccountEmail}:${delegatedEmail}`;
|
|
29
|
+
let token = TOKEN_CACHE[cacheKey];
|
|
30
|
+
if (token && token.expiresAt > Date.now()) {
|
|
31
|
+
return token.accessToken;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const privateKey = formatPrivateKey(privateKeyRaw);
|
|
35
|
+
const now = Math.floor(Date.now() / 1000);
|
|
36
|
+
|
|
37
|
+
let jwt;
|
|
38
|
+
const payload = {
|
|
39
|
+
iss: serviceAccountEmail,
|
|
40
|
+
scope: 'https://mail.google.com/',
|
|
41
|
+
aud: 'https://oauth2.googleapis.com/token',
|
|
42
|
+
exp: now + 3600,
|
|
43
|
+
iat: now,
|
|
44
|
+
sub: delegatedEmail,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
if (jwtLib) {
|
|
48
|
+
jwt = jwtLib.sign(payload, privateKey, {
|
|
49
|
+
algorithm: 'RS256',
|
|
50
|
+
header: { kid: privateKey, typ: 'JWT', alg: 'RS256' },
|
|
51
|
+
});
|
|
52
|
+
} else {
|
|
53
|
+
const header = { alg: 'RS256', typ: 'JWT', kid: privateKey };
|
|
54
|
+
const signatureInput = base64UrlEncode(JSON.stringify(header)) + '.' + base64UrlEncode(JSON.stringify(payload));
|
|
55
|
+
const signer = crypto.createSign('RSA-SHA256');
|
|
56
|
+
signer.update(signatureInput);
|
|
57
|
+
const sig = signer.sign(privateKey);
|
|
58
|
+
jwt = signatureInput + '.' + base64urlEscape(sig.toString('base64'));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const response = await ctx.helpers.httpRequest({
|
|
62
|
+
method: 'POST',
|
|
63
|
+
url: 'https://oauth2.googleapis.com/token',
|
|
64
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
65
|
+
body: new URLSearchParams({
|
|
66
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
|
67
|
+
assertion: jwt,
|
|
68
|
+
}).toString(),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
TOKEN_CACHE[cacheKey] = {
|
|
72
|
+
accessToken: response.access_token,
|
|
73
|
+
expiresAt: Date.now() + 3500 * 1000,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return response.access_token;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function clearTokenCache(serviceAccountEmail, delegatedEmail) {
|
|
80
|
+
delete TOKEN_CACHE[`${serviceAccountEmail}:${delegatedEmail}`];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = {
|
|
84
|
+
formatPrivateKey,
|
|
85
|
+
base64UrlEncode,
|
|
86
|
+
base64urlEscape,
|
|
87
|
+
getOrRefreshAccessToken,
|
|
88
|
+
clearTokenCache,
|
|
89
|
+
TOKEN_CACHE,
|
|
90
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "n8n-nodes-gmail-custom",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Custom Gmail
|
|
3
|
+
"version": "0.5.1",
|
|
4
|
+
"description": "Custom Gmail nodes for n8n with token caching — Send and Trigger, self-contained credentials",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"n8n-community-node-package",
|
|
7
7
|
"n8n",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"n8nNodesApiVersion": 1,
|
|
20
20
|
"credentials": [],
|
|
21
21
|
"nodes": [
|
|
22
|
-
"nodes/GmailCustom/GmailCustom.node.js"
|
|
22
|
+
"nodes/GmailCustom/GmailCustom.node.js",
|
|
23
|
+
"nodes/GmailCustom/GmailTriggerCustom.node.js"
|
|
23
24
|
]
|
|
24
25
|
}
|
|
25
26
|
}
|