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 and 429 retry',
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 cacheKey = `${credentials.email}:${fromEmail}`;
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 maxRetries = 4;
456
- let lastError;
457
-
458
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
459
- try {
460
- const sendResponse = await this.helpers.httpRequest({
461
- method: 'POST',
462
- url: 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send',
463
- headers: {
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
- if (lastError) throw lastError;
438
+ returnData.push({
439
+ json: sendResponse,
440
+ pairedItem: { item: i },
441
+ });
489
442
 
490
443
  } catch (error) {
491
- const continueOnFail = typeof this.continueOnFail === 'function' ? this.continueOnFail() : false;
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-gmail-custom",
3
- "version": "0.1.8",
3
+ "version": "0.2.1",
4
4
  "description": "Custom Gmail node for n8n with token caching and 429 retry",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",