n8n-nodes-gmail-custom 0.5.0 → 0.6.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.
@@ -0,0 +1,238 @@
1
+ const { getOrRefreshAccessToken } = require('./utils');
2
+
3
+ class GmailGetCustom {
4
+ constructor() {
5
+ this.description = {
6
+ displayName: 'Gmail Get Custom',
7
+ name: 'gmailGetCustom',
8
+ icon: 'fa:envelope',
9
+ group: ['output'],
10
+ version: 1,
11
+ subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}',
12
+ description: 'Get a Message or Thread from Gmail with token caching',
13
+ defaults: {
14
+ name: 'Gmail Get Custom',
15
+ color: '#1a73e8',
16
+ },
17
+ inputs: ['main'],
18
+ outputs: ['main'],
19
+ properties: [
20
+ {
21
+ displayName: 'Service Account Email',
22
+ name: 'serviceAccountEmail',
23
+ type: 'string',
24
+ default: '',
25
+ required: true,
26
+ placeholder: 'sa-name@project.iam.gserviceaccount.com',
27
+ },
28
+ {
29
+ displayName: 'Private Key',
30
+ name: 'privateKey',
31
+ type: 'string',
32
+ typeOptions: { password: true },
33
+ default: '',
34
+ required: true,
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)',
44
+ },
45
+ {
46
+ displayName: 'Resource',
47
+ name: 'resource',
48
+ type: 'options',
49
+ noDataExpression: true,
50
+ options: [
51
+ { name: 'Message', value: 'message' },
52
+ { name: 'Thread', value: 'thread' },
53
+ ],
54
+ default: 'message',
55
+ },
56
+ {
57
+ displayName: 'Operation',
58
+ name: 'operation',
59
+ type: 'options',
60
+ noDataExpression: true,
61
+ options: [{ name: 'Get', value: 'get' }],
62
+ default: 'get',
63
+ displayOptions: {
64
+ show: { resource: ['message'] },
65
+ },
66
+ },
67
+ {
68
+ displayName: 'Operation',
69
+ name: 'operation',
70
+ type: 'options',
71
+ noDataExpression: true,
72
+ options: [{ name: 'Get', value: 'get' }],
73
+ default: 'get',
74
+ displayOptions: {
75
+ show: { resource: ['thread'] },
76
+ },
77
+ },
78
+ {
79
+ displayName: 'Message ID',
80
+ name: 'messageId',
81
+ type: 'string',
82
+ default: '',
83
+ required: true,
84
+ displayOptions: {
85
+ show: { resource: ['message'] },
86
+ },
87
+ placeholder: '172ce2c4a72cc243',
88
+ },
89
+ {
90
+ displayName: 'Thread ID',
91
+ name: 'threadId',
92
+ type: 'string',
93
+ default: '',
94
+ required: true,
95
+ displayOptions: {
96
+ show: { resource: ['thread'] },
97
+ },
98
+ placeholder: '172ce2c4a72cc243',
99
+ },
100
+ {
101
+ displayName: 'Simplify',
102
+ name: 'simple',
103
+ type: 'boolean',
104
+ default: true,
105
+ displayOptions: {
106
+ show: { resource: ['message', 'thread'] },
107
+ },
108
+ description: 'Whether to return a simplified version (metadata headers only) instead of raw data',
109
+ },
110
+ {
111
+ displayName: 'Options',
112
+ name: 'options',
113
+ type: 'collection',
114
+ placeholder: 'Add option',
115
+ default: {},
116
+ displayOptions: {
117
+ show: { resource: ['thread'] },
118
+ },
119
+ options: [
120
+ {
121
+ displayName: 'Return Only Messages',
122
+ name: 'returnOnlyMessages',
123
+ type: 'boolean',
124
+ default: false,
125
+ description: 'Whether to return only the messages array without thread metadata',
126
+ },
127
+ ],
128
+ },
129
+ ],
130
+ };
131
+ }
132
+
133
+ async execute() {
134
+ const items = this.getInputData();
135
+ if (!items || items.length === 0) {
136
+ return [[]];
137
+ }
138
+
139
+ const returnData = [];
140
+
141
+ for (let i = 0; i < items.length; i++) {
142
+ try {
143
+ const serviceAccountEmail = this.getNodeParameter('serviceAccountEmail', i, '');
144
+ const privateKey = this.getNodeParameter('privateKey', i, '');
145
+ const delegatedEmail = this.getNodeParameter('delegatedEmail', i, '');
146
+ const resource = this.getNodeParameter('resource', i, 'message');
147
+ const simple = this.getNodeParameter('simple', i, true);
148
+ const options = this.getNodeParameter('options', i, {});
149
+
150
+ const accessToken = await getOrRefreshAccessToken(this, serviceAccountEmail, privateKey, delegatedEmail);
151
+
152
+ const qs = {};
153
+ if (simple) {
154
+ qs.format = 'metadata';
155
+ qs.metadataHeaders = ['From', 'To', 'Cc', 'Bcc', 'Subject'];
156
+ } else {
157
+ qs.format = resource === 'thread' ? 'full' : 'raw';
158
+ }
159
+
160
+ let url;
161
+ if (resource === 'message') {
162
+ const messageId = this.getNodeParameter('messageId', i, '');
163
+ if (!messageId) throw new Error('Message ID is required');
164
+ url = `https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}`;
165
+ } else {
166
+ const threadId = this.getNodeParameter('threadId', i, '');
167
+ if (!threadId) throw new Error('Thread ID is required');
168
+ url = `https://gmail.googleapis.com/gmail/v1/users/me/threads/${threadId}`;
169
+ }
170
+
171
+ const response = await this.helpers.httpRequest({
172
+ method: 'GET',
173
+ url,
174
+ headers: { Authorization: `Bearer ${accessToken}` },
175
+ qs,
176
+ });
177
+
178
+ let result;
179
+ if (resource === 'thread') {
180
+ if (simple) {
181
+ if (response.messages) {
182
+ response.messages = response.messages.map((msg) => {
183
+ const flat = { ...msg };
184
+ if (flat.payload && flat.payload.headers) {
185
+ for (const h of flat.payload.headers) {
186
+ flat[h.name] = h.value;
187
+ }
188
+ }
189
+ return flat;
190
+ });
191
+ }
192
+ }
193
+
194
+ if (options.returnOnlyMessages) {
195
+ result = response.messages || [];
196
+ } else {
197
+ result = { json: response };
198
+ }
199
+ } else {
200
+ // Message
201
+ if (simple && response.payload && response.payload.headers) {
202
+ for (const h of response.payload.headers) {
203
+ response[h.name] = h.value;
204
+ }
205
+ }
206
+ result = { json: response };
207
+ }
208
+
209
+ const isArray = Array.isArray(result);
210
+ if (isArray) {
211
+ for (const item of result) {
212
+ returnData.push({ json: item, pairedItem: { item: i } });
213
+ }
214
+ } else {
215
+ returnData.push({ ...result, pairedItem: { item: i } });
216
+ }
217
+ } catch (error) {
218
+ let continueOnFail = false;
219
+ try { continueOnFail = this.continueOnFail(); } catch (e) {}
220
+ if (continueOnFail) {
221
+ returnData.push({
222
+ json: { error: error.message || 'Unknown error' },
223
+ pairedItem: { item: i },
224
+ });
225
+ continue;
226
+ }
227
+ throw error;
228
+ }
229
+ }
230
+
231
+ return [returnData];
232
+ }
233
+ }
234
+
235
+ module.exports = {
236
+ nodeTypes: [GmailGetCustom],
237
+ GmailGetCustom,
238
+ };
@@ -49,6 +49,24 @@ class GmailTriggerCustom {
49
49
  default: true,
50
50
  description: 'Whether to return a simplified version of the response instead of the raw data',
51
51
  },
