@5minds/node-red-contrib-processcube-tools 1.0.1-feature-050c1a-mfe18hnk → 1.0.1-feature-f506be-mfe3agh6

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,236 +0,0 @@
1
- module.exports = function(RED) {
2
- const Imap = require('node-imap');
3
- const mailparser = require('mailparser');
4
-
5
- function EmailReceiverNode(config) {
6
- RED.nodes.createNode(this, config);
7
- const node = this;
8
-
9
- node.on('input', function(msg) {
10
- // Retrieve and validate configuration values
11
- const imap_host = RED.util.evaluateNodeProperty(config.host, config.hostType, node, msg);
12
- const imap_port = RED.util.evaluateNodeProperty(config.port, config.portType, node, msg);
13
- const imap_tls = RED.util.evaluateNodeProperty(config.tls, config.tlsType, node, msg);
14
- const imap_user = RED.util.evaluateNodeProperty(config.user, config.userType, node, msg);
15
- const imap_password = RED.util.evaluateNodeProperty(config.password, config.passwordType, node, msg);
16
-
17
- // Check if the folder is actually an array
18
- const imap_folder = RED.util.evaluateNodeProperty(config.folder, config.folderType, node, msg);
19
- let folders;
20
- if (Array.isArray(imap_folder)) {
21
- folders = imap_folder;
22
- } else {
23
- const errorMsg = "The 'folders' property must be an array of strings.";
24
- node.status({ fill: 'red', shape: 'ring', text: errorMsg });
25
- node.error(errorMsg, msg);
26
- return;
27
- }
28
- const imap_markSeen = RED.util.evaluateNodeProperty(config.markseen, config.markseenType, node, msg);
29
-
30
- const finalConfig = {
31
- host: imap_host,
32
- port: (typeof imap_port === 'string') ? parseInt(imap_port, 10) : imap_port,
33
- tls: imap_tls,
34
- user: imap_user,
35
- password: imap_password,
36
- folders: (Array.isArray(imap_folder)) ? imap_folder : imap_folder.split(',').map(f => f.trim()).filter(f => f.length > 0),
37
- markSeen: imap_markSeen,
38
- connTimeout: msg.imap_connTimeout || 10000,
39
- authTimeout: msg.imap_authTimeout || 5000,
40
- keepalive: msg.imap_keepalive !== undefined ? msg.imap_keepalive : true,
41
- autotls: msg.imap_autotls || 'never',
42
- tlsOptions: msg.imap_tlsOptions || { rejectUnauthorized: false }
43
- };
44
-
45
- if (!finalConfig.user || !finalConfig.password || !finalConfig.port || !finalConfig.host || !finalConfig.folders) {
46
- const missingFields = [];
47
- if (!finalConfig.user) missingFields.push('user');
48
- if (!finalConfig.password) missingFields.push('password');
49
- if (!finalConfig.port) missingFields.push('port');
50
- if (!finalConfig.host) missingFields.push('host');
51
- if (!finalConfig.folders) missingFields.push('folders');
52
-
53
- const errorMessage = `Missing required IMAP config: ${missingFields.join(', ')}. Aborting.`;
54
- node.status({ fill: 'red', shape: 'ring', text: 'missing config' });
55
- node.error(errorMessage);
56
- return;
57
- }
58
-
59
- const fetchEmails = ({
60
- host,
61
- port,
62
- tls,
63
- user,
64
- password,
65
- folders,
66
- markSeen = true,
67
- connTimeout = 10000,
68
- authTimeout = 5000,
69
- keepalive = true,
70
- autotls = 'never',
71
- tlsOptions = { rejectUnauthorized: false }
72
- }, onMail) => {
73
- const imap = new Imap({
74
- user,
75
- password,
76
- host,
77
- port,
78
- tls,
79
- connTimeout,
80
- authTimeout,
81
- keepalive,
82
- autotls,
83
- tlsOptions
84
- });
85
-
86
- const state = {
87
- totalFolders: folders.length,
88
- processedFolders: 0,
89
- successes: 0,
90
- failures: 0,
91
- totalMails: 0,
92
- errors: [],
93
- };
94
-
95
- // Helper to update Node-RED status
96
- const updateStatus = (color, text) => {
97
- node.status({ fill: color, shape: 'dot', text });
98
- };
99
-
100
- // Helper to finalize status and clean up
101
- const finalizeSession = (error = null) => {
102
- if (error) {
103
- node.error('IMAP session terminated: ' + error.message);
104
- node.status({ fill: 'red', shape: 'ring', text: 'connection error' });
105
- } else if (state.failures > 0) {
106
- node.status({
107
- fill: 'red',
108
- shape: 'dot',
109
- text: `Done, ${state.totalMails} mails from ${state.successes}/${state.totalFolders} folders. ${state.failures} failed.`
110
- });
111
- } else {
112
- node.status({
113
- fill: 'green',
114
- shape: 'dot',
115
- text: `Done, fetched ${state.totalMails} mails from ${folders.join(', ')}.`
116
- });
117
- }
118
- if (imap && imap.state !== 'disconnected') {
119
- imap.end();
120
- }
121
- };
122
-
123
- const fetchFromFolder = (folder) => {
124
- updateStatus('yellow', `Fetching from "${folder}"...`);
125
-
126
- imap.openBox(folder, false, (err, box) => {
127
- if (err) {
128
- node.error(`Could not open folder "${folder}": ${err.message}`);
129
- state.failures++;
130
- state.processedFolders++;
131
- return startNextFolder();
132
- }
133
-
134
- imap.search(['UNSEEN'], (err, results) => {
135
- if (err) {
136
- node.error(`Search failed in folder "${folder}": ${err.message}`);
137
- state.failures++;
138
- state.processedFolders++;
139
- return startNextFolder();
140
- }
141
-
142
- if (!results || !results.length) {
143
- state.successes++;
144
- state.processedFolders++;
145
- return startNextFolder();
146
- }
147
-
148
- state.totalMails += results.length;
149
-
150
- const fetch = imap.fetch(results, { bodies: '' });
151
-
152
- fetch.on('message', msg => {
153
- msg.on('body', stream => {
154
- mailparser.simpleParser(stream, (err, parsed) => {
155
- if (err) {
156
- node.error(`Parse error for email from folder "${folder}": ${err.message}`);
157
- return;
158
- }
159
-
160
- const outMsg = {
161
- topic: parsed.subject,
162
- payload: parsed.text,
163
- html: parsed.html,
164
- from: parsed.replyTo?.text || parsed.from?.text,
165
- date: parsed.date,
166
- folder,
167
- header: parsed.headers,
168
- attachments: parsed.attachments.map(att => ({
169
- contentType: att.contentType,
170
- fileName: att.filename,
171
- transferEncoding: att.transferEncoding,
172
- contentDisposition: att.contentDisposition,
173
- generatedFileName: att.cid || att.checksum,
174
- contentId: att.cid,
175
- checksum: att.checksum,
176
- length: att.size,
177
- content: att.content
178
- }))
179
- };
180
- onMail(outMsg);
181
- });
182
- });
183
- });
184
-
185
- fetch.once('error', err => {
186
- node.error(`Fetch error in folder "${folder}": ${err.message}`);
187
- });
188
-
189
- fetch.once('end', () => {
190
- state.successes++;
191
- state.processedFolders++;
192
- updateStatus('green', `Fetched ${results.length} from "${folder}".`);
193
- startNextFolder();
194
- });
195
- });
196
- });
197
- };
198
-
199
- const startNextFolder = () => {
200
- if (state.processedFolders >= state.totalFolders) {
201
- finalizeSession();
202
- } else {
203
- fetchFromFolder(folders[state.processedFolders]);
204
- }
205
- };
206
-
207
- // Centralized event listeners for the IMAP connection
208
- imap.once('ready', () => {
209
- node.status({ fill: 'green', shape: 'dot', text: 'connected' });
210
- startNextFolder();
211
- });
212
-
213
- imap.once('error', err => {
214
- finalizeSession(err);
215
- });
216
-
217
- imap.once('end', () => {
218
- node.log('IMAP connection ended.');
219
- });
220
-
221
- updateStatus('yellow', 'Connecting to IMAP...');
222
- imap.connect();
223
- };
224
-
225
- fetchEmails(finalConfig, mail => {
226
- node.send(mail);
227
- });
228
- });
229
- }
230
-
231
- RED.nodes.registerType("email-receiver", EmailReceiverNode, {
232
- credentials: {
233
- password: { type: "password" }
234
- }
235
- });
236
- };
@@ -1,379 +0,0 @@
1
- /**
2
- * Shared mock objects and utilities for Email Receiver Node tests
3
- */
4
-
5
- /**
6
- * Mock IMAP implementation for testing
7
- */
8
- function createMockImap() {
9
- return function MockImap(config) {
10
- this.config = config;
11
- this.events = {};
12
-
13
- // Simulate connection behavior
14
- this.connect = () => {
15
- // Simulate successful connection by emitting 'ready' event
16
- if (this.events && this.events.ready) {
17
- // Use setTimeout to simulate async behavior
18
- setTimeout(() => this.events.ready(), 10);
19
- }
20
- };
21
-
22
- // Simulate opening a mailbox
23
- this.openBox = (folder, readOnly, callback) => {
24
- setTimeout(() => {
25
- callback(null, {
26
- messages: { total: 1 },
27
- name: folder,
28
- readOnly: readOnly
29
- });
30
- }, 10);
31
- };
32
-
33
- // Simulate searching for emails
34
- this.search = (criteria, callback) => {
35
- setTimeout(() => {
36
- // Return mock message IDs
37
- callback(null, [123, 456, 789]);
38
- }, 10);
39
- };
40
-
41
- // Simulate fetching email messages
42
- this.fetch = (results, options) => {
43
- return {
44
- on: (event, callback) => {
45
- if (event === 'message') {
46
- setTimeout(() => {
47
- const mockMessage = {
48
- on: (messageEvent, messageCallback) => {
49
- if (messageEvent === 'body') {
50
- setTimeout(() => {
51
- messageCallback(Buffer.from('mock email body'));
52
- }, 5);
53
- } else if (messageEvent === 'attributes') {
54
- setTimeout(() => {
55
- messageCallback({
56
- uid: 123,
57
- flags: ['\\Seen'],
58
- date: new Date(),
59
- size: 1024
60
- });
61
- }, 5);
62
- }
63
- },
64
- once: (messageEvent, messageCallback) => {
65
- if (messageEvent === 'end') {
66
- setTimeout(() => messageCallback(), 15);
67
- }
68
- }
69
- };
70
- callback(mockMessage);
71
- }, 10);
72
- }
73
- },
74
- once: (event, callback) => {
75
- if (event === 'end') {
76
- setTimeout(() => callback(), 20);
77
- } else if (event === 'error') {
78
- // Store error callback for potential use
79
- this.errorCallback = callback;
80
- }
81
- }
82
- };
83
- };
84
-
85
- // Simulate closing connection
86
- this.end = () => {
87
- if (this.events && this.events.end) {
88
- setTimeout(() => this.events.end(), 5);
89
- }
90
- };
91
-
92
- // Event listener setup
93
- this.once = (event, callback) => {
94
- if (!this.events) this.events = {};
95
- this.events[event] = callback;
96
- };
97
-
98
- // Additional IMAP methods that might be used
99
- this.addFlags = (source, flags, callback) => {
100
- setTimeout(() => callback(null), 5);
101
- };
102
-
103
- this.removeFlags = (source, flags, callback) => {
104
- setTimeout(() => callback(null), 5);
105
- };
106
-
107
- return this;
108
- };
109
- }
110
-
111
- /**
112
- * Mock Mailparser implementation for testing
113
- */
114
- function createMockMailparser() {
115
- return {
116
- simpleParser: function(source, options = {}) {
117
- return Promise.resolve({
118
- subject: options.subject || 'Mock Email Subject',
119
- text: options.text || 'This is a mock email body for testing purposes.',
120
- html: options.html || '<p>This is a mock email body for testing purposes.</p>',
121
- from: {
122
- text: options.from || 'sender@test.com',
123
- value: [{ address: options.from || 'sender@test.com', name: 'Test Sender' }]
124
- },
125
- to: {
126
- text: options.to || 'recipient@test.com',
127
- value: [{ address: options.to || 'recipient@test.com', name: 'Test Recipient' }]
128
- },
129
- date: options.date || new Date(),
130
- messageId: options.messageId || '<mock-message-id@test.com>',
131
- headers: new Map([
132
- ['message-id', '<mock-message-id@test.com>'],
133
- ['subject', options.subject || 'Mock Email Subject'],
134
- ['from', options.from || 'sender@test.com'],
135
- ['to', options.to || 'recipient@test.com']
136
- ]),
137
- attachments: options.attachments || []
138
- });
139
- }
140
- };
141
- }
142
-
143
- /**
144
- * Create mock Node-RED object for unit testing
145
- */
146
- function createMockNodeRED(options = {}) {
147
- return {
148
- nodes: {
149
- createNode: function(node, config) {
150
- // Apply config properties to node
151
- Object.assign(node, {
152
- id: config.id || 'mock-node-id',
153
- type: config.type || 'email-receiver',
154
- name: config.name || 'Mock Node',
155
- on: options.onHandler || function() {},
156
- status: options.statusHandler || function() {},
157
- error: options.errorHandler || function() {},
158
- send: options.sendHandler || function() {},
159
- log: options.logHandler || function() {},
160
- warn: options.warnHandler || function() {},
161
- debug: options.debugHandler || function() {}
162
- });
163
- return node;
164
- },
165
- registerType: function(type, constructor) {
166
- // Store registration for verification in tests
167
- this.lastRegisteredType = type;
168
- this.lastRegisteredConstructor = constructor;
169
- }
170
- },
171
- util: {
172
- evaluateNodeProperty: function(value, type, node, msg, callback) {
173
- if (type === 'json') {
174
- try {
175
- // Simulate parsing a JSON string into an object
176
- return JSON.parse(JSON.stringify(value));
177
- } catch (e) {
178
- if (callback) {
179
- callback(e, null);
180
- }
181
- return null;
182
- }
183
- }
184
-
185
- // Simple mock implementation
186
- if (callback) {
187
- callback(null, value);
188
- }
189
- return value;
190
- },
191
- encrypt: function(value) {
192
- return 'encrypted:' + value;
193
- },
194
- decrypt: function(value) {
195
- return value.replace('encrypted:', '');
196
- }
197
- },
198
- log: {
199
- info: options.logInfo || function() {},
200
- warn: options.logWarn || function() {},
201
- error: options.logError || function() {},
202
- debug: options.logDebug || function() {}
203
- }
204
- };
205
- }
206
-
207
- /**
208
- * Set up module mocks for require() calls
209
- */
210
- function setupModuleMocks() {
211
- const mockModules = {
212
- 'node-imap': createMockImap(),
213
- 'mailparser': createMockMailparser()
214
- };
215
-
216
- const Module = require('module');
217
- const originalLoad = Module._load;
218
-
219
- Module._load = function(request, parent) {
220
- if (mockModules[request]) {
221
- return mockModules[request];
222
- }
223
- return originalLoad.apply(this, arguments);
224
- };
225
-
226
- // Return cleanup function
227
- return function cleanup() {
228
- Module._load = originalLoad;
229
- };
230
- }
231
-
232
- /**
233
- * Create test configurations for different scenarios
234
- */
235
- const testConfigs = {
236
- valid: {
237
- id: 'test-node-1',
238
- type: 'email-receiver',
239
- name: 'Test Email Receiver',
240
- host: 'imap.test.com',
241
- hostType: 'str',
242
- port: 993,
243
- portType: 'num',
244
- tls: true,
245
- tlsType: 'bool',
246
- user: 'test@test.com',
247
- userType: 'str',
248
- password: 'testpass',
249
- passwordType: 'str',
250
- folder: 'INBOX',
251
- folderType: 'str',
252
- markseen: true,
253
- markseenType: 'bool'
254
- },
255
-
256
- arrayFolders: {
257
- id: 'test-node-3',
258
- type: 'email-receiver',
259
- name: 'Array Folders Test',
260
- host: 'imap.test.com',
261
- hostType: 'str',
262
- port: 993,
263
- portType: 'num',
264
- user: 'test@test.com',
265
- userType: 'str',
266
- password: 'testpass',
267
- passwordType: 'str',
268
- folder: ['INBOX', 'Junk', 'Drafts'],
269
- folderType: 'json',
270
- markseen: false,
271
- markseenType: 'bool'
272
- },
273
-
274
- invalidConfig: {
275
- id: 'test-node-4',
276
- type: 'email-receiver',
277
- name: 'Invalid Config Test',
278
- host: '', // Missing host
279
- hostType: 'str',
280
- port: 993,
281
- portType: 'num',
282
- user: 'test@test.com',
283
- userType: 'str',
284
- password: '', // Missing password
285
- passwordType: 'str',
286
- folder: 123, // Wrong type
287
- folderType: 'num'
288
- },
289
-
290
- minimal: {
291
- id: 'test-node-5',
292
- type: 'email-receiver',
293
- host: 'imap.minimal.com',
294
- hostType: 'str',
295
- port: 993,
296
- portType: 'num',
297
- user: 'minimal@test.com',
298
- userType: 'str',
299
- password: 'minimalpass',
300
- passwordType: 'str',
301
- folder: 'INBOX',
302
- folderType: 'str'
303
- }
304
- };
305
-
306
- /**
307
- * Create test flows for Node-RED integration tests
308
- */
309
- const testFlows = {
310
- single: [
311
- testConfigs.valid
312
- ],
313
-
314
- withHelper: [
315
- testConfigs.valid,
316
- { id: 'h1', type: 'helper' }
317
- ],
318
-
319
- connected: [
320
- { ...testConfigs.valid, wires: [['h1']] },
321
- { id: 'h1', type: 'helper' }
322
- ],
323
-
324
- multiOutput: [
325
- { ...testConfigs.valid, wires: [['h1', 'h2']] },
326
- { id: 'h1', type: 'helper' },
327
- { id: 'h2', type: 'helper' }
328
- ]
329
- };
330
-
331
- /**
332
- * Utility functions for test assertions
333
- */
334
- const testUtils = {
335
- /**
336
- * Wait for a specified amount of time
337
- */
338
- wait: (ms = 100) => new Promise(resolve => setTimeout(resolve, ms)),
339
-
340
- /**
341
- * Create a promise that resolves when a node receives a message
342
- */
343
- waitForMessage: (node, timeout = 1000) => {
344
- return new Promise((resolve, reject) => {
345
- const timer = setTimeout(() => {
346
- reject(new Error('Timeout waiting for message'));
347
- }, timeout);
348
-
349
- node.on('input', (msg) => {
350
- clearTimeout(timer);
351
- resolve(msg);
352
- });
353
- });
354
- },
355
-
356
- /**
357
- * Verify that a message has expected properties
358
- */
359
- verifyMessage: (msg, expectedProps = {}) => {
360
- const should = require('should');
361
- should.exist(msg);
362
-
363
- Object.keys(expectedProps).forEach(prop => {
364
- if (expectedProps[prop] !== undefined) {
365
- msg.should.have.property(prop, expectedProps[prop]);
366
- }
367
- });
368
- }
369
- };
370
-
371
- module.exports = {
372
- createMockImap,
373
- createMockMailparser,
374
- createMockNodeRED,
375
- setupModuleMocks,
376
- testConfigs,
377
- testFlows,
378
- testUtils
379
- };