n8n-nodes-gmail-custom 0.4.0 → 0.5.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,28 +1,5 @@
1
1
  const crypto = require('crypto');
2
-
3
- let jwtLib;
4
- try { jwtLib = require('jsonwebtoken'); } catch (e) {}
5
-
6
- const TOKEN_CACHE = {};
7
-
8
- function formatPrivateKey(privateKey) {
9
- if (privateKey.includes('\\n')) {
10
- return privateKey.replace(/\\n/g, '\n');
11
- }
12
- return privateKey;
13
- }
14
-
15
- function base64UrlEncode(str) {
16
- return Buffer.from(str)
17
- .toString('base64')
18
- .replace(/=/g, '')
19
- .replace(/\+/g, '-')
20
- .replace(/\//g, '_');
21
- }
22
-
23
- function base64urlEscape(base64) {
24
- return base64.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
25
- }
2
+ const { getOrRefreshAccessToken } = require('./utils');
26
3
 
27
4
  function generateBoundary() {
28
5
  return '----=_Part_' + crypto.randomBytes(16).toString('hex');
@@ -166,58 +143,6 @@ async function buildMimeMessage(options) {
166
143
  .replace(/=+$/, '');
167
144
  }
168
145
 
169
- async function getOrRefreshAccessToken(ctx, serviceAccountEmail, privateKeyRaw, fromEmail) {
170
- const cacheKey = `${serviceAccountEmail}:${fromEmail}`;
171
- let token = TOKEN_CACHE[cacheKey];
172
- if (token && token.expiresAt > Date.now()) {
173
- return token.accessToken;
174
- }
175
-
176
- const privateKey = formatPrivateKey(privateKeyRaw);
177
- const now = Math.floor(Date.now() / 1000);
178
-
179
- let jwt;
180
- const payload = {
181
- iss: serviceAccountEmail,
182
- scope: 'https://mail.google.com/',
183
- aud: 'https://oauth2.googleapis.com/token',
184
- exp: now + 3600,
185
- iat: now,
186
- sub: fromEmail,
187
- };
188
-
189
- if (jwtLib) {
190
- jwt = jwtLib.sign(payload, privateKey, {
191
- algorithm: 'RS256',
192
- header: { kid: privateKey, typ: 'JWT', alg: 'RS256' },
193
- });
194
- } else {
195
- const header = { alg: 'RS256', typ: 'JWT', kid: privateKey };
196
- const signatureInput = base64UrlEncode(JSON.stringify(header)) + '.' + base64UrlEncode(JSON.stringify(payload));
197
- const signer = crypto.createSign('RSA-SHA256');
198
- signer.update(signatureInput);
199
- const sig = signer.sign(privateKey);
200
- jwt = signatureInput + '.' + base64urlEscape(sig.toString('base64'));
201
- }
202
-
203
- const response = await ctx.helpers.httpRequest({
204
- method: 'POST',
205
- url: 'https://oauth2.googleapis.com/token',
206
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
207
- body: new URLSearchParams({
208
- grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
209
- assertion: jwt,
210
- }).toString(),
211
- });
212
-
213
- TOKEN_CACHE[cacheKey] = {
214
- accessToken: response.access_token,
215
- expiresAt: Date.now() + 3500 * 1000,
216
- };
217
-
218
- return response.access_token;
219
- }
220
-
221
146
  class GmailCustom {
222
147
 
223
148
  constructor() {
@@ -0,0 +1,285 @@
1
+ const { getOrRefreshAccessToken } = require('./utils');
2
+
3
+ class GmailTriggerCustom {
4
+ constructor() {
5
+ this.description = {
6
+ displayName: 'Gmail Trigger Custom',
7
+ name: 'gmailTriggerCustom',
8
+ icon: 'fa:envelope',
9
+ group: ['trigger'],
10
+ version: 1,
11
+ subtitle: 'Gmail Trigger',
12
+ description: 'Fetches emails from Gmail on polling intervals with token caching',
13
+ defaults: { name: 'Gmail Trigger Custom' },
14
+ polling: true,
15
+ inputs: [],
16
+ outputs: ['main'],
17
+ properties: [
18
+ {
19
+ displayName: 'Service Account Email',
20
+ name: 'serviceAccountEmail',
21
+ type: 'string',
22
+ default: '',
23
+ required: true,
24
+ placeholder: 'sa-name@project.iam.gserviceaccount.com',
25
+ description: 'The service account client_email (issuer for JWT)',
26
+ },
27
+ {
28
+ displayName: 'Private Key',
29
+ name: 'privateKey',
30
+ type: 'string',
31
+ typeOptions: { password: true },
32
+ default: '',
33
+ required: true,
34
+ description: 'The service account RSA private key in PEM format',
35
+ },
36
+ {
37
+ displayName: 'Delegated Email',
38
+ name: 'delegatedEmail',
39
+ type: 'string',
40
+ default: '',
41
+ required: true,
42
+ placeholder: 'user@domain.com',
43
+ description: 'The Gmail mailbox to access (user to impersonate via domain-wide delegation)',
44
+ },
45
+ {
46
+ displayName: 'Simplify',
47
+ name: 'simple',
48
+ type: 'boolean',
49
+ default: true,
50
+ description: 'Whether to return a simplified version of the response instead of the raw data',
51
+ },
52
+ {
53
+ displayName: 'Filters',
54
+ name: 'filters',
55
+ type: 'collection',
56
+ placeholder: 'Add Filter',
57
+ default: {},
58
+ options: [
59
+ {
60
+ displayName: 'Include Spam and Trash',
61
+ name: 'includeSpamTrash',
62
+ type: 'boolean',
63
+ default: false,
64
+ description: 'Whether to include messages from SPAM and TRASH in the results',
65
+ },
66
+ {
67
+ displayName: 'Include Drafts',
68
+ name: 'includeDrafts',
69
+ type: 'boolean',
70
+ default: false,
71
+ description: 'Whether to include email drafts in the results',
72
+ },
73
+ {
74
+ displayName: 'Label Names or IDs',
75
+ name: 'labelIds',
76
+ type: 'string',
77
+ default: '',
78
+ description: 'Comma-separated label IDs to filter',
79
+ },
80
+ {
81
+ displayName: 'Search',
82
+ name: 'q',
83
+ type: 'string',
84
+ default: '',
85
+ placeholder: 'has:attachment',
86
+ description: 'Gmail search query. See <a href="https://support.google.com/mail/answer/7190">Gmail search syntax</a>.',
87
+ },
88
+ {
89
+ displayName: 'Read Status',
90
+ name: 'readStatus',
91
+ type: 'options',
92
+ default: 'unread',
93
+ options: [
94
+ { name: 'Unread and read emails', value: 'both' },
95
+ { name: 'Unread emails only', value: 'unread' },
96
+ { name: 'Read emails only', value: 'read' },
97
+ ],
98
+ },
99
+ {
100
+ displayName: 'Sender',
101
+ name: 'sender',
102
+ type: 'string',
103
+ default: '',
104
+ hint: 'Enter an email or part of a sender name',
105
+ },
106
+ ],
107
+ },
108
+ ],
109
+ };
110
+ }
111
+
112
+ async poll() {
113
+ const webhookName = 'default';
114
+ const workflowStaticData = this.getWorkflowStaticData('node') || {};
115
+ if (!workflowStaticData[webhookName]) {
116
+ workflowStaticData[webhookName] = {};
117
+ }
118
+ const nodeStaticData = workflowStaticData[webhookName];
119
+
120
+ const now = Math.floor(Date.now() / 1000);
121
+
122
+ if (this.getMode() !== 'manual') {
123
+ nodeStaticData.lastTimeChecked = nodeStaticData.lastTimeChecked || now;
124
+ }
125
+ const startDate = nodeStaticData.lastTimeChecked || now;
126
+
127
+ const serviceAccountEmail = this.getNodeParameter('serviceAccountEmail', 0) || '';
128
+ const privateKey = this.getNodeParameter('privateKey', 0) || '';
129
+ const delegatedEmail = this.getNodeParameter('delegatedEmail', 0) || '';
130
+ const simple = this.getNodeParameter('simple', 0);
131
+ const filters = this.getNodeParameter('filters', 0, {});
132
+
133
+ const accessToken = await getOrRefreshAccessToken(this, serviceAccountEmail, privateKey, delegatedEmail);
134
+
135
+ const responseData = [];
136
+ const allFetchedMessages = [];
137
+
138
+ const getEmailDateAsSeconds = (email) => {
139
+ let date;
140
+ if (email.internalDate) {
141
+ date = Math.floor(parseInt(email.internalDate, 10) / 1000);
142
+ }
143
+ if (!date || isNaN(date)) {
144
+ return startDate;
145
+ }
146
+ return date;
147
+ };
148
+
149
+ const fetchAndProcessMessage = async (messageId, fetchQs) => {
150
+ const fullMessage = await this.helpers.httpRequest({
151
+ method: 'GET',
152
+ url: `https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}`,
153
+ headers: { Authorization: `Bearer ${accessToken}` },
154
+ qs: fetchQs,
155
+ });
156
+
157
+ allFetchedMessages.push({
158
+ id: fullMessage.id,
159
+ date: getEmailDateAsSeconds(fullMessage),
160
+ });
161
+
162
+ if (!filters.includeDrafts && fullMessage.labelIds && fullMessage.labelIds.includes('DRAFT')) {
163
+ return;
164
+ }
165
+ if (fullMessage.labelIds && fullMessage.labelIds.includes('SENT') && !fullMessage.labelIds.includes('INBOX')) {
166
+ return;
167
+ }
168
+
169
+ if (simple) {
170
+ responseData.push({ json: fullMessage });
171
+ } else {
172
+ responseData.push({ json: fullMessage });
173
+ }
174
+ };
175
+
176
+ try {
177
+ const qs = {};
178
+ const parts = [];
179
+
180
+ if (!simple) {
181
+ qs.format = 'raw';
182
+ } else {
183
+ qs.format = 'metadata';
184
+ qs.metadataHeaders = ['From', 'To', 'Cc', 'Bcc', 'Subject'];
185
+ }
186
+
187
+ if (this.getMode() !== 'manual') {
188
+ parts.push(`after:${startDate}`);
189
+ }
190
+
191
+ if (filters.includeSpamTrash) {
192
+ parts.push('in:anywhere');
193
+ } else {
194
+ parts.push('-in:spam -in:trash');
195
+ }
196
+
197
+ if (filters.readStatus === 'unread') {
198
+ parts.push('is:unread');
199
+ } else if (filters.readStatus === 'read') {
200
+ parts.push('is:read');
201
+ }
202
+
203
+ if (filters.sender) {
204
+ parts.push(`from:${filters.sender}`);
205
+ }
206
+
207
+ if (filters.labelIds) {
208
+ for (const labelId of filters.labelIds.split(',')) {
209
+ parts.push(`label:${labelId.trim()}`);
210
+ }
211
+ }
212
+
213
+ if (filters.q) {
214
+ parts.push(filters.q);
215
+ }
216
+
217
+ parts.push('-in:scheduled');
218
+
219
+ qs.q = parts.join(' ');
220
+
221
+ if (this.getMode() === 'manual') {
222
+ qs.maxResults = 1;
223
+ }
224
+
225
+ const messagesResponse = await this.helpers.httpRequest({
226
+ method: 'GET',
227
+ url: 'https://gmail.googleapis.com/gmail/v1/users/me/messages',
228
+ headers: { Authorization: `Bearer ${accessToken}` },
229
+ qs,
230
+ });
231
+
232
+ const messages = messagesResponse.messages || [];
233
+
234
+ if (!messages.length && !allFetchedMessages.length) {
235
+ return null;
236
+ }
237
+
238
+ const possibleDuplicates = new Set(nodeStaticData.possibleDuplicates || []);
239
+ const filteredMessages = possibleDuplicates.size > 0
240
+ ? messages.filter((m) => !possibleDuplicates.has(m.id))
241
+ : messages;
242
+
243
+ if (!filteredMessages.length && !allFetchedMessages.length) {
244
+ return null;
245
+ }
246
+
247
+ const fetchQs = {};
248
+ if (!simple) {
249
+ fetchQs.format = 'raw';
250
+ } else {
251
+ fetchQs.format = 'metadata';
252
+ fetchQs.metadataHeaders = ['From', 'To', 'Cc', 'Bcc', 'Subject'];
253
+ }
254
+
255
+ for (const message of filteredMessages) {
256
+ await fetchAndProcessMessage(message.id, fetchQs);
257
+ }
258
+ } catch (error) {
259
+ if (this.getMode() === 'manual' || !nodeStaticData.lastTimeChecked) {
260
+ throw error;
261
+ }
262
+ try {
263
+ this.logger.error(`Gmail Trigger Custom poll error: ${error.message}`);
264
+ } catch (e) {}
265
+ }
266
+
267
+ if (!allFetchedMessages.length) {
268
+ return null;
269
+ }
270
+
271
+ const lastEmailDate = allFetchedMessages.reduce(
272
+ (last, m) => (m.date > last ? m.date : last), 0,
273
+ );
274
+
275
+ nodeStaticData.possibleDuplicates = allFetchedMessages.map((m) => m.id);
276
+ nodeStaticData.lastTimeChecked = Math.max(lastEmailDate || startDate, startDate);
277
+
278
+ return responseData.length > 0 ? [responseData] : null;
279
+ }
280
+ }
281
+
282
+ module.exports = {
283
+ nodeTypes: [GmailTriggerCustom],
284
+ GmailTriggerCustom,
285
+ };
@@ -0,0 +1,90 @@
1
+ const crypto = require('crypto');
2
+
3
+ let jwtLib;
4
+ try { jwtLib = require('jsonwebtoken'); } catch (e) {}
5
+
6
+ const TOKEN_CACHE = {};
7
+
8
+ function formatPrivateKey(privateKey) {
9
+ if (privateKey.includes('\\n')) {
10
+ return privateKey.replace(/\\n/g, '\n');
11
+ }
12
+ return privateKey;
13
+ }
14
+
15
+ function base64UrlEncode(str) {
16
+ return Buffer.from(str)
17
+ .toString('base64')
18
+ .replace(/=/g, '')
19
+ .replace(/\+/g, '-')
20
+ .replace(/\//g, '_');
21
+ }
22
+
23
+ function base64urlEscape(base64) {
24
+ return base64.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
25
+ }
26
+
27
+ async function getOrRefreshAccessToken(ctx, serviceAccountEmail, privateKeyRaw, delegatedEmail) {
28
+ const cacheKey = `${serviceAccountEmail}:${delegatedEmail}`;
29
+ let token = TOKEN_CACHE[cacheKey];
30
+ if (token && token.expiresAt > Date.now()) {
31
+ return token.accessToken;
32
+ }
33
+
34
+ const privateKey = formatPrivateKey(privateKeyRaw);
35
+ const now = Math.floor(Date.now() / 1000);
36
+
37
+ let jwt;
38
+ const payload = {
39
+ iss: serviceAccountEmail,
40
+ scope: 'https://mail.google.com/',
41
+ aud: 'https://oauth2.googleapis.com/token',
42
+ exp: now + 3600,
43
+ iat: now,
44
+ sub: delegatedEmail,
45
+ };
46
+
47
+ if (jwtLib) {
48
+ jwt = jwtLib.sign(payload, privateKey, {
49
+ algorithm: 'RS256',
50
+ header: { kid: privateKey, typ: 'JWT', alg: 'RS256' },
51
+ });
52
+ } else {
53
+ const header = { alg: 'RS256', typ: 'JWT', kid: privateKey };
54
+ const signatureInput = base64UrlEncode(JSON.stringify(header)) + '.' + base64UrlEncode(JSON.stringify(payload));
55
+ const signer = crypto.createSign('RSA-SHA256');
56
+ signer.update(signatureInput);
57
+ const sig = signer.sign(privateKey);
58
+ jwt = signatureInput + '.' + base64urlEscape(sig.toString('base64'));
59
+ }
60
+
61
+ const response = await ctx.helpers.httpRequest({
62
+ method: 'POST',
63
+ url: 'https://oauth2.googleapis.com/token',
64
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
65
+ body: new URLSearchParams({
66
+ grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
67
+ assertion: jwt,
68
+ }).toString(),
69
+ });
70
+
71
+ TOKEN_CACHE[cacheKey] = {
72
+ accessToken: response.access_token,
73
+ expiresAt: Date.now() + 3500 * 1000,
74
+ };
75
+
76
+ return response.access_token;
77
+ }
78
+
79
+ function clearTokenCache(serviceAccountEmail, delegatedEmail) {
80
+ delete TOKEN_CACHE[`${serviceAccountEmail}:${delegatedEmail}`];
81
+ }
82
+
83
+ module.exports = {
84
+ formatPrivateKey,
85
+ base64UrlEncode,
86
+ base64urlEscape,
87
+ getOrRefreshAccessToken,
88
+ clearTokenCache,
89
+ TOKEN_CACHE,
90
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "n8n-nodes-gmail-custom",
3
- "version": "0.4.0",
4
- "description": "Custom Gmail node for n8n with token caching and 429 retry",
3
+ "version": "0.5.0",
4
+ "description": "Custom Gmail nodes for n8n with token caching — Send and Trigger, self-contained credentials",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",
7
7
  "n8n",
@@ -19,7 +19,8 @@
19
19
  "n8nNodesApiVersion": 1,
20
20
  "credentials": [],
21
21
  "nodes": [
22
- "nodes/GmailCustom/GmailCustom.node.js"
22
+ "nodes/GmailCustom/GmailCustom.node.js",
23
+ "nodes/GmailCustom/GmailTriggerCustom.node.js"
23
24
  ]
24
25
  }
25
26
  }