@5minds/node-red-contrib-processcube 1.16.0-feature-d3ee2b-mfcjbu9w → 1.16.1-develop-136f5e-mh90amev

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.
@@ -28,16 +28,16 @@ module.exports = function (RED) {
28
28
  options['workerId'] = this.workername;
29
29
  }
30
30
 
31
- if (!options['lockDuration'] && process.env.NODE_RED_ETW_LOCK_DURATION) {
32
- options['lockDuration'] = parseInt(process.env.NODE_RED_ETW_LOCK_DURATION) || undefined;
31
+ if (!options['lockDuration'] && (process.env.NODE_RED_ETW_LOCK_DURATION || process.env.NODERED_ETW_LOCK_DURATION)) {
32
+ options['lockDuration'] = parseInt(process.env.NODE_RED_ETW_LOCK_DURATION) || parseInt(process.env.NODERED_ETW_LOCK_DURATION) || undefined;
33
33
  }
34
34
 
35
35
  if (!options['longpollingTimeout']) {
36
- options['longpollingTimeout'] = parseInt(process.env.NODE_RED_ETW_LONGPOLLING_TIMEOUT) || undefined;
36
+ options['longpollingTimeout'] = parseInt(process.env.NODE_RED_ETW_LONGPOLLING_TIMEOUT) || parseInt(process.env.NODERED_ETW_LONGPOLLING_TIMEOUT) || undefined;
37
37
  }
38
38
 
39
39
  if (!options['idleTimeout']) {
40
- options['idleTimeout'] = parseInt(process.env.NODE_RED_ETW_IDLE_TIMEOUT) || undefined;
40
+ options['idleTimeout'] = parseInt(process.env.NODE_RED_ETW_IDLE_TIMEOUT) || parseInt(process.env.NODERED_ETW_IDLE_TIMEOUT) || undefined;
41
41
  }
42
42
 
43
43
  node._subscribed = true;
@@ -164,7 +164,7 @@ module.exports = function (RED) {
164
164
  }
165
165
  };
166
166
 
