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 +50 -16
- package/nodes/GmailCustom/GmailCustom.node.js +89 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,32 +1,66 @@
|
|
|
1
1
|
# n8n-nodes-gmail-custom
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Self-contained n8n node for sending emails via Gmail API with a Google service account and domain-wide delegation.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
No credential setup in n8n required — all parameters are inline, supports expressions.
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
- **Token caching
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
-
|
|
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** →
|
|
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.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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,
|
|
165
|
-
const cacheKey = `${
|
|
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(
|
|
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:
|
|
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
|
-
|
|
238
|
+
properties: [
|
|
234
239
|
{
|
|
235
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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;
|