@5minds/node-red-contrib-processcube-tools 1.2.0-develop-d19f89-mg68thdf → 1.2.0-develop-59ef22-mg9d9ja5

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.
Files changed (55) hide show
  1. package/.mocharc.json +5 -0
  2. package/package.json +26 -10
  3. package/src/custom-node-template/custom-node-template.html.template +45 -0
  4. package/src/custom-node-template/custom-node-template.ts.template +69 -0
  5. package/src/email-receiver/email-receiver.ts +439 -0
  6. package/src/email-sender/email-sender.ts +210 -0
  7. package/{processcube-html-to-text/processcube-html-to-text.html → src/html-to-text/html-to-text.html} +3 -3
  8. package/src/html-to-text/html-to-text.ts +53 -0
  9. package/src/index.ts +12 -0
  10. package/src/interfaces/EmailReceiverMessage.ts +22 -0
  11. package/src/interfaces/EmailSenderNodeProperties.ts +37 -0
  12. package/src/interfaces/FetchState.ts +9 -0
  13. package/src/interfaces/ImapConnectionConfig.ts +14 -0
  14. package/src/test/framework/advanced-test-patterns.ts +224 -0
  15. package/src/test/framework/generic-node-test-suite.ts +58 -0
  16. package/src/test/framework/index.ts +17 -0
  17. package/src/test/framework/integration-assertions.ts +67 -0
  18. package/src/test/framework/integration-scenario-builder.ts +77 -0
  19. package/src/test/framework/integration-test-runner.ts +101 -0
  20. package/src/test/framework/node-assertions.ts +63 -0
  21. package/src/test/framework/node-test-runner.ts +260 -0
  22. package/src/test/framework/test-scenario-builder.ts +74 -0
  23. package/src/test/framework/types.ts +61 -0
  24. package/src/test/helpers/email-receiver-test-configs.ts +67 -0
  25. package/src/test/helpers/email-receiver-test-flows.ts +16 -0
  26. package/src/test/helpers/email-sender-test-configs.ts +123 -0
  27. package/src/test/helpers/email-sender-test-flows.ts +16 -0
  28. package/src/test/integration/email-receiver.integration.test.ts +41 -0
  29. package/src/test/integration/email-sender.integration.test.ts +129 -0
  30. package/src/test/interfaces/email-data.ts +10 -0
  31. package/src/test/interfaces/email-receiver-config.ts +12 -0
  32. package/src/test/interfaces/email-sender-config.ts +26 -0
  33. package/src/test/interfaces/imap-config.ts +9 -0
  34. package/src/test/interfaces/imap-mailbox.ts +5 -0
  35. package/src/test/interfaces/mail-options.ts +20 -0
  36. package/src/test/interfaces/parsed-email.ts +11 -0
  37. package/src/test/interfaces/send-mail-result.ts +7 -0
  38. package/src/test/mocks/imap-mock.ts +147 -0
  39. package/src/test/mocks/mailparser-mock.ts +82 -0
  40. package/src/test/mocks/nodemailer-mock.ts +118 -0
  41. package/src/test/unit/email-receiver.unit.test.ts +471 -0
  42. package/src/test/unit/email-sender.unit.test.ts +550 -0
  43. package/tsconfig.json +23 -0
  44. package/email-receiver/email-receiver.js +0 -304
  45. package/email-sender/email-sender.js +0 -178
  46. package/examples/.gitkeep +0 -0
  47. package/processcube-html-to-text/processcube-html-to-text.js +0 -22
  48. package/test/helpers/email-receiver.mocks.js +0 -447
  49. package/test/helpers/email-sender.mocks.js +0 -368
  50. package/test/integration/email-receiver.integration.test.js +0 -515
  51. package/test/integration/email-sender.integration.test.js +0 -239
  52. package/test/unit/email-receiver.unit.test.js +0 -304
  53. package/test/unit/email-sender.unit.test.js +0 -570
  54. /package/{email-receiver → src/email-receiver}/email-receiver.html +0 -0
  55. /package/{email-sender → src/email-sender}/email-sender.html +0 -0
