n8n-nodes-gmail-custom 0.2.1 → 0.4.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/README.md CHANGED
@@ -1,32 +1,66 @@
1
1
  # n8n-nodes-gmail-custom
2
2
 
3
- Custom Gmail node for n8n with token caching and 429 retry.
3
+ Self-contained n8n node for sending emails via Gmail API with a Google service account and domain-wide delegation.
4
4
 
5
- A drop-in replacement for the native Gmail v2 Send operation, optimized for service account usage with domain-wide delegation.
5
+ No credential setup in n8n required all parameters are inline, supports expressions.
6
6
 
7
7
  ## Features
8
8
 
9
- - Same UI as native Gmail Send (To, Subject, Email Type, Message, Options with CC/BCC/ReplyTo/Attachments)
10
- - **Token caching** access token is reused across calls (1 auth call per hour instead of per email)
11
- - **429 retry** automatic retry with exponential backoff when rate limited
12
- - **From Email inline** specify the delegated user directly in the node (no need to set in credential)
13
- - No extra API calls (no GET /profile like the native node)
9
+ - **Self-contained**: service account email + private key are node parameters (no n8n credential needed)
10
+ - **Token caching**: access token reused for 58 minutes — only 1 oauth call per hour instead of per email
11
+ - **Domain-wide delegation**: send as any user in your Workspace domain
12
+ - **Sender name**: set a display name without extra API calls
13
+ - **Optional custom Message-ID**: auto-generated `<uuid@domain>` or specify your own included in output
14
+ - **Attachments**: from binary data with graceful skip if missing
15
+ - **HTML and plain text email support**
14
16
 
15
17
  ## Installation
16
18
 
17
- In n8n: **Settings → Community Nodes → Install** → Enter `n8n-nodes-gmail-custom`
19
+ In n8n: **Settings → Community Nodes → Install** → `n8n-nodes-gmail-custom`
18
20
 
19
21
  ## Usage
20
22
 
21
23
  1. Add a **Gmail Custom** node to your workflow
22
- 2. Select your **Google API** credential (service account)
23
- 3. Fill in:
24
- - **From Email**: the delegated user to send as (e.g. `segreteria.a@domain.com`)
25
- - **To**: recipient(s)
26
- - **Subject**: email subject
27
- - **Email Type**: Text or HTML
28
- - **Message**: email body
29
- 4. Configure advanced Options as needed (CC, BCC, ReplyTo, Attachments)
24
+ 2. Fill in the required fields (supports expressions):
25
+
26
+ | Field | Description | Example |
27
+ |---|---|---|
28
+ | Service Account Email | Your SA `client_email` | `sa-name@project.iam.gserviceaccount.com` |
29
+ | Private Key | RSA private key (PEM, hidden) | Expression from a previous node |
30
+ | From Email | The user to impersonate (DWD) | `sender@your-domain.com` |
31
+ | To | Recipient(s), comma-separated | `recipient@example.com` |
32
+ | Subject | Email subject | `Hello World` |
33
+ | Email Type | `HTML` or `Text` | `HTML` |
34
+ | Message | Email body | Your HTML or text |
35
+
36
+ 3. Configure **Options** as needed:
37
+ - **CC / BCC**: carbon copy recipients
38
+ - **Reply To**: reply-to address
39
+ - **Sender Name**: display name (e.g. `John Doe`)
40
+ - **Attachments**: binary data field names from previous nodes
41
+ - **Custom Message-ID**: enable and optionally specify a `Message-ID` header. Auto-generated as `<uuid@domain>` if left empty. Returned in the node output.
42
+
43
+ ## Output
44
+
45
+ ```json
46
+ {
47
+ "id": "19ef486e4ed521e6",
48
+ "threadId": "19ef486e4ed521e6",
49
+ "labelIds": ["SENT"],
50
+ "messageId": "<generated-uuid@your-domain.com>"
51
+ }
52
+ ```
53
+
54
+ `messageId` is only present when the Custom Message-ID option is enabled.
55
+
56
+ ## How it works
57
+
58
+ 1. Signs a JWT with the service account private key
59
+ 2. Obtains an access token from `oauth2.googleapis.com/token` (cached)
60
+ 3. Builds a MIME email message
61
+ 4. Sends via `POST gmail.googleapis.com/gmail/v1/users/me/messages/send`
62
+
63
+ Per email sent: **1 Gmail API call** (token is cached for 1 hour).
30
64
 
