@5minds/node-red-contrib-processcube 1.16.0-feature-671c49-mfaxerba → 1.16.0-feature-d3ee2b-mfcjbu9w

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,166 @@
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>
@@ -0,0 +1,231 @@
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
+ };
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "@5minds/node-red-contrib-processcube",
3
- "version": "1.16.0-feature-671c49-mfaxerba",
3
+ "version": "1.16.0-feature-d3ee2b-mfcjbu9w",
4
4
  "license": "MIT",
5
5
  "description": "Node-RED nodes for ProcessCube",
6
6
  "scripts": {
7
- "lint": "prettier --write --config ./.prettierrc.json \"**/*.{html,js}\""
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"
8
10
  },
9
11
  "authors": [
10
12
  {
@@ -18,6 +20,10 @@
18
20
  {
19
21
  "name": "André Siebelist",
20
22
  "email": "Andre.Siebelist@5Minds.de"
23
+ },
24
+ {
25
+ "name": "Diana Stefan",
26
+ "email": "Diana.Stefan@5Minds.de"
21
27
  }
22
28
  ],
23
29
  "repository": {
@@ -37,6 +43,7 @@
37
43
  "nodes": {
38
44
  "checkAuthorization": "check-authorization.js",
39
45
  "DataobjectInstanceQuery": "dataobject-instance-query.js",
46
+ "emailReceiver": "email-receiver.js",
40
47
  "EndEventFinishedListener": "endevent-finished-listener.js",
41
48
  "externaltaskInput": "externaltask-input.js",
42
49
  "externaltaskOutput": "externaltask-output.js",
@@ -61,11 +68,23 @@
61
68
  },
62
69
  "examples": "examples"
63
70
  },
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
+ },
64
79
  "dependencies": {
65
80
  "@5minds/processcube_engine_client": "^6.1.4",
66
81
  "adm-zip": "^0.5.16",
67
82
  "jwt-decode": "^4.0.0",
68
- "openid-client": "^5.5.0"
83
+ "mailparser": "^3.6.8",
84
+ "node-imap": "^0.9.6"
85
+ },
86
+ "overrides": {
87
+ "semver": ">=7.0.0"
69
88
  },
70
89
  "keywords": [
71
90
  "node-red",
@@ -0,0 +1,477 @@
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
+ });