@5minds/node-red-contrib-processcube 1.16.0-feature-671c49-mfaxerba → 1.16.0-feature-320605-mfchyhti
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.
- package/email-receiver.html +166 -0
- package/email-receiver.js +231 -0
- package/package.json +23 -3
- package/test/email-receiver_spec.js +317 -0
@@ -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-
|
3
|
+
"version": "1.16.0-feature-320605-mfchyhti",
|
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,24 @@
|
|
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",
|
82
|
+
"chai": "^4.3.4",
|
67
83
|
"jwt-decode": "^4.0.0",
|
68
|
-
"
|
84
|
+
"mailparser": "^3.6.8",
|
85
|
+
"node-imap": "^0.9.6"
|
86
|
+
},
|
87
|
+
"overrides": {
|
88
|
+
"semver": ">=7.0.0"
|
69
89
|
},
|
70
90
|
"keywords": [
|
71
91
|
"node-red",
|
@@ -0,0 +1,317 @@
|
|
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
|
+
|
11
|
+
before(function() {
|
12
|
+
// Create mock modules
|
13
|
+
const mockModules = {
|
14
|
+
'node-imap': function(config) {
|
15
|
+
this.config = config;
|
16
|
+
this.connect = function() {};
|
17
|
+
this.openBox = function() {};
|
18
|
+
this.search = function() {};
|
19
|
+
this.fetch = function() { return { on: function() {}, once: function() {} }; };
|
20
|
+
this.end = function() {};
|
21
|
+
this.once = function() {};
|
22
|
+
return this;
|
23
|
+
},
|
24
|
+
'mailparser': {
|
25
|
+
simpleParser: function() {
|
26
|
+
return Promise.resolve({
|
27
|
+
subject: 'test',
|
28
|
+
text: 'test body',
|
29
|
+
html: '<p>test</p>',
|
30
|
+
from: { text: 'test@test.com' },
|
31
|
+
date: new Date(),
|
32
|
+
headers: new Map(),
|
33
|
+
attachments: []
|
34
|
+
});
|
35
|
+
}
|
36
|
+
}
|
37
|
+
};
|
38
|
+
|
39
|
+
// Override require
|
40
|
+
const Module = require('module');
|
41
|
+
originalLoad = Module._load;
|
42
|
+
Module._load = function(request, parent) {
|
43
|
+
if (mockModules[request]) {
|
44
|
+
return mockModules[request];
|
45
|
+
}
|
46
|
+
return originalLoad.apply(this, arguments);
|
47
|
+
};
|
48
|
+
|
49
|
+
// Load the node with mocked dependencies
|
50
|
+
emailReceiverNode = require('../email-receiver.js');
|
51
|
+
});
|
52
|
+
|
53
|
+
after(function() {
|
54
|
+
// Restore original module loading
|
55
|
+
if (originalLoad) {
|
56
|
+
const Module = require('module');
|
57
|
+
Module._load = originalLoad;
|
58
|
+
}
|
59
|
+
});
|
60
|
+
|
61
|
+
describe('Unit Tests', function() {
|
62
|
+
it('should export a function', function() {
|
63
|
+
// ARRANGE: Node module is already loaded
|
64
|
+
|
65
|
+
// ACT: Check the type of the exported module
|
66
|
+
|
67
|
+
// ASSERT: Should be a function
|
68
|
+
emailReceiverNode.should.be.type('function');
|
69
|
+
});
|
70
|
+
|
71
|
+
it('should register node type without errors', function() {
|
72
|
+
// ARRANGE: Set up mock RED object and capture registration calls
|
73
|
+
let registeredType;
|
74
|
+
let registeredConstructor;
|
75
|
+
|
76
|
+
const mockRED = {
|
77
|
+
nodes: {
|
78
|
+
createNode: function(node, config) {
|
79
|
+
node.id = config.id;
|
80
|
+
node.type = config.type;
|
81
|
+
node.name = config.name;
|
82
|
+
node.on = function() {};
|
83
|
+
node.status = function() {};
|
84
|
+
node.error = function() {};
|
85
|
+
node.send = function() {};
|
86
|
+
return node;
|
87
|
+
},
|
88
|
+
registerType: function(type, constructor) {
|
89
|
+
registeredType = type;
|
90
|
+
registeredConstructor = constructor;
|
91
|
+
}
|
92
|
+
},
|
93
|
+
util: {
|
94
|
+
evaluateNodeProperty: function(value, type) {
|
95
|
+
return value;
|
96
|
+
},
|
97
|
+
encrypt: function(value) {
|
98
|
+
return 'encrypted:' + value;
|
99
|
+
}
|
100
|
+
}
|
101
|
+
};
|
102
|
+
|
103
|
+
// ACT: Call the node registration function
|
104
|
+
emailReceiverNode(mockRED);
|
105
|
+
|
106
|
+
// ASSERT: Verify registration was called correctly
|
107
|
+
registeredType.should.equal('email-receiver');
|
108
|
+
registeredConstructor.should.be.type('function');
|
109
|
+
});
|
110
|
+
|
111
|
+
it('should handle node instantiation', function() {
|
112
|
+
// ARRANGE: Set up mock RED object and node instance tracking
|
113
|
+
let nodeInstance;
|
114
|
+
|
115
|
+
const mockRED = {
|
116
|
+
nodes: {
|
117
|
+
createNode: function(node, config) {
|
118
|
+
nodeInstance = node;
|
119
|
+
node.id = config.id;
|
120
|
+
node.type = config.type;
|
121
|
+
node.name = config.name;
|
122
|
+
node.on = function() {};
|
123
|
+
node.status = function() {};
|
124
|
+
node.error = function() {};
|
125
|
+
node.send = function() {};
|
126
|
+
return node;
|
127
|
+
},
|
128
|
+
registerType: function(type, NodeConstructor) {
|
129
|
+
// Simulate creating a node instance with valid config
|
130
|
+
const config = {
|
131
|
+
id: 'test-node',
|
132
|
+
type: 'email-receiver',
|
133
|
+
name: 'Test Email Receiver',
|
134
|
+
host: 'imap.test.com',
|
135
|
+
hostType: 'str',
|
136
|
+
port: 993,
|
137
|
+
portType: 'num',
|
138
|
+
user: 'test@test.com',
|
139
|
+
userType: 'str',
|
140
|
+
password: 'testpass',
|
141
|
+
passwordType: 'str',
|
142
|
+
folder: 'INBOX',
|
143
|
+
folderType: 'str',
|
144
|
+
markseen: true,
|
145
|
+
markseenType: 'bool'
|
146
|
+
};
|
147
|
+
|
148
|
+
new NodeConstructor(config);
|
149
|
+
}
|
150
|
+
},
|
151
|
+
util: {
|
152
|
+
evaluateNodeProperty: function(value, type) {
|
153
|
+
return value;
|
154
|
+
},
|
155
|
+
encrypt: function(value) {
|
156
|
+
return 'encrypted:' + value;
|
157
|
+
}
|
158
|
+
}
|
159
|
+
};
|
160
|
+
|
161
|
+
// ACT: Register the node and create an instance
|
162
|
+
emailReceiverNode(mockRED);
|
163
|
+
|
164
|
+
// ASSERT: Verify the node instance was created with correct properties
|
165
|
+
should.exist(nodeInstance);
|
166
|
+
nodeInstance.should.have.property('name', 'Test Email Receiver');
|
167
|
+
});
|
168
|
+
});
|
169
|
+
|
170
|
+
describe('Integration Tests with Node-RED Helper', function() {
|
171
|
+
const helper = require('node-red-node-test-helper');
|
172
|
+
|
173
|
+
// CRITICAL: Initialize the helper with Node-RED
|
174
|
+
before(function(done) {
|
175
|
+
// This is the missing piece that was causing the clearRegistry error
|
176
|
+
helper.init(require.resolve('node-red'));
|
177
|
+
done();
|
178
|
+
});
|
179
|
+
|
180
|
+
beforeEach(function(done) {
|
181
|
+
helper.startServer(done);
|
182
|
+
});
|
183
|
+
|
184
|
+
afterEach(function(done) {
|
185
|
+
helper.unload();
|
186
|
+
helper.stopServer(done);
|
187
|
+
});
|
188
|
+
|
189
|
+
it('should load in Node-RED test environment', function(done) {
|
190
|
+
// ARRANGE: Set up Node-RED flow with proper configuration
|
191
|
+
const flow = [
|
192
|
+
{
|
193
|
+
id: "n1",
|
194
|
+
type: "email-receiver",
|
195
|
+
name: "test node",
|
196
|
+
host: "imap.test.com",
|
197
|
+
hostType: "str",
|
198
|
+
port: "993",
|
199
|
+
portType: "str",
|
200
|
+
tls: true,
|
201
|
+
tlsType: "bool",
|
202
|
+
user: "test@example.com",
|
203
|
+
userType: "str",
|
204
|
+
password: "testpass",
|
205
|
+
passwordType: "str",
|
206
|
+
folder: "INBOX",
|
207
|
+
folderType: "str",
|
208
|
+
markseen: true,
|
209
|
+
markseenType: "bool"
|
210
|
+
}
|
211
|
+
];
|
212
|
+
|
213
|
+
// ACT: Load the node in the test helper environment
|
214
|
+
helper.load(emailReceiverNode, flow, function() {
|
215
|
+
try {
|
216
|
+
// ASSERT: Verify the node loaded correctly
|
217
|
+
const n1 = helper.getNode("n1");
|
218
|
+
should.exist(n1);
|
219
|
+
n1.should.have.property('name', 'test node');
|
220
|
+
n1.should.have.property('type', 'email-receiver');
|
221
|
+
done();
|
222
|
+
} catch (err) {
|
223
|
+
done(err);
|
224
|
+
}
|
225
|
+
});
|
226
|
+
});
|
227
|
+
|
228
|
+
it('should create wired connections correctly', function(done) {
|
229
|
+
// ARRANGE: Set up flow with helper node to catch output
|
230
|
+
const flow = [
|
231
|
+
{
|
232
|
+
id: "n1",
|
233
|
+
type: "email-receiver",
|
234
|
+
name: "test node",
|
235
|
+
host: "imap.test.com",
|
236
|
+
hostType: "str",
|
237
|
+
port: "993",
|
238
|
+
portType: "str",
|
239
|
+
tls: true,
|
240
|
+
tlsType: "bool",
|
241
|
+
user: "test@example.com",
|
242
|
+
userType: "str",
|
243
|
+
password: "testpass",
|
244
|
+
passwordType: "str",
|
245
|
+
folder: "INBOX",
|
246
|
+
folderType: "str",
|
247
|
+
markseen: true,
|
248
|
+
markseenType: "bool",
|
249
|
+
wires: [["n2"]]
|
250
|
+
},
|
251
|
+
{ id: "n2", type: "helper" }
|
252
|
+
];
|
253
|
+
|
254
|
+
// ACT: Load nodes and verify connections
|
255
|
+
helper.load(emailReceiverNode, flow, function() {
|
256
|
+
try {
|
257
|
+
const n1 = helper.getNode("n1");
|
258
|
+
const n2 = helper.getNode("n2");
|
259
|
+
|
260
|
+
// ASSERT: Both nodes should exist and be connected
|
261
|
+
should.exist(n1);
|
262
|
+
should.exist(n2);
|
263
|
+
n1.should.have.property('name', 'test node');
|
264
|
+
n2.should.have.property('type', 'helper');
|
265
|
+
|
266
|
+
done();
|
267
|
+
} catch (err) {
|
268
|
+
done(err);
|
269
|
+
}
|
270
|
+
});
|
271
|
+
});
|
272
|
+
|
273
|
+
it('should handle input without crashing', function(done) {
|
274
|
+
// ARRANGE: Set up minimal flow
|
275
|
+
const flow = [
|
276
|
+
{
|
277
|
+
id: "n1",
|
278
|
+
type: "email-receiver",
|
279
|
+
name: "test node",
|
280
|
+
host: "imap.test.com",
|
281
|
+
hostType: "str",
|
282
|
+
port: "993",
|
283
|
+
portType: "str",
|
284
|
+
tls: true,
|
285
|
+
tlsType: "bool",
|
286
|
+
user: "test@example.com",
|
287
|
+
userType: "str",
|
288
|
+
password: "testpass",
|
289
|
+
passwordType: "str",
|
290
|
+
folder: "INBOX",
|
291
|
+
folderType: "str",
|
292
|
+
markseen: true,
|
293
|
+
markseenType: "bool"
|
294
|
+
}
|
295
|
+
];
|
296
|
+
|
297
|
+
// ACT: Load node and send input
|
298
|
+
helper.load(emailReceiverNode, flow, function() {
|
299
|
+
try {
|
300
|
+
const n1 = helper.getNode("n1");
|
301
|
+
should.exist(n1);
|
302
|
+
|
303
|
+
// Send input - this should not crash due to mocked IMAP
|
304
|
+
n1.receive({ payload: "test input" });
|
305
|
+
|
306
|
+
// ASSERT: If we reach here, the node handled input gracefully
|
307
|
+
setTimeout(() => {
|
308
|
+
done(); // Success if no errors thrown
|
309
|
+
}, 500);
|
310
|
+
|
311
|
+
} catch (err) {
|
312
|
+
done(err);
|
313
|
+
}
|
314
|
+
});
|
315
|
+
});
|
316
|
+
});
|
317
|
+
});
|