n8n-nodes-gmail-custom 0.1.8 → 0.2.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.
|
@@ -161,6 +161,58 @@ async function buildMimeMessage(options) {
|
|
|
161
161
|
.replace(/=+$/, '');
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
async function getOrRefreshAccessToken(ctx, credentials, fromEmail) {
|
|
165
|
+
const cacheKey = `${credentials.email}:${fromEmail}`;
|
|
166
|
+
let token = TOKEN_CACHE[cacheKey];
|
|
167
|
+
if (token && token.expiresAt > Date.now()) {
|
|
168
|
+
return token.accessToken;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const privateKey = formatPrivateKey(credentials.privateKey);
|
|
172
|
+
const now = Math.floor(Date.now() / 1000);
|
|
173
|
+
|
|
174
|
+
let jwt;
|
|
175
|
+
const payload = {
|
|
176
|
+
iss: credentials.email,
|
|
177
|
+
scope: 'https://mail.google.com/',
|
|
178
|
+
aud: 'https://oauth2.googleapis.com/token',
|
|
179
|
+
exp: now + 3600,
|
|
180
|
+
iat: now,
|
|
181
|
+
sub: fromEmail,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
if (jwtLib) {
|
|
185
|
+
jwt = jwtLib.sign(payload, privateKey, {
|
|
186
|
+
algorithm: 'RS256',
|
|
187
|
+
header: { kid: privateKey, typ: 'JWT', alg: 'RS256' },
|
|
188
|
+
});
|
|
189
|
+
} else {
|
|
190
|
+
const header = { alg: 'RS256', typ: 'JWT', kid: privateKey };
|
|
191
|
+
const signatureInput = base64UrlEncode(JSON.stringify(header)) + '.' + base64UrlEncode(JSON.stringify(payload));
|
|
192
|
+
const signer = crypto.createSign('RSA-SHA256');
|
|
193
|
+
signer.update(signatureInput);
|
|
194
|
+
const sig = signer.sign(privateKey);
|
|
195
|
+
jwt = signatureInput + '.' + base64urlEscape(sig.toString('base64'));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const response = await ctx.helpers.httpRequest({
|
|
199
|
+
method: 'POST',
|
|
200
|
+
url: 'https://oauth2.googleapis.com/token',
|
|
201
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
202
|
+
body: new URLSearchParams({
|
|
203
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
|
204
|
+
assertion: jwt,
|
|
205
|
+
}).toString(),
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
TOKEN_CACHE[cacheKey] = {
|
|
209
|
+
accessToken: response.access_token,
|
|
210
|
+
expiresAt: Date.now() + 3500 * 1000,
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
return response.access_token;
|
|
214
|
+
}
|
|
215
|
+
|
|
164
216
|
class GmailCustom {
|
|
165
217
|
|
|
166
218
|
constructor() {
|
|
@@ -171,7 +223,7 @@ class GmailCustom {
|
|
|
171
223
|
group: ['output'],
|
|
172
224
|
version: 1,
|
|
173
225
|
subtitle: '={{$parameter["subject"] || "Send Email"}}',
|
|
174
|
-
description: 'Send email via Gmail API with token caching
|
|
226
|
+
description: 'Send email via Gmail API with token caching for service accounts',
|
|
175
227
|
defaults: {
|
|
176
228
|
name: 'Gmail Custom',
|
|
177
229
|
color: '#1a73e8',
|
|
@@ -291,13 +343,6 @@ class GmailCustom {
|
|
|
291
343
|
default: '',
|
|
292
344
|
description: 'The email address that the reply message is sent to',
|
|
293
345
|
},
|
|
294
|
-
{
|
|
295
|
-
displayName: 'Append n8n Attribution',
|
|
296
|
-
name: 'appendAttribution',
|
|
297
|
-
type: 'boolean',
|
|
298
|
-
default: true,
|
|
299
|
-
description: 'Whether to include the phrase "This email was sent automatically with n8n" to the end of the email',
|
|
300
|
-
},
|
|
301
346
|
],
|
|
302
347
|
},
|
|
303
348
|
],
|
|
@@ -306,10 +351,15 @@ class GmailCustom {
|
|
|
306
351
|
|
|
307
352
|
async execute() {
|
|
308
353
|
const items = this.getInputData();
|
|
354
|
+
if (!items || items.length === 0) {
|
|
355
|
+
return [[]];
|
|
356
|
+
}
|
|
357
|
+
|
|
309
358
|
const returnData = [];
|
|
310
359
|
|
|
311
360
|
for (let i = 0; i < items.length; i++) {
|
|
312
361
|
try {
|
|
362
|
+
const item = items[i];
|
|
313
363
|
const fromEmail = this.getNodeParameter('fromEmail', i, '');
|
|
314
364
|
const sendTo = this.getNodeParameter('sendTo', i, '');
|
|
315
365
|
const subject = this.getNodeParameter('subject', i, '');
|
|
@@ -325,7 +375,6 @@ class GmailCustom {
|
|
|
325
375
|
let cc = options.ccList || '';
|
|
326
376
|
let bcc = options.bccList || '';
|
|
327
377
|
let replyTo = options.replyTo || '';
|
|
328
|
-
const appendAttribution = options.appendAttribution === undefined ? true : options.appendAttribution;
|
|
329
378
|
|
|
330
379
|
let bodyText = message;
|
|
331
380
|
let bodyHtml = '';
|
|
@@ -338,84 +387,8 @@ class GmailCustom {
|
|
|
338
387
|
bodyText = message;
|
|
339
388
|
}
|
|
340
389
|
|
|
341
|
-
if (appendAttribution) {
|
|
342
|
-
const attributionText = '\n\n---\nThis email was sent automatically with n8n';
|
|
343
|
-
if (emailType === 'html') {
|
|
344
|
-
bodyHtml += '<br><br>---<br><em>This email was sent automatically with n8n</em>';
|
|
345
|
-
} else {
|
|
346
|
-
bodyText += attributionText;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
390
|
const credentials = await this.getCredentials('googleApi');
|
|
351
|
-
const
|
|
352
|
-
let accessToken;
|
|
353
|
-
let expiresAt = 0;
|
|
354
|
-
|
|
355
|
-
if (TOKEN_CACHE[cacheKey]) {
|
|
356
|
-
expiresAt = TOKEN_CACHE[cacheKey].expiresAt;
|
|
357
|
-
accessToken = TOKEN_CACHE[cacheKey].accessToken;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
if (!accessToken || Date.now() >= expiresAt) {
|
|
361
|
-
const privateKey = formatPrivateKey(credentials.privateKey);
|
|
362
|
-
const now = Math.floor(Date.now() / 1000);
|
|
363
|
-
|
|
364
|
-
let jwt;
|
|
365
|
-
if (jwtLib) {
|
|
366
|
-
jwt = jwtLib.sign(
|
|
367
|
-
{
|
|
368
|
-
iss: credentials.email,
|
|
369
|
-
scope: 'https://mail.google.com/',
|
|
370
|
-
aud: 'https://oauth2.googleapis.com/token',
|
|
371
|
-
exp: now + 3600,
|
|
372
|
-
iat: now,
|
|
373
|
-
sub: fromEmail,
|
|
374
|
-
},
|
|
375
|
-
privateKey,
|
|
376
|
-
{
|
|
377
|
-
algorithm: 'RS256',
|
|
378
|
-
header: {
|
|
379
|
-
kid: privateKey,
|
|
380
|
-
typ: 'JWT',
|
|
381
|
-
alg: 'RS256',
|
|
382
|
-
},
|
|
383
|
-
},
|
|
384
|
-
);
|
|
385
|
-
} else {
|
|
386
|
-
const header = { alg: 'RS256', typ: 'JWT', kid: privateKey };
|
|
387
|
-
const payload = {
|
|
388
|
-
iss: credentials.email,
|
|
389
|
-
scope: 'https://mail.google.com/',
|
|
390
|
-
aud: 'https://oauth2.googleapis.com/token',
|
|
391
|
-
exp: now + 3600,
|
|
392
|
-
iat: now,
|
|
393
|
-
sub: fromEmail,
|
|
394
|
-
};
|
|
395
|
-
const signatureInput = base64UrlEncode(JSON.stringify(header)) + '.' + base64UrlEncode(JSON.stringify(payload));
|
|
396
|
-
const signer = crypto.createSign('RSA-SHA256');
|
|
397
|
-
signer.update(signatureInput);
|
|
398
|
-
const sig = signer.sign(privateKey);
|
|
399
|
-
jwt = signatureInput + '.' + base64urlEscape(sig.toString('base64'));
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const response = await this.helpers.request({
|
|
403
|
-
method: 'POST',
|
|
404
|
-
uri: 'https://oauth2.googleapis.com/token',
|
|
405
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
406
|
-
form: {
|
|
407
|
-
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
|
408
|
-
assertion: jwt,
|
|
409
|
-
},
|
|
410
|
-
json: true,
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
accessToken = response.access_token;
|
|
414
|
-
TOKEN_CACHE[cacheKey] = {
|
|
415
|
-
accessToken,
|
|
416
|
-
expiresAt: Date.now() + 3500 * 1000,
|
|
417
|
-
};
|
|
418
|
-
}
|
|
391
|
+
const accessToken = await getOrRefreshAccessToken(this, credentials, fromEmail);
|
|
419
392
|
|
|
420
393
|
let attachments = [];
|
|
421
394
|
if (options.attachmentsUi && options.attachmentsUi.attachmentsBinary) {
|
|
@@ -452,46 +425,27 @@ class GmailCustom {
|
|
|
452
425
|
attachments,
|
|
453
426
|
});
|
|
454
427
|
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
Authorization: `Bearer ${accessToken}`,
|
|
465
|
-
'Content-Type': 'application/json',
|
|
466
|
-
},
|
|
467
|
-
body: { raw: base64UrlMessage },
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
returnData.push({
|
|
471
|
-
json: sendResponse,
|
|
472
|
-
pairedItem: { item: i },
|
|
473
|
-
});
|
|
474
|
-
lastError = null;
|
|
475
|
-
break;
|
|
476
|
-
} catch (error) {
|
|
477
|
-
lastError = error;
|
|
478
|
-
const statusCode = error.statusCode || error.response?.statusCode;
|
|
479
|
-
if (statusCode === 429 && attempt < maxRetries) {
|
|
480
|
-
const delay = Math.min(Math.pow(2, attempt) * 1000 + Math.random() * 1000, 30000);
|
|
481
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
482
|
-
continue;
|
|
483
|
-
}
|
|
484
|
-
throw error;
|
|
485
|
-
}
|
|
486
|
-
}
|
|
428
|
+
const sendResponse = await this.helpers.httpRequest({
|
|
429
|
+
method: 'POST',
|
|
430
|
+
url: 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send',
|
|
431
|
+
headers: {
|
|
432
|
+
Authorization: `Bearer ${accessToken}`,
|
|
433
|
+
'Content-Type': 'application/json',
|
|
434
|
+
},
|
|
435
|
+
body: { raw: base64UrlMessage },
|
|
436
|
+
});
|
|
487
437
|
|
|
488
|
-
|
|
438
|
+
returnData.push({
|
|
439
|
+
json: sendResponse,
|
|
440
|
+
pairedItem: { item: i },
|
|
441
|
+
});
|
|
489
442
|
|
|
490
443
|
} catch (error) {
|
|
491
|
-
|
|
444
|
+
let continueOnFail = false;
|
|
445
|
+
try { continueOnFail = this.continueOnFail(); } catch (e) {}
|
|
492
446
|
if (continueOnFail) {
|
|
493
447
|
returnData.push({
|
|
494
|
-
json: { error: error.message },
|
|
448
|
+
json: { error: error.message || 'Unknown error' },
|
|
495
449
|
pairedItem: { item: i },
|
|
496
450
|
});
|
|
497
451
|
continue;
|