167
- RED.hooks.add('preDeliver', (sendEvent) => {
167
+ const onPreDeliver = (sendEvent) => {
168
168
  if (node.isHandling() && node.ownMessage(sendEvent.msg)) {
169
169
 
170
170
  const sourceNode = sendEvent?.source?.node;
@@ -185,14 +185,15 @@ module.exports = function (RED) {
185
185
 
186
186
  node.traceExecution(debugMsg);
187
187
 
188
- if (process.env.NODE_RED_ETW_STEP_LOGGING == 'true') {
188
+ if (process.env.NODE_RED_ETW_STEP_LOGGING == 'true' || process.env.NODERED_ETW_STEP_LOGGING == 'true') {
189
189
  node._trace = `'${sourceNode.name || sourceNode.type}'->'${destinationNode.name || destinationNode.type}'`;
190
190
  node.log(`preDeliver: ${node._trace}`);
191
191
  }
192
192
  }
193
- });
193
+ };
194
+ RED.hooks.add('preDeliver', onPreDeliver);
194
195
 
195
- RED.hooks.add('postDeliver', (sendEvent) => {
196
+ const onPostDeliver = (sendEvent) => {
196
197
  if (node.isHandling() && node.ownMessage(sendEvent.msg)) {
197
198
  const sourceNode = sendEvent?.source?.node;
198
199
  const destinationNode = sendEvent?.destination?.node;
@@ -211,12 +212,13 @@ module.exports = function (RED) {
211
212
 
212
213
  node.traceExecution(debugMsg);
213
214
 
214
- if (process.env.NODE_RED_ETW_STEP_LOGGING == 'true') {
215
+ if (process.env.NODE_RED_ETW_STEP_LOGGING == 'true' || process.env.NODERED_ETW_STEP_LOGGING == 'true') {
215
216
  node._trace = `'${sourceNode.name || sourceNode.type}'->'${destinationNode.name || destinationNode.type}'`;
216
217
  node.log(`postDeliver: ${node._trace}`);
217
218
  }
218
219
  }
219
- });
220
+ };
221
+ RED.hooks.add('postDeliver', onPostDeliver);
220
222
 
221
223
  node.setSubscribedStatus = () => {
222
224
  this._subscribed = true;
@@ -440,7 +442,7 @@ module.exports = function (RED) {
440
442
  externalTaskWorker.onHeartbeat((event, external_task_id) => {
441
443
  node.setSubscribedStatus();
442
444
 
443
- if (process.env.NODE_RED_ETW_HEARTBEAT_LOGGING == 'true') {
445
+ if (process.env.NODE_RED_ETW_HEARTBEAT_LOGGING == 'true' || process.env.NODERED_ETW_HEARTBEAT_LOGGING == 'true') {
444
446
  if (external_task_id) {
445
447
  this.log(`subscription (heartbeat:topic ${node.topic}, ${event} for ${external_task_id}).`);
446
448
  } else {
@@ -460,7 +462,7 @@ module.exports = function (RED) {
460
462
 
461
463
  node.setUnsubscribedStatus(error);
462
464
 
463
- if (process.env.NODE_RED_ETW_STOP_IF_FAILED == 'true') {
465
+ if (process.env.NODE_RED_ETW_STOP_IF_FAILED == 'true' || process.env.NODERED_ETW_STOP_IF_FAILED == 'true') {
464
466
  // abort the external task MM: waiting for a fix in the client.ts
465
467
  externalTaskWorker.abortExternalTaskIfPresent(externalTask.id);
466
468
  // mark the external task as finished, cause it is gone
@@ -486,7 +488,10 @@ module.exports = function (RED) {
486
488
 
487
489
  node.on('close', () => {
488
490
  try {
491
+ RED.hooks.remove('preDeliver', onPreDeliver);
492
+ RED.hooks.remove('postDeliver', onPostDeliver);
489
493
  externalTaskWorker.stop();
494
+ node.log('External Task Worker closed.');
490
495
  } catch {
491
496
  node.error('Client close failed', {});
492
497
  }
package/package.json CHANGED
@@ -1,12 +1,10 @@
1
1
  {
2
2
  "name": "@5minds/node-red-contrib-processcube",
3
- "version": "1.16.0-feature-d3ee2b-mfcjbu9w",
3
+ "version": "1.16.1-develop-136f5e-mh90amev",
4
4
  "license": "MIT",
5
5
  "description": "Node-RED nodes for ProcessCube",
6
6
  "scripts": {
7
- "lint": "prettier --write --config ./.prettierrc.json \"**/*.{html,js}\"",
8
- "test": "mocha test/**/*_spec.js --timeout 10000",
9
- "test:debug": "mocha test/**/*_spec.js --timeout 0 --reporter spec"
7
+ "lint": "prettier --write --config ./.prettierrc.json \"**/*.{html,js}\""
10
8
  },
11
9
  "authors": [
12
10
  {
@@ -20,10 +18,6 @@
20
18
  {
21
19
  "name": "André Siebelist",
22
20
  "email": "Andre.Siebelist@5Minds.de"
23
- },
24
- {
25
- "name": "Diana Stefan",
26
- "email": "Diana.Stefan@5Minds.de"
27
21
  }
28
22
  ],
29
23
  "repository": {
@@ -43,7 +37,6 @@
43
37
  "nodes": {
44
38
  "checkAuthorization": "check-authorization.js",
45
39
  "DataobjectInstanceQuery": "dataobject-instance-query.js",
46
- "emailReceiver": "email-receiver.js",
47
40
  "EndEventFinishedListener": "endevent-finished-listener.js",
48
41
  "externaltaskInput": "externaltask-input.js",
49
42
  "externaltaskOutput": "externaltask-output.js",
@@ -68,23 +61,11 @@
68
61
  },
69
62
  "examples": "examples"
70
63
  },
71
- "devDependencies": {
72
- "chai": "^4.3.4",
73
- "mocha": "^11.7.2",
74
- "node-red": "^4.1.0",
75
- "node-red-node-test-helper": "^0.3.5",
76
- "should": "^13.2.3",
77
- "sinon": "^11.1.2"
78
- },
79
64
  "dependencies": {
80
65
  "@5minds/processcube_engine_client": "^6.1.4",
81
66
  "adm-zip": "^0.5.16",
82
67
  "jwt-decode": "^4.0.0",
83
- "mailparser": "^3.6.8",
84
- "node-imap": "^0.9.6"
85
- },
86
- "overrides": {
87
- "semver": ">=7.0.0"
68
+ "openid-client": "^5.5.0"
88
69
  },
89
70
  "keywords": [
90
71
  "node-red",
@@ -1,166 +0,0 @@
1
- <script type="text/javascript">
2
- RED.nodes.registerType('email-receiver', {
3
- category: 'ProcessCube Tools',
4
- color: '#02AFD6',
5
- defaults: {
6
- name: { value: "" },
7
- host: { value: "", required: true, validate: RED.validators.typedInput("hostType") },
8
- hostType: { value: "str" },
9
- port: { value: "", required: true, validate: RED.validators.typedInput("portType") },
10
- portType: { value: "num" },
11
- tls: { value: true, required: true, validate: RED.validators.typedInput("tlsType") },
12
- tlsType: { value: "bool" },
13
- user: { value: "", required: true, validate: RED.validators.typedInput("userType") },
14
- userType: { value: "str" },
15
- password: { value: "", required: true, type: "password" },
16
- passwordType: { value: "cred", required: true },
17
- folder: { value: "", required: true, validate: RED.validators.typedInput("folderType") },
18
- folderType: { value: "json" },
19
- markseen: { value: true, validate: RED.validators.typedInput("markseenType") },
20
- markseenType: { value: "bool" }
21
- },
22
- inputs: 1,
23
- outputs: 1,
24
- icon: "font-awesome/fa-inbox",
25
- label: function() {
26
- return this.name || "E-Mail Receiver";
27
- },
28
- oneditprepare: function() {
29
- $('#node-input-host').typedInput({
30
- default: 'str',
31
- types: ['str', 'msg', 'flow', 'global'],
32
- typeField: '#node-input-hostType'
33
- });
34
-
35
- $('#node-input-port').typedInput({
36
- default: 'num',
37
- types: ['num', 'msg', 'flow', 'global'],
38
- typeField: '#node-input-portType'
39
- });
40
-
41
- $('#node-input-tls').typedInput({
42
- default: 'bool',
43
- types: ['bool', 'msg', 'flow', 'global'],
44
- typeField: '#node-input-tlsType'
45
- });
46
-
47
- $('#node-input-user').typedInput({
48
- default: 'str',
49
- types: ['str', 'msg', 'flow', 'global'],
50
- typeField: '#node-input-userType'
51
- });
52
-
53
- $('#node-input-password').typedInput({
54
- default: 'cred',
55
- types: ['cred', 'msg', 'flow', 'global'],
56
- typeField: '#node-input-passwordType'
57
- });
58
-
59
- $('#node-input-folder').typedInput({
60
- default: 'json',
61
- types: ['msg', 'flow', 'global', 'json', 'jsonata'],
62
- typeField: '#node-input-folderType'
63
- });
64
-
65
- $('#node-input-markseen').typedInput({
66
- default: 'bool',
67
- types: ['bool', 'msg', 'flow', 'global'],
68
- typeField: '#node-input-markseenType'
69
- });
70
- }
71
- });
72
- </script>
73
-
74
- <script type="text/html" data-template-name="email-receiver">
75
- <div class="form-row">
76
- <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
77
- <input type="text" id="node-input-name" placeholder="Name">
78
- </div>
79
-
80
- <div class="form-row">
81
- <label for="node-input-host"><i class="fa fa-server"></i> IMAP Host</label>
82
- <input type="text" id="node-input-host" placeholder="imap.gmail.com">
83
- <input type="hidden" id="node-input-hostType">
84
- </div>
85
-
86
- <div class="form-row">
87
- <label for="node-input-port"><i class="fa fa-terminal"></i> Port</label>
88
- <input type="text" id="node-input-port" placeholder="993">
89
- <input type="hidden" id="node-input-portType">
90
- </div>
91
-
92
- <div class="form-row">
93
- <label for="node-input-tls"><i class="fa fa-lock"></i> Use TLS</label>
94
- <input type="text" id="node-input-tls">
95
- <input type="hidden" id="node-input-tlsType">
96
- </div>
97
-
98
- <div class="form-row">
99
- <label for="node-input-user"><i class="fa fa-user"></i> User</label>
100
- <input type="text" id="node-input-user">
101
- <input type="hidden" id="node-input-userType">
102
- </div>
103
-
104
- <div class="form-row">
105
- <label for="node-input-password"><i class="fa fa-key"></i> Password</label>
106
- <input type="text" id="node-input-password">
107
- <input type="hidden" id="node-input-passwordType">
108
- </div>
109
-
110
- <div class="form-row">
111
- <label for="node-input-folder"><i class="fa fa-folder-open"></i> Folder(s)</label>
112
- <input type="text" id="node-input-folder" placeholder="[INBOX]">
113
- <input type="hidden" id="node-input-folderType">
114
- </div>
115
-
116
- <div class="form-row">
117
- <label for="node-input-markseen"><i class="fa fa-eye"></i> Mark as seen</label>
118
- <input type="text" id="node-input-markseen">
119
- <input type="hidden" id="node-input-markseenType">
120
- </div>
121
- </script>
122
-
123
- <script type="text/html" data-help-name="email-receiver">
124
- <p>A Node-RED node that fetches unseen emails from a specified IMAP server. Each fetched email is sent as a separate message on the output.</p>
125
- <h3>Configuration</h3>
126
- <p>All fields can be configured as a **string**, from a **message property (msg)**, **flow** or **global** context, or an **environment variable**.</p>
127
- <h3>Inputs</h3>
128
- <dl class="message-properties">
129
- <dt>payload</dt>
130
- <dd>The node is triggered by any incoming message. The node's configuration can be overridden by properties of the incoming <code>msg</code> object.</dd>
131
- </dl>
132
- <h3>Outputs</h3>
133
- <dl class="message-properties">
134
- <dt>payload
135
- <span class="property-type">string</span>
136
- </dt>
137
- <dd>The text body of the email.</dd>
138
- </dl>
139
-
140
- <hr/>
141
-
142
- <h3>Optional Message Properties</h3>
143
- <p>You can override default settings by passing the following properties in the incoming <code>msg</code> object:</p>
144
- <dl class="message-properties">
145
- <dt>msg.imap_connTimeout
146
- <span class="property-type">number</span>
147
- </dt>
148
- <dd>The connection timeout in milliseconds (default: 10000).</dd>
149
- <dt>msg.imap_authTimeout
150
- <span class="property-type">number</span>
151
- </dt>
152
- <dd>The authentication timeout in milliseconds (default: 5000).</dd>
153
- <dt>msg.imap_keepalive
154
- <span class="property-type">boolean</span>
155
- </dt>
156
- <dd>If set to `true`, a periodic NOOP command is sent to keep the connection alive (default: `true`).</dd>
157
- <dt>msg.imap_autotls
158
- <span class="property-type">string</span>
159
- </dt>
160
- <dd>Controls STARTTLS behavior. Set to `never` to disable it (default: `never`).</dd>
161
- <dt>msg.imap_tlsOptions
162
- <span class="property-type">object</span>
163
- </dt>
164
- <dd>An object containing TLS options for the connection.</dd>
165
- </dl>
166
- </script>
package/email-receiver.js DELETED
@@ -1,231 +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 if (typeof imap_folder === 'string') {
23
- folders = imap_folder.split(',').map(f => f.trim()).filter(f => f.length > 0);
24
- } else {
25
- const errorMsg = "The 'folders' property must be an array of strings or a comma-separated string.";
26
- node.status({ fill: 'red', shape: 'ring', text: errorMsg });
27
- node.error(errorMsg, msg);
28
- return;
29
- }
30
- const imap_markSeen = RED.util.evaluateNodeProperty(config.markseen, config.markseenType, node, msg);
31
-
32
- const finalConfig = {
33
- host: imap_host,
34
- port: (typeof imap_port === 'string') ? parseInt(imap_port, 10) : imap_port,
35
- tls: imap_tls,
36
- user: imap_user,
37
- password: imap_password,
38
- folders: (Array.isArray(imap_folder)) ? imap_folder : imap_folder.split(',').map(f => f.trim()).filter(f => f.length > 0),
39
- markSeen: imap_markSeen,
40
- connTimeout: msg.imap_connTimeout || 10000,
41
- authTimeout: msg.imap_authTimeout || 5000,
42
- keepalive: msg.imap_keepalive !== undefined ? msg.imap_keepalive : true,
43
- autotls: msg.imap_autotls || 'never',
44
- tlsOptions: msg.imap_tlsOptions || { rejectUnauthorized: false }
45
- };
46
-
47
- if (!finalConfig.user || !finalConfig.password || !finalConfig.port || !finalConfig.host || !finalConfig.folders) {
48
- const errorMessage = 'Missing required IMAP config (user, password, port, host, or folders missing). Aborting.';
49
- node.status({ fill: 'red', shape: 'ring', text: 'missing config' });
50
- node.error(errorMessage);
51
- return;
52
- }
53
-
54
- const fetchEmails = ({
55
- host,
56
- port,
57
- tls,
58
- user,
59
- password,
60
- folders,
61
- markSeen = true,
62
- connTimeout = 10000,
63
- authTimeout = 5000,
64
- keepalive = true,
65
- autotls = 'never',
66
- tlsOptions = { rejectUnauthorized: false }
67
- }, onMail) => {
68
- const imap = new Imap({
69
- user,
70
- password,
71
- host,
72
- port,
73
- tls,
74
- connTimeout,
75
- authTimeout,
76
- keepalive,
77
- autotls,
78
- tlsOptions
79
- });
80
-
81
- const state = {
82
- totalFolders: folders.length,
83
- processedFolders: 0,
84
- successes: 0,
85
- failures: 0,
86
- totalMails: 0,
87
- errors: [],
88
- };
89
-
90
- // Helper to update Node-RED status
91
- const updateStatus = (color, text) => {
92
- node.status({ fill: color, shape: 'dot', text });
93
- };
94
-
95
- // Helper to finalize status and clean up
96
- const finalizeSession = (error = null) => {
97
- if (error) {
98
- node.error('IMAP session terminated: ' + error.message);
99
- node.status({ fill: 'red', shape: 'ring', text: 'connection error' });
100
- } else if (state.failures > 0) {
101
- node.status({
102
- fill: 'red',
103
- shape: 'dot',
104
- text: `Done, ${state.totalMails} mails from ${state.successes}/${state.totalFolders} folders. ${state.failures} failed.`
105
- });
106
- } else {
107
- node.status({
108
- fill: 'green',
109
- shape: 'dot',
110
- text: `Done, fetched ${state.totalMails} mails from ${folders.join(', ')}.`
111
- });
112
- }
113
- if (imap && imap.state !== 'disconnected') {
114
- imap.end();
115
- }
116
- };
117
-
118
- const fetchFromFolder = (folder) => {
119
- updateStatus('yellow', `Fetching from "${folder}"...`);
120
-
121
- imap.openBox(folder, false, (err, box) => {
122
- if (err) {
123
- node.error(`Could not open folder "${folder}": ${err.message}`);
124
- state.failures++;
125
- state.processedFolders++;
126
- return startNextFolder();
127
- }
128
-
129
- imap.search(['UNSEEN'], (err, results) => {
130
- if (err) {
131
- node.error(`Search failed in folder "${folder}": ${err.message}`);
132
- state.failures++;
133
- state.processedFolders++;
134
- return startNextFolder();
135
- }
136
-
137
- if (!results || !results.length) {
138
- state.successes++;
139
- state.processedFolders++;
140
- return startNextFolder();
141
- }
142
-
143
- state.totalMails += results.length;
144
-
145
- const fetch = imap.fetch(results, { bodies: '' });
146
-
147
- fetch.on('message', msg => {
148
- msg.on('body', stream => {
149
- mailparser.simpleParser(stream, (err, parsed) => {
150
- if (err) {
151
- node.error(`Parse error for email from folder "${folder}": ${err.message}`);
152
- return;
153
- }
154
-
155
- const outMsg = {
156
- topic: parsed.subject,
157
- payload: parsed.text,
158
- html: parsed.html,
159
- from: parsed.replyTo?.text || parsed.from?.text,
160
- date: parsed.date,
161
- folder,
162
- header: parsed.headers,
163
- attachments: parsed.attachments.map(att => ({
164
- contentType: att.contentType,
165
- fileName: att.filename,
166
- transferEncoding: att.transferEncoding,
167
- contentDisposition: att.contentDisposition,
168
- generatedFileName: att.cid || att.checksum,
169
- contentId: att.cid,
170
- checksum: att.checksum,
171
- length: att.size,
172
- content: att.content
173
- }))
174
- };
175
- onMail(outMsg);
176
- });
177
- });
178
- });
179
-
180
- fetch.once('error', err => {
181
- node.error(`Fetch error in folder "${folder}": ${err.message}`);
182
- });
183
-
184
- fetch.once('end', () => {
185
- state.successes++;
186
- state.processedFolders++;
187
- updateStatus('green', `Fetched ${results.length} from "${folder}".`);
188
- startNextFolder();
189
- });
190
- });
191
- });
192
- };
193
-
194
- const startNextFolder = () => {
195
- if (state.processedFolders >= state.totalFolders) {
196
- finalizeSession();
197
- } else {
198
- fetchFromFolder(folders[state.processedFolders]);
199
- }
200
- };
201
-
202
- // Centralized event listeners for the IMAP connection
203
- imap.once('ready', () => {
204
- node.status({ fill: 'green', shape: 'dot', text: 'connected' });
205
- startNextFolder();
206
- });
207
-
208
- imap.once('error', err => {
209
- finalizeSession(err);
210
- });
211
-
212
- imap.once('end', () => {
213
- node.log('IMAP connection ended.');
214
- });
215
-
216
- updateStatus('yellow', 'Connecting to IMAP...');
217
- imap.connect();
218
- };
219
-
220
- fetchEmails(finalConfig, mail => {
221
- node.send(mail);
222
- });
223
- });
224
- }
225
-
226
- RED.nodes.registerType("email-receiver", EmailReceiverNode, {
227
- credentials: {
228
- password: { type: "password" }
229
- }
230
- });
231
- };
@@ -1,477 +0,0 @@
1
- const should = require('should');
2
-
3
- describe('Email Receiver Node', function() {
4
- // Set a reasonable timeout
5
- this.timeout(10000);
6
-
7
- // Module and mocking setup
8
- let emailReceiverNode;
9
- let originalLoad;
10
- let mockImap;
11
- let mockMailparser;
12
-
13
- before(function() {
14
- // Create mock modules with correct behavior
15
- mockImap = function(config) {
16
- this.config = config;
17
- this.connect = () => {
18
- // Simulate a successful connection by immediately emitting 'ready'
19
- if (this.events && this.events.ready) {
20
- this.events.ready();
21
- }
22
- };
23
- this.openBox = (folder, readOnly, callback) => { callback(null, { messages: { total: 1 } }); };
24
- this.search = (criteria, callback) => { callback(null, [123]); };
25
- this.fetch = (results, options) => {
26
- return {
27
- on: (event, cb) => {
28
- if (event === 'message') {
29
- cb({ on: (e, bodyCb) => { if (e === 'body') bodyCb({}); } });
30
- }
31
- },
32
- once: (event, cb) => {
33
- if (event === 'end') { cb(); }
34
- }
35
- };
36
- };
37
- this.end = () => {};
38
- this.once = (event, callback) => {
39
- if (!this.events) this.events = {};
40
- this.events[event] = callback;
41
- };
42
- return this;
43
- };
44
-
45
- mockMailparser = {
46
- simpleParser: function() {
47
- return Promise.resolve({
48
- subject: 'test',
49
- text: 'test body',
50
- html: '<p>test</p>',
51
- from: { text: 'test@test.com' },
52
- date: new Date(),
53
- headers: new Map(),
54
- attachments: []
55
- });
56
- }
57
- };
58
-
59
- const mockModules = {
60
- 'node-imap': mockImap,
61
- 'mailparser': mockMailparser
62
- };
63
-
64
- // Override require
65
- const Module = require('module');
66
- originalLoad = Module._load;
67
- Module._load = function(request, parent) {
68
- if (mockModules[request]) {
69
- return mockModules[request];
70
- }
71
- return originalLoad.apply(this, arguments);
72
- };
73
-
74
- // Load the node with mocked dependencies
75
- emailReceiverNode = require('../email-receiver.js');
76
- });
77
-
78
- after(function() {
79
- // Restore original module loading
80
- if (originalLoad) {
81
- const Module = require('module');
82
- Module._load = originalLoad;
83
- }
84
- });
85
-
86
- describe('Unit Tests', function() {
87
- it('should export a function', function() {
88
- // ARRANGE: Node module is already loaded
89
-
90
- // ACT: Check the type of the exported module
91
-
92
- // ASSERT: Should be a function
93
- emailReceiverNode.should.be.type('function');
94
- });
95
-
96
- it('should register node type without errors', function() {
97
- // ARRANGE: Set up mock RED object and capture registration calls
98
- let registeredType;
99
- let registeredConstructor;
100
-
101
- const mockRED = {
102
- nodes: {
103
- createNode: function(node, config) {
104
- node.id = config.id;
105
- node.type = config.type;
106
- node.name = config.name;
107
- node.on = function() {};
108
- node.status = function() {};
109
- node.error = function() {};
110
- node.send = function() {};
111
- return node;
112
- },
113
- registerType: function(type, constructor) {
114
- registeredType = type;
115
- registeredConstructor = constructor;
116
- }
117
- },
118
- util: {
119
- evaluateNodeProperty: function(value, type) {
120
- return value;
121
- },
122
- encrypt: function(value) {
123
- return 'encrypted:' + value;
124
- }
125
- }
126
- };
127
-
128
- // ACT: Call the node registration function
129
- emailReceiverNode(mockRED);
130
-
131
- // ASSERT: Verify registration was called correctly
132
- registeredType.should.equal('email-receiver');
133
- registeredConstructor.should.be.type('function');
134
- });
135
-
136
- it('should handle node instantiation', function() {
137
- // ARRANGE: Set up mock RED object and node instance tracking
138
- let nodeInstance;
139
-
140
- const mockRED = {
141
- nodes: {
142
- createNode: function(node, config) {
143
- nodeInstance = node;
144
- node.id = config.id;
145
- node.type = config.type;
146
- node.name = config.name;
147
- node.on = function() {};
148
- node.status = function() {};
149
- node.error = function() {};
150
- node.send = function() {};
151
- return node;
152
- },
153
- registerType: function(type, NodeConstructor) {
154
- // Simulate creating a node instance with valid config
155
- const config = {
156
- id: 'test-node',
157
- type: 'email-receiver',
158
- name: 'Test Email Receiver',
159
- host: 'imap.test.com',
160
- hostType: 'str',
161
- port: 993,
162
- portType: 'num',
163
- user: 'test@test.com',
164
- userType: 'str',
165
- password: 'testpass',
166
- passwordType: 'str',
167
- folder: 'INBOX',
168
- folderType: 'str',
169
- markseen: true,
170
- markseenType: 'bool'
171
- };
172
-
173
- new NodeConstructor(config);
174
- }
175
- },
176
- util: {
177
- evaluateNodeProperty: function(value, type) {
178
- return value;
179
- },
180
- encrypt: function(value) {
181
- return 'encrypted:' + value;
182
- }
183
- }
184
- };
185
-
186
- // ACT: Register the node and create an instance
187
- emailReceiverNode(mockRED);
188
-
189
- // ASSERT: Verify the node instance was created with correct properties
190
- should.exist(nodeInstance);
191
- nodeInstance.should.have.property('name', 'Test Email Receiver');
192
- });
193
-
194
- it('should handle comma-separated folder string', function(done) {
195
- // ARRANGE: Mock the Node-RED and IMAP environment
196
- let nodeInstance;
197
- let inputCallback;
198
- const mockRED = {
199
- nodes: {
200
- createNode: function(node, config) {
201
- nodeInstance = node;
202
- node.on = (event, callback) => { if (event === 'input') inputCallback = callback; };
203
- node.status = () => {};
204
- node.error = () => {};
205
- node.send = (msg) => {
206
- should.exist(msg);
207
- msg.payload.should.equal('test body');
208
- done();
209
- };
210
- return node;
211
- },
212
- registerType: (type, constructor) => {
213
- new constructor({
214
- host: "imap.test.com", hostType: "str",
215
- port: 993, portType: "num",
216
- user: "test@test.com", userType: "str",
217
- password: "testpass", passwordType: "str",
218
- folder: "INBOX, Spam, Sent", folderType: 'str',
219
- markseen: true, markseenType: 'bool'
220
- });
221
- }
222
- },
223
- util: { evaluateNodeProperty: (value) => value },
224
- };
225
-
226
- // ACT: Register the node, then simulate input
227
- emailReceiverNode(mockRED);
228
- inputCallback({});
229
- });
230
-
231
- it('should handle an array of folders', function(done) {
232
- // ARRANGE: Mock the Node-RED and IMAP environment
233
- let nodeInstance;
234
- let inputCallback;
235
- const mockRED = {
236
- nodes: {
237
- createNode: function(node, config) {
238
- nodeInstance = node;
239
- node.on = (event, callback) => { if (event === 'input') inputCallback = callback; };
240
- node.status = () => {};
241
- node.error = () => {};
242
- node.send = (msg) => {
243
- should.exist(msg);
244
- msg.payload.should.equal('test body');
245
- done();
246
- };
247
- return node;
248
- },
249
- registerType: (type, constructor) => {
250
- new constructor({
251
- host: "imap.test.com", hostType: "str",
252
- port: 993, portType: "num",
253
- user: "test@test.com", userType: "str",
254
- password: "testpass", passwordType: "str",
255
- folder: ["INBOX", "Junk"], folderType: 'json',
256
- markseen: true, markseenType: 'bool'
257
- });
258
- }
259
- },
260
- util: { evaluateNodeProperty: (value) => value },
261
- };
262
-
263
- // ACT: Register the node, then simulate input
264
- emailReceiverNode(mockRED);
265
- inputCallback({});
266
- });
267
-
268
- it('should call node.error for invalid folder type', function(done) {
269
- // ARRANGE: Mock the node instance to capture errors
270
- let errorCalled = false;
271
- const nodeInstance = {
272
- config: { folder: 123, folderType: 'num' },
273
- on: (event, callback) => { if (event === 'input') nodeInstance.inputCallback = callback; },
274
- status: () => {},
275
- error: (err) => {
276
- errorCalled = true;
277
- err.should.containEql('The \'folders\' property must be an array of strings');
278
- done();
279
- },
280
- send: () => {},
281
- };
282
- const mockRED = {
283
- nodes: {
284
- createNode: (node, config) => Object.assign(node, { on: nodeInstance.on, status: nodeInstance.status, error: nodeInstance.error, send: nodeInstance.send }),
285
- registerType: (type, constructor) => new constructor(nodeInstance.config),
286
- },
287
- util: { evaluateNodeProperty: (value, type) => value },
288
- };
289
-
290
- // ACT: Register and instantiate the node, then simulate an input message
291
- emailReceiverNode(mockRED);
292
- nodeInstance.inputCallback({});
293
- });
294
-
295
- it('should call node.error for missing config', function(done) {
296
- // ARRANGE: Mock the node instance to capture errors
297
- let errorCalled = false;
298
- let statusCalled = false;
299
- const nodeInstance = {
300
- config: {
301
- host: "imap.test.com", hostType: "str",
302
- port: 993, portType: "num",
303
- user: "test@test.com", userType: "str",
304
- password: "", passwordType: "str",
305
- folder: "INBOX", folderType: "str"
306
- },
307
- on: (event, callback) => { if (event === 'input') nodeInstance.inputCallback = callback; },
308
- status: (s) => { statusCalled = true; s.fill.should.equal('red'); },
309
- error: (err) => {
310
- errorCalled = true;
311
- err.should.containEql('Missing required IMAP config');
312
- done();
313
- },
314
- send: () => {},
315
- };
316
- const mockRED = {
317
- nodes: {
318
- createNode: (node, config) => Object.assign(node, { on: nodeInstance.on, status: nodeInstance.status, error: nodeInstance.error, send: nodeInstance.send }),
319
- registerType: (type, constructor) => new constructor(nodeInstance.config),
320
- },
321
- util: { evaluateNodeProperty: (value, type) => value },
322
- };
323
-
324
- // ACT: Register and instantiate the node, then simulate an input message
325
- emailReceiverNode(mockRED);
326
- nodeInstance.inputCallback({});
327
- });
328
- });
329
-
330
- describe('Integration Tests with Node-RED Helper', function() {
331
- const helper = require('node-red-node-test-helper');
332
-
333
- // CRITICAL: Initialize the helper with Node-RED
334
- before(function(done) {
335
- // This is the missing piece that was causing the clearRegistry error
336
- helper.init(require.resolve('node-red'));
337
- done();
338
- });
339
-
340
- beforeEach(function(done) {
341
- helper.startServer(done);
342
- });
343
-
344
- afterEach(function(done) {
345
- helper.unload();
346
- helper.stopServer(done);
347
- });
348
-
349
- it('should load in Node-RED test environment', function(done) {
350
- // ARRANGE: Set up Node-RED flow with proper configuration
351
- const flow = [
352
- {
353
- id: "n1",
354
- type: "email-receiver",
355
- name: "test node",
356
- host: "imap.test.com",
357
- hostType: "str",
358
- port: "993",
359
- portType: "str",
360
- tls: true,
361
- tlsType: "bool",
362
- user: "test@example.com",
363
- userType: "str",
364
- password: "testpass",
365
- passwordType: "str",
366
- folder: "INBOX",
367
- folderType: "str",
368
- markseen: true,
369
- markseenType: "bool"
370
- }
371
- ];
372
-
373
- // ACT: Load the node in the test helper environment
374
- helper.load(emailReceiverNode, flow, function() {
375
- try {
376
- // ASSERT: Verify the node loaded correctly
377
- const n1 = helper.getNode("n1");
378
- should.exist(n1);
379
- n1.should.have.property('name', 'test node');
380
- n1.should.have.property('type', 'email-receiver');
381
- done();
382
- } catch (err) {
383
- done(err);
384
- }
385
- });
386
- });
387
-
388
- it('should create wired connections correctly', function(done) {
389
- // ARRANGE: Set up flow with helper node to catch output
390
- const flow = [
391
- {
392
- id: "n1",
393
- type: "email-receiver",
394
- name: "test node",
395
- host: "imap.test.com",
396
- hostType: "str",
397
- port: "993",
398
- portType: "str",
399
- tls: true,
400
- tlsType: "bool",
401
- user: "test@example.com",
402
- userType: "str",
403
- password: "testpass",
404
- passwordType: "str",
405
- folder: "INBOX",
406
- folderType: "str",
407
- markseen: true,
408
- markseenType: "bool",
409
- wires: [["n2"]]
410
- },
411
- { id: "n2", type: "helper" }
412
- ];
413
-
414
- // ACT: Load nodes and verify connections
415
- helper.load(emailReceiverNode, flow, function() {
416
- try {
417
- const n1 = helper.getNode("n1");
418
- const n2 = helper.getNode("n2");
419
-
420
- // ASSERT: Both nodes should exist and be connected
421
- should.exist(n1);
422
- should.exist(n2);
423
- n1.should.have.property('name', 'test node');
424
- n2.should.have.property('type', 'helper');
425
-
426
- done();
427
- } catch (err) {
428
- done(err);
429
- }
430
- });
431
- });
432
-
433
- it('should handle input without crashing', function(done) {
434
- // ARRANGE: Set up minimal flow
435
- const flow = [
436
- {
437
- id: "n1",
438
- type: "email-receiver",
439
- name: "test node",
440
- host: "imap.test.com",
441
- hostType: "str",
442
- port: "993",
443
- portType: "str",
444
- tls: true,
445
- tlsType: "bool",
446
- user: "test@example.com",
447
- userType: "str",
448
- password: "testpass",
449
- passwordType: "str",
450
- folder: "INBOX",
451
- folderType: "str",
452
- markseen: true,
453
- markseenType: "bool"
454
- }
455
- ];
456
-
457
- // ACT: Load node and send input
458
- helper.load(emailReceiverNode, flow, function() {
459
- try {
460
- const n1 = helper.getNode("n1");
461
- should.exist(n1);
462
-
463
- // Send input - this should not crash due to mocked IMAP
464
- n1.receive({ payload: "test input" });
465
-
466
- // ASSERT: If we reach here, the node handled input gracefully
467
- setTimeout(() => {
468
- done(); // Success if no errors thrown
469
- }, 500);
470
-
471
- } catch (err) {
472
- done(err);
473
- }
474
- });
475
- });
476
- });
477
- });