52
+ {
53
+ displayName: 'Max Emails per Poll',
54
+ name: 'maxResults',
55
+ type: 'number',
56
+ default: 10,
57
+ typeOptions: {
58
+ minValue: 1,
59
+ maxValue: 50,
60
+ },
61
+ description: 'Maximum number of emails to fetch each time the node polls. Remaining emails are picked up in subsequent polls.',
62
+ },
63
+ {
64
+ displayName: 'Mark as Read',
65
+ name: 'markAsRead',
66
+ type: 'boolean',
67
+ default: false,
68
+ description: 'Whether to mark processed emails as read (adds 1 API call per email)',
69
+ },
52
70
  {
53
71
  displayName: 'Filters',
54
72
  name: 'filters',
@@ -128,6 +146,8 @@ class GmailTriggerCustom {
128
146
  const privateKey = this.getNodeParameter('privateKey', 0) || '';
129
147
  const delegatedEmail = this.getNodeParameter('delegatedEmail', 0) || '';
130
148
  const simple = this.getNodeParameter('simple', 0);
149
+ let maxResults = this.getNodeParameter('maxResults', 0) || 10;
150
+ const markAsRead = this.getNodeParameter('markAsRead', 0);
131
151
  const filters = this.getNodeParameter('filters', 0, {});
132
152
 
133
153
  const accessToken = await getOrRefreshAccessToken(this, serviceAccountEmail, privateKey, delegatedEmail);
@@ -166,11 +186,18 @@ class GmailTriggerCustom {
166
186
  return;
167
187
  }
168
188
 
169
- if (simple) {
170
- responseData.push({ json: fullMessage });
171
- } else {
172
- responseData.push({ json: fullMessage });
189
+ if (markAsRead) {
190
+ try {
191
+ await this.helpers.httpRequest({
192
+ method: 'POST',
193
+ url: `https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}/modify`,
194
+ headers: { Authorization: `Bearer ${accessToken}` },
195
+ body: { removeLabelIds: ['UNREAD'] },
196
+ });
197
+ } catch (e) {}
173
198
  }
199
+
200
+ responseData.push({ json: fullMessage });
174
201
  };
175
202
 
176
203
  try {
@@ -219,41 +246,58 @@ class GmailTriggerCustom {
219
246
  qs.q = parts.join(' ');
220
247
 
221
248
  if (this.getMode() === 'manual') {
222
- qs.maxResults = 1;
249
+ maxResults = 1;
223
250
  }
224
251
 
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
- });
252
+ let budget = maxResults;
231
253
 
232
- const messages = messagesResponse.messages || [];
254
+ // Process pending messages from previous poll first.
255
+ const pendingIds = nodeStaticData.pendingMessageIds || [];
256
+ if (pendingIds.length > 0) {
257
+ const idsToFetch = pendingIds.slice(0, budget);
258
+ nodeStaticData.pendingMessageIds = pendingIds.slice(budget);
233
259
 
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;
260
+ const fetchQs = {};
261
+ if (!simple) { fetchQs.format = 'raw'; }
262
+ else { fetchQs.format = 'metadata'; fetchQs.metadataHeaders = ['From', 'To', 'Cc', 'Bcc', 'Subject']; }
242
263
 
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'];
264
+ for (const id of idsToFetch) {
265
+ await fetchAndProcessMessage(id, fetchQs);
266
+ }
267
+ budget -= idsToFetch.length;
253
268
  }
254
269
 
255
- for (const message of filteredMessages) {
256
- await fetchAndProcessMessage(message.id, fetchQs);
270
+ // Only list new messages if budget remains.
271
+ if (budget > 0) {
272
+ const messagesResponse = await this.helpers.httpRequest({
273
+ method: 'GET',
274
+ url: 'https://gmail.googleapis.com/gmail/v1/users/me/messages',
275
+ headers: { Authorization: `Bearer ${accessToken}` },
276
+ qs,
277
+ });
278
+
279
+ const messages = messagesResponse.messages || [];
280
+
281
+ if (messages.length > 0) {
282
+ const possibleDuplicates = new Set(nodeStaticData.possibleDuplicates || []);
283
+ const filteredMessages = possibleDuplicates.size > 0
284
+ ? messages.filter((m) => !possibleDuplicates.has(m.id))
285
+ : messages;
286
+
287
+ let messagesToProcess = filteredMessages;
288
+ if (filteredMessages.length > budget) {
289
+ messagesToProcess = filteredMessages.slice(0, budget);
290
+ nodeStaticData.pendingMessageIds = filteredMessages.slice(budget).map((m) => m.id);
291
+ }
292
+
293
+ const fetchQs = {};
294
+ if (!simple) { fetchQs.format = 'raw'; }
295
+ else { fetchQs.format = 'metadata'; fetchQs.metadataHeaders = ['From', 'To', 'Cc', 'Bcc', 'Subject']; }
296
+
297
+ for (const message of messagesToProcess) {
298
+ await fetchAndProcessMessage(message.id, fetchQs);
299
+ }
300
+ }
257
301
  }
258
302
  } catch (error) {
259
303
  if (this.getMode() === 'manual' || !nodeStaticData.lastTimeChecked) {
@@ -273,7 +317,9 @@ class GmailTriggerCustom {
273
317
  );
274
318
 
275
319
  nodeStaticData.possibleDuplicates = allFetchedMessages.map((m) => m.id);
276
- nodeStaticData.lastTimeChecked = Math.max(lastEmailDate || startDate, startDate);
320
+ if (!nodeStaticData.pendingMessageIds || nodeStaticData.pendingMessageIds.length === 0) {
321
+ nodeStaticData.lastTimeChecked = Math.max(lastEmailDate || startDate, startDate);
322
+ }
277
323
 
278
324
  return responseData.length > 0 ? [responseData] : null;
279
325
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "n8n-nodes-gmail-custom",
3
- "version": "0.5.0",
4
- "description": "Custom Gmail nodes for n8n with token cachingSend and Trigger, self-contained credentials",
3
+ "version": "0.6.0",
4
+ "description": "Custom Gmail nodes for n8n Send, Trigger, Get (Message+Thread) token caching, self-contained",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",
7
7
  "n8n",
@@ -20,7 +20,8 @@
20
20
  "credentials": [],
21
21
  "nodes": [
22
22
  "nodes/GmailCustom/GmailCustom.node.js",
23
- "nodes/GmailCustom/GmailTriggerCustom.node.js"
23
+ "nodes/GmailCustom/GmailTriggerCustom.node.js",
24
+ "nodes/GmailCustom/GmailGetCustom.node.js"
24
25
  ]
25
26
  }
26
27
  }