n8n-nodes-gmail-custom 0.1.7 → 0.2.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.
@@ -1,5 +1,8 @@
1
1
  const crypto = require('crypto');
2
2
 
3
+ let jwtLib;
4
+ try { jwtLib = require('jsonwebtoken'); } catch (e) {}
5
+
3
6
  const TOKEN_CACHE = {};
4
7
 
5
8
  function formatPrivateKey(privateKey) {
@@ -158,6 +161,58 @@ async function buildMimeMessage(options) {
158
161
  .replace(/=+$/, '');
159
162
  }
160
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
+
161
216
  class GmailCustom {
162
217
 
163
218
  constructor() {
@@ -303,10 +358,15 @@ class GmailCustom {
303
358
 
304
359
  async execute() {
305
360
  const items = this.getInputData();
361
+ if (!items || items.length === 0) {
362
+ return [[]];
363
+ }
364
+
306
365
  const returnData = [];
307
366
 
308
367
  for (let i = 0; i < items.length; i++) {
309
368
  try {
369
+ const item = items[i];
310
370
  const fromEmail = this.getNodeParameter('fromEmail', i, '');
311
371
  const sendTo = this.getNodeParameter('sendTo', i, '');
312
372
  const subject = this.getNodeParameter('subject', i, '');
@@ -336,59 +396,15 @@ class GmailCustom {
336
396
  }
337
397
 
338
398
  if (appendAttribution) {
339
- const attributionText = '\n\n---\nThis email was sent automatically with n8n';
340
399
  if (emailType === 'html') {
341
400
  bodyHtml += '<br><br>---<br><em>This email was sent automatically with n8n</em>';
342
401
  } else {
343
- bodyText += attributionText;
402
+ bodyText += '\n\n---\nThis email was sent automatically with n8n';
344
403
  }
345
404
  }
346
405
 
347
406
  const credentials = await this.getCredentials('googleApi');
348
- const cacheKey = `${credentials.email}:${fromEmail}`;
349
- let accessToken;
350
- let expiresAt = 0;
351
-
352
- if (TOKEN_CACHE[cacheKey]) {
353
- expiresAt = TOKEN_CACHE[cacheKey].expiresAt;
354
- accessToken = TOKEN_CACHE[cacheKey].accessToken;
355
- }
356
-
357
- if (!accessToken || Date.now() >= expiresAt) {
358
- const privateKey = formatPrivateKey(credentials.privateKey);
359
- const now = Math.floor(Date.now() / 1000);
360
- const header = { alg: 'RS256', typ: 'JWT', kid: privateKey };
361
- const payload = {
362
- iss: credentials.email,
363
- scope: 'https://mail.google.com/',
364
- aud: 'https://oauth2.googleapis.com/token',
365
- exp: now + 3600,
366
- iat: now,
367
- sub: fromEmail,
368
- };
369
-
370
- const signatureInput = base64UrlEncode(JSON.stringify(header)) + '.' + base64UrlEncode(JSON.stringify(payload));
371
- const signer = crypto.createSign('RSA-SHA256');
372
- signer.update(signatureInput);
373
- const sig = signer.sign(privateKey);
374
- const jwt = signatureInput + '.' + base64urlEscape(sig.toString('base64'));
375
-
376
- const response = await this.helpers.httpRequest({
377
- method: 'POST',
378
- url: 'https://oauth2.googleapis.com/token',
379
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
380
- body: new URLSearchParams({
381
- grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
382
- assertion: jwt,
383
- }).toString(),
384
- });
385
-
386
- accessToken = response.access_token;
387
- TOKEN_CACHE[cacheKey] = {
388
- accessToken,
389
- expiresAt: Date.now() + 3500 * 1000,
390
- };
391
- }
407
+ const accessToken = await getOrRefreshAccessToken(this, credentials, fromEmail);
392
408
 
393
409
  let attachments = [];
394
410
  if (options.attachmentsUi && options.attachmentsUi.attachmentsBinary) {
@@ -461,10 +477,11 @@ class GmailCustom {
461
477
  if (lastError) throw lastError;
462
478
 
463
479
  } catch (error) {
464
- const continueOnFail = typeof this.continueOnFail === 'function' ? this.continueOnFail() : false;
480
+ let continueOnFail = false;
481
+ try { continueOnFail = this.continueOnFail(); } catch (e) {}
465
482
  if (continueOnFail) {
466
483
  returnData.push({
467
- json: { error: error.message },
484
+ json: { error: error.message || 'Unknown error' },
468
485
  pairedItem: { item: i },
469
486
  });
470
487
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-gmail-custom",
3
- "version": "0.1.7",
3
+ "version": "0.2.0",
4
4
  "description": "Custom Gmail node for n8n with token caching and 429 retry",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",