@@ -1,304 +0,0 @@
1
- module.exports = function (RED) {
2
- const Imap = require('node-imap');
3
- const mailparser = require('mailparser');
4
-
5
- function toBoolean(val, defaultValue = false) {
6
- if (typeof val === "boolean") return val; // schon korrekt
7
- if (typeof val === "number") return val !== 0; // 0 = false, sonst true
8
- if (typeof val === "string") {
9
- const v = val.trim().toLowerCase();
10
- if (["true", "1", "yes", "on"].includes(v)) return true;
11
- if (["false", "0", "no", "off"].includes(v)) return false;
12
- }
13
- return defaultValue; // fallback
14
- }
15
-
16
- function EmailReceiverNode(config) {
17
- RED.nodes.createNode(this, config);
18
- const node = this;
19
-
20
- node.on('input', function (msg) {
21
- // Retrieve and validate configuration values
22
- const imap_host = RED.util.evaluateNodeProperty(config.host, config.hostType, node, msg);
23
- const imap_port = RED.util.evaluateNodeProperty(config.port, config.portType, node, msg);
24
- const imap_tls = RED.util.evaluateNodeProperty(config.tls, config.tlsType, node, msg);
25
- const imap_user = RED.util.evaluateNodeProperty(config.user, config.userType, node, msg);
26
- const imap_password = RED.util.evaluateNodeProperty(config.password, config.passwordType, node, msg);
27
- const sendstatus = config.sendstatus === true || config.sendstatus === 'true';
28
-
29
- // Check if the folder is actually an array
30
- const imap_folder = RED.util.evaluateNodeProperty(config.folder, config.folderType, node, msg);
31
- let folders;
32
- if (Array.isArray(imap_folder)) {
33
- folders = imap_folder;
34
- } else {
35
- const errorMsg = "The 'folders' property must be an array of strings.";
36
- node.status({ fill: 'red', shape: 'ring', text: errorMsg });
37
- node.error(errorMsg, msg);
38
- return;
39
- }
40
- const imap_markSeen = RED.util.evaluateNodeProperty(config.markseen, config.markseenType, node, msg);
41
-
42
- const finalConfig = {
43
- host: imap_host,
44
- port: typeof imap_port === 'string' ? parseInt(imap_port, 10) : imap_port,
45
- tls: imap_tls,
46
- user: imap_user,
47
- password: imap_password,
48
- folders: Array.isArray(imap_folder)
49
- ? imap_folder
50
- : imap_folder
51
- .split(',')
52
- .map((f) => f.trim())
53
- .filter((f) => f.length > 0),
54
- markSeen: toBoolean(imap_markSeen, true),
55
- connTimeout: msg.imap_connTimeout || 10000,
56
- authTimeout: msg.imap_authTimeout || 5000,
57
- keepalive: msg.imap_keepalive !== undefined ? msg.imap_keepalive : true,
58
- autotls: msg.imap_autotls || 'never',
59
- tlsOptions: msg.imap_tlsOptions || { rejectUnauthorized: false },
60
- };
61
-
62
- if (
63
- !finalConfig.user ||
64
- !finalConfig.password ||
65
- !finalConfig.port ||
66
- !finalConfig.host ||
67
- !finalConfig.folders
68
- ) {
69
- const missingFields = [];
70
- if (!finalConfig.user) missingFields.push('user');
71
- if (!finalConfig.password) missingFields.push('password');
72
- if (!finalConfig.port) missingFields.push('port');
73
- if (!finalConfig.host) missingFields.push('host');
74
- if (!finalConfig.folders) missingFields.push('folders');
75
-
76
- const errorMessage = `Missing required IMAP config: ${missingFields.join(', ')}. Aborting.`;
77
- node.status({ fill: 'red', shape: 'ring', text: 'missing config' });
78
- node.error(errorMessage);
79
- return;
80
- }
81
-
82
- const fetchEmails = (
83
- {
84
- host,
85
- port,
86
- tls,
87
- user,
88
- password,
89
- folders,
90
- markSeen = true,
91
- connTimeout = 10000,
92
- authTimeout = 5000,
93
- keepalive = true,
94
- autotls = 'never',
95
- tlsOptions = { rejectUnauthorized: false },
96
- },
97
- onMail,
98
- ) => {
99
- const imap = new Imap({
100
- user,
101
- password,
102
- host,
103
- port,
104
- tls,
105
- connTimeout,
106
- authTimeout,
107
- keepalive,
108
- autotls,
109
- tlsOptions,
110
- });
111
-
112
- const state = {
113
- totalFolders: folders.length,
114
- processedFolders: 0,
115
- folderCount: {},
116
- successes: 0,
117
- failures: 0,
118
- totalMails: 0,
119
- errors: [],
120
- };
121
-
122
- // Helper to update Node-RED status
123
- const updateStatus = (color, text) => {
124
- node.status({ fill: color, shape: 'dot', text });
125
- };
126
-
127
- // Helper to finalize status and clean up
128
- const finalizeSession = (error = null) => {
129
- if (error) {
130
- node.error('IMAP session terminated: ' + error.message);
131
- node.status({ fill: 'red', shape: 'ring', text: 'connection error' });
132
- if (sendstatus) {
133
- node.send([null, {
134
- payload: {
135
- status: 'error',
136
- message: error.message,
137
- errors: state.errors,
138
- }
139
- }]);
140
- }
141
- } else if (state.failures > 0) {
142
- node.status({
143
- fill: 'red',
144
- shape: 'dot',
145
- text: `Done, ${state.totalMails} mails from ${state.successes}/${state.totalFolders} folders. ${state.failures} failed.`,
146
- });
147
- if (sendstatus) {
148
- node.send([null, {
149
- payload: {
150
- status: 'warning',
151
- total: state.totalMails,
152
- successes: state.successes,
153
- failures: state.failures,
154
- totalFolders: state.totalFolders,
155
- errors: state.errors,
156
- }
157
- }]);
158
- }
159
-
160
- } else {
161
- node.status({
162
- fill: 'green',
163
- shape: 'dot',
164
- text: `Done, fetched ${state.totalMails} mails from ${folders.join(', ')}.`,
165
- });
166
-
167
- if (sendstatus) {
168
- node.send([null, {
169
- payload: {
170
- status: 'success',
171
- total: state.totalMails,
172
- folderCount: state.folderCount,
173
- folders: folders.join(', '),
174
- }
175
- }]);
176
- }
177
-
178
- }
179
- if (imap && imap.state !== 'disconnected') {
180
- imap.end();
181
- }
182
- };
183
-
184
- const fetchFromFolder = (folder) => {
185
- updateStatus('yellow', `Fetching from "${folder}"...`);
186
-
187
- imap.openBox(folder, false, (err, box) => {
188
- if (err) {
189
- node.error(`Could not open folder "${folder}": ${err.message}`);
190
- state.failures++;
191
- state.processedFolders++;
192
- return startNextFolder();
193
- }
194
-
195
- state.folderCount[folder] = 0;
196
-
197
- imap.search(['UNSEEN'], (err, results) => {
198
- if (err) {
199
- node.error(`Search failed in folder "${folder}": ${err.message}`);
200
- state.failures++;
201
- state.processedFolders++;
202
- return startNextFolder();
203
- }
204
-
205
- if (!results || !results.length) {
206
- state.successes++;
207
- state.processedFolders++;
208
- return startNextFolder();
209
- }
210
-
211
- state.totalMails += results.length;
212
-
213
- const fetch = imap.fetch(results, { bodies: '', markSeen: markSeen });
214
-
215
- fetch.on('message', (msg) => {
216
- msg.on('body', (stream) => {
217
- mailparser.simpleParser(stream, (err, parsed) => {
218
- if (err) {
219
- node.error(`Parse error for email from folder "${folder}": ${err.message}`);
220
- return;
221
- }
222
-
223
- const outMsg = {
224
- topic: parsed.subject,
225
- payload: parsed.text,
226
- html: parsed.html,
227
- from: parsed.replyTo?.text || parsed.from?.text,
228
- date: parsed.date,
229
- folder,
230
- header: parsed.headers,
231
- attachments: parsed.attachments.map((att) => ({
232
- contentType: att.contentType,
233
- fileName: att.filename,
234
- transferEncoding: att.transferEncoding,
235
- contentDisposition: att.contentDisposition,
236
- generatedFileName: att.cid || att.checksum,
237
- contentId: att.cid,
238
- checksum: att.checksum,
239
- length: att.size,
240
- content: att.content,
241
- })),
242
- };
243
- state.folderCount[folder] = (state.folderCount[folder] || 0) + 1;
244
- onMail(outMsg);
245
- });
246
- });
247
- });
248
-
249
- fetch.once('error', (err) => {
250
- node.error(`Fetch error in folder "${folder}": ${err.message}`);
251
- });
252
-
253
- fetch.once('end', () => {
254
- state.successes++;
255
- state.processedFolders++;
256
- updateStatus('green', `Fetched ${results.length} from "${folder}".`);
257
- startNextFolder();
258
- });
259
- });
260
- });
261
- };
262
-
263
- const startNextFolder = () => {
264
- if (state.processedFolders >= state.totalFolders) {
265
- finalizeSession();
266
- } else {
267
- fetchFromFolder(folders[state.processedFolders]);
268
- }
269
- };
270
-
271
- // Centralized event listeners for the IMAP connection
272
- imap.once('ready', () => {
273
- node.status({ fill: 'green', shape: 'dot', text: 'connected' });
274
- startNextFolder();
275
- });
276
-
277
- imap.once('error', (err) => {
278
- finalizeSession(err);
279
- });
280
-
281
- imap.once('end', () => {
282
- node.log('IMAP connection ended.');
283
- });
284
-
285
- try {
286
- updateStatus('yellow', 'Connecting to IMAP...');
287
- imap.connect();
288
- } catch (err) {
289
- updateStatus('red', 'Connection error: ' + err.message);
290
- }
291
- };
292
-
293
- fetchEmails(finalConfig, (mail) => {
294
- node.send(mail);
295
- });
296
- });
297
- }
298
-
299
- RED.nodes.registerType('email-receiver', EmailReceiverNode, {
300
- credentials: {
301
- password: { type: 'password' },
302
- },
303
- });
304
- };
@@ -1,178 +0,0 @@
1
- module.exports = function (RED) {
2
- 'use strict';
3
- const nodemailer = require('nodemailer');
4
-
5
- function EmailSenderNode(config) {
6
- RED.nodes.createNode(this, config);
7
- var node = this;
8
-
9
- node.on('input', function (msg, send, done) {
10
- send =
11
- send ||
12
- function () {
13
- node.send.apply(node, arguments);
14
- };
15
- done =
16
- done ||
17
- function (err) {
18
- if (err) node.error(err, msg);
19
- };
20
-
21
- // Retrieve and evaluate mail configuration values
22
- const sender = RED.util.evaluateNodeProperty(config.sender, config.senderType, node, msg);
23
- const address = RED.util.evaluateNodeProperty(config.address, config.addressType, node, msg);
24
- const to = RED.util.evaluateNodeProperty(config.to, config.toType, node, msg);
25
- const cc = RED.util.evaluateNodeProperty(config.cc, config.ccType, node, msg) || '';
26
- const bcc = RED.util.evaluateNodeProperty(config.bcc, config.bccType, node, msg) || '';
27
- const replyTo = RED.util.evaluateNodeProperty(config.replyTo, config.replyToType, node, msg) || '';
28
- const subject =
29
- RED.util.evaluateNodeProperty(config.subject, config.subjectType, node, msg) ||
30
- msg.topic ||
31
- 'Message from Node-RED';
32
- const htmlContent = RED.util.evaluateNodeProperty(config.htmlContent, config.htmlContentType, node, msg);
33
- const attachments = safeEvaluatePropertyAttachment(config, node, msg);
34
-
35
- // Retrieve and evaluate SMTP configuration values
36
- const host = RED.util.evaluateNodeProperty(config.host, config.hostType, node, msg);
37
- const port = RED.util.evaluateNodeProperty(config.port, config.portType, node, msg);
38
- const user = RED.util.evaluateNodeProperty(config.user, config.userType, node, msg);
39
- const password = RED.util.evaluateNodeProperty(config.password, config.passwordType, node, msg);
40
- const secure = RED.util.evaluateNodeProperty(config.secure, config.secureType, node, msg);
41
- const rejectUnauthorized = RED.util.evaluateNodeProperty(
42
- config.rejectUnauthorized,
43
- config.rejectUnauthorizedType,
44
- node,
45
- msg,
46
- );
47
-
48
- // Handle attachments and format them for Nodemailer
49
- let processedAttachments = [];
50
-
51
- let parsedAttachments = attachments;
52
-
53
- if (config.attachmentsType === 'json' && typeof parsedAttachments === 'string') {
54
- try {
55
- parsedAttachments = JSON.parse(parsedAttachments);
56
- } catch (e) {
57
- node.error('Failed to parse attachments JSON: ' + e.message);
58
- return;
59
- }
60
- }
61
-
62
- if (parsedAttachments) {
63
- // Check if it's a single attachment or an array
64
- const attachmentArray = Array.isArray(parsedAttachments) ? parsedAttachments : [parsedAttachments];
65
-
66
- for (const attachment of attachmentArray) {
67
- try {
68
- // Assuming the attachment object has a 'filename' and 'content' property
69
- if (attachment.filename && attachment.content) {
70
- processedAttachments.push({
71
- filename: attachment.filename,
72
- content: attachment.content,
73
- });
74
- } else {
75
- node.status({ fill: 'red', shape: 'dot', text: 'attachment error' });
76
- node.error("Attachment object is missing 'filename' or 'content' property.");
77
- return;
78
- }
79
- } catch (e) {
80
- node.error('Failed to process attachment: ' + e.message);
81
- }
82
- }
83
- }
84
-
85
- // Create SMTP transporter
86
- const transporter = nodemailer.createTransport({
87
- host: host,
88
- port: port,
89
- secure: secure,
90
- auth: {
91
- user: user,
92
- pass: password,
93
- },
94
- tls: {
95
- rejectUnauthorized: rejectUnauthorized,
96
- },
97
- });
98
-
99
- // Create email object
100
- const mailOptions = {
101
- from: {
102
- name: sender,
103
- address: address,
104
- },
105
- to: to,
106
- cc: cc,
107
- bcc: bcc,
108
- replyTo: replyTo,
109
- subject: subject,
110
- html: Buffer.from(htmlContent, 'utf-8'),
111
- attachments: processedAttachments,
112
- };
113
-
114
- // Send email
115
- transporter.sendMail(mailOptions, (error, info) => {
116
- if (error) {
117
- node.status({ fill: 'red', shape: 'dot', text: 'error sending' });
118
- if (
119
- error.message &&
120
- error.message.includes('SSL routines') &&
121
- error.message.includes('wrong version number')
122
- ) {
123
- // Improved error message for SSL/TLS issues
124
- done(
125
- new Error(
126
- 'SSL/TLS connection failed: Wrong version number. ' +
127
- 'This usually means the wrong port or security settings are used. ' +
128
- 'For SMTP: use port 587 with secure=false (STARTTLS) or port 465 with secure=true (SSL/TLS).',
129
- ),
130
- );
131
- } else {
132
- done(error);
133
- }
134
- } else {
135
- node.log('Email sent: ' + info.response);
136
- msg.payload = info;
137
-
138
- if (msg.payload.accepted && msg.payload.accepted.length > 0) {
139
- msg.payload = msg.input;
140
- node.status({ fill: 'green', shape: 'dot', text: 'sent' });
141
- send(msg);
142
- done();
143
- } else if (msg.payload.rejected && msg.payload.rejected.length > 0) {
144
- msg.error = { result: msg.payload.rejected };
145
- node.status({ fill: 'red', shape: 'dot', text: 'rejected' });
146
- done(new Error('Email rejected: ' + msg.payload.rejected.join(', ')));
147
- } else if (msg.payload.pending && msg.payload.pending.length > 0) {
148
- msg.error = { result: msg.payload.pending };
149
- node.status({ fill: 'yellow', shape: 'dot', text: 'pending' });
150
- done(new Error('Email pending: ' + msg.payload.pending.join(', ')));
151
- } else {
152
- node.status({ fill: 'red', shape: 'dot', text: 'unknown error' });
153
- done(new Error('Unknown error while sending email.'));
154
- }
155
- }
156
- });
157
- });
158
- }
159
-
160
- function safeEvaluatePropertyAttachment(config, node, msg) {
161
- if (config.attachments && config.attachments.trim() !== '') {
162
- try {
163
- return RED.util.evaluateNodeProperty(config.attachments, config.attachmentsType, node, msg);
164
- } catch (e) {
165
- node.error('Failed to evaluate attachments property: ' + e.message, msg);
166
- return null;
167
- }
168
- }
169
-
170
- return null;
171
- }
172
-
173
- RED.nodes.registerType('email-sender', EmailSenderNode, {
174
- credentials: {
175
- password: { type: 'password' },
176
- },
177
- });
178
- };
package/examples/.gitkeep DELETED
File without changes
@@ -1,22 +0,0 @@
1
- module.exports = function (RED) {
2
- const { compile } = require('html-to-text');
3
-
4
- function ProcesscubeHtmlToText(config) {
5
- RED.nodes.createNode(this, config);
6
- const node = this;
7
-
8
- const options = {
9
- wordwrap: 130,
10
- // ...
11
- };
12
- const compiledConvert = compile(options); // options passed here
13
-
14
- node.on('input', async function (msg) {
15
- msg.payload = compiledConvert(msg.payload);
16
-
17
- node.send(msg);
18
- });
19
- }
20
-
21
- RED.nodes.registerType('processcube-html-to-text', ProcesscubeHtmlToText);
22
- };