31
65
  ## License
32
66
 
@@ -51,6 +51,10 @@ async function buildMimeMessage(options) {
51
51
  mailOptions.html = options.htmlBody;
52
52
  }
53
53
 
54
+ if (options.messageId) {
55
+ mailOptions.messageId = options.messageId;
56
+ }
57
+
54
58
  if (options.attachments && options.attachments.length > 0) {
55
59
  mailOptions.attachments = options.attachments.map((att) => ({
56
60
  filename: att.fileName || 'attachment',
@@ -89,6 +93,7 @@ async function buildMimeMessage(options) {
89
93
  if (cc) lines.push(`CC: ${cc}`);
90
94
  if (bcc) lines.push(`BCC: ${bcc}`);
91
95
  if (replyTo) lines.push(`Reply-To: ${replyTo}`);
96
+ if (options.messageId) lines.push(`Message-ID: ${options.messageId}`);
92
97
  lines.push(`Subject: ${subject}`);
93
98
  lines.push('MIME-Version: 1.0');
94
99
 
@@ -161,19 +166,19 @@ async function buildMimeMessage(options) {
161
166
  .replace(/=+$/, '');
162
167
  }
163
168
 
164
- async function getOrRefreshAccessToken(ctx, credentials, fromEmail) {
165
- const cacheKey = `${credentials.email}:${fromEmail}`;
169
+ async function getOrRefreshAccessToken(ctx, serviceAccountEmail, privateKeyRaw, fromEmail) {
170
+ const cacheKey = `${serviceAccountEmail}:${fromEmail}`;
166
171
  let token = TOKEN_CACHE[cacheKey];
167
172
  if (token && token.expiresAt > Date.now()) {
168
173
  return token.accessToken;
169
174
  }
170
175
 
171
- const privateKey = formatPrivateKey(credentials.privateKey);
176
+ const privateKey = formatPrivateKey(privateKeyRaw);
172
177
  const now = Math.floor(Date.now() / 1000);
173
178
 
174
179
  let jwt;
175
180
  const payload = {
176
- iss: credentials.email,
181
+ iss: serviceAccountEmail,
177
182
  scope: 'https://mail.google.com/',
178
183
  aud: 'https://oauth2.googleapis.com/token',
179
184
  exp: now + 3600,
@@ -230,13 +235,25 @@ class GmailCustom {
230
235
  },
231
236
  inputs: ['main'],
232
237
  outputs: ['main'],
233
- credentials: [
238
+ properties: [
234
239
  {
235
- name: 'googleApi',
240
+ displayName: 'Service Account Email',
241
+ name: 'serviceAccountEmail',
242
+ type: 'string',
243
+ default: '',
236
244
  required: true,
245
+ placeholder: 'sa-name@project.iam.gserviceaccount.com',
246
+ description: 'The service account client_email (issuer for JWT)',
247
+ },
248
+ {
249
+ displayName: 'Private Key',
250
+ name: 'privateKey',
251
+ type: 'string',
252
+ typeOptions: { password: true },
253
+ default: '',
254
+ required: true,
255
+ description: 'The service account RSA private key in PEM format',
237
256
  },
238
- ],
239
- properties: [
240
257
  {
241
258
  displayName: 'From Email',
242
259
  name: 'fromEmail',
@@ -343,6 +360,33 @@ class GmailCustom {
343
360
  default: '',
344
361
  description: 'The email address that the reply message is sent to',
345
362
  },
363
+ {
364
+ displayName: 'Sender Name',
365
+ name: 'senderName',
366
+ type: 'string',
367
+ default: '',
368
+ placeholder: 'e.g. John Doe',
369
+ description: 'Name shown as the sender in recipients\' inboxes',
370
+ },
371
+ {
372
+ displayName: 'Custom Message-ID',
373
+ name: 'enableMessageId',
374
+ type: 'boolean',
375
+ default: false,
376
+ description: 'Whether to set a custom Message-ID header (auto-generated if not specified)',
377
+ },
378
+ {
379
+ displayName: 'Message-ID Value',
380
+ name: 'customMessageId',
381
+ type: 'string',
382
+ default: '',
383
+ displayOptions: {
384
+ show: {
385
+ enableMessageId: [true],
386
+ },
387
+ },
388
+ description: 'Leave empty to auto-generate a unique Message-ID',
389
+ },
346
390
  ],
347
391
  },
348
392
  ],
@@ -360,6 +404,8 @@ class GmailCustom {
360
404
  for (let i = 0; i < items.length; i++) {
361
405
  try {
362
406
  const item = items[i];
407
+ const serviceAccountEmail = this.getNodeParameter('serviceAccountEmail', i, '');
408
+ const privateKey = this.getNodeParameter('privateKey', i, '');
363
409
  const fromEmail = this.getNodeParameter('fromEmail', i, '');
364
410
  const sendTo = this.getNodeParameter('sendTo', i, '');
365
411
  const subject = this.getNodeParameter('subject', i, '');
@@ -375,6 +421,22 @@ class GmailCustom {
375
421
  let cc = options.ccList || '';
376
422
  let bcc = options.bccList || '';
377
423
  let replyTo = options.replyTo || '';
424
+ const senderName = options.senderName || '';
425
+
426
+ let fromHeader = fromEmail;
427
+ if (senderName) {
428
+ fromHeader = `${senderName} <${fromEmail}>`;
429
+ }
430
+
431
+ let messageId = null;
432
+ if (options.enableMessageId) {
433
+ if (options.customMessageId) {
434
+ messageId = options.customMessageId;
435
+ } else {
436
+ const domain = fromEmail.includes('@') ? fromEmail.split('@')[1] : 'localhost';
437
+ messageId = `<${crypto.randomUUID()}@${domain}>`;
438
+ }
439
+ }
378
440
 
379
441
  let bodyText = message;
380
442
  let bodyHtml = '';
@@ -387,8 +449,7 @@ class GmailCustom {
387
449
  bodyText = message;
388
450
  }
389
451
 
390
- const credentials = await this.getCredentials('googleApi');
391
- const accessToken = await getOrRefreshAccessToken(this, credentials, fromEmail);
452
+ const accessToken = await getOrRefreshAccessToken(this, serviceAccountEmail, privateKey, fromEmail);
392
453
 
393
454
  let attachments = [];
394
455
  if (options.attachmentsUi && options.attachmentsUi.attachmentsBinary) {
@@ -414,7 +475,7 @@ class GmailCustom {
414
475
  }
415
476
 
416
477
  const base64UrlMessage = await buildMimeMessage({
417
- from: fromEmail,
478
+ from: fromHeader,
418
479
  to: sendTo,
419
480
  cc,
420
481
  bcc,
@@ -423,22 +484,27 @@ class GmailCustom {
423
484
  textBody: bodyText,
424
485
  htmlBody: bodyHtml,
425
486
  attachments,
487
+ messageId,
426
488
  });
427
489
 
428
490
  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
- });
491
+ method: 'POST',
492
+ url: 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send',
493
+ headers: {
494
+ Authorization: `Bearer ${accessToken}`,
495
+ 'Content-Type': 'application/json',
496
+ },
497
+ body: { raw: base64UrlMessage },
498
+ });
437
499
 
438
- returnData.push({
439
- json: sendResponse,
440
- pairedItem: { item: i },
441
- });
500
+ const result = { ...sendResponse };
501
+ if (messageId) {
502
+ result.messageId = messageId;
503
+ }
504
+ returnData.push({
505
+ json: result,
506
+ pairedItem: { item: i },
507
+ });
442
508
 
443
509
  } catch (error) {
444
510
  let continueOnFail = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-gmail-custom",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "Custom Gmail node for n8n with token caching and 429 retry",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",