@5minds/node-red-contrib-processcube-tools 1.2.0-develop-2eb127-mg68t7xt → 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
package/.mocharc.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "require": ["ts-node/register"],
3
+ "spec": "src/test/**/*.test.ts",
4
+ "timeout": 5000
5
+ }
package/package.json CHANGED
@@ -1,11 +1,16 @@
1
1
  {
2
2
  "name": "@5minds/node-red-contrib-processcube-tools",
3
- "version": "1.2.0-develop-2eb127-mg68t7xt",
3
+ "version": "1.2.0-develop-59ef22-mg9d9ja5",
4
4
  "license": "MIT",
5
5
  "description": "Node-RED tools nodes for ProcessCube",
6
6
  "scripts": {
7
- "lint": "prettier --write --config ./.prettierrc.json \"**/*.{html,js}\"",
8
- "test": "mocha test/unit/ test/integration"
7
+ "build": "npm-run-all build:*",
8
+ "build:ts": "tsc",
9
+ "build:html": "cpx \"./src/**/*.html\" ./dist",
10
+ "lint": "prettier --write --config ./.prettierrc.json \"**/*.{html,ts}\"",
11
+ "test": "mocha --require ts-node/register 'src/test/**/*.test.ts'",
12
+ "test:unit": "mocha --require ts-node/register 'src/test/unit/**/*.test.ts'",
13
+ "test:integration": "mocha --require ts-node/register 'src/test/integration/**/*.test.ts'"
9
14
  },
10
15
  "authors": [
11
16
  {
@@ -34,32 +39,43 @@
34
39
  "npm": ">=8.0.0"
35
40
  },
36
41
  "node-red": {
37
- "version": ">=3.1.9",
42
+ "version": "^4.1.0",
38
43
  "nodes": {
39
- "EmailReceiver": "email-receiver/email-receiver.js",
40
- "EmailSender": "email-sender/email-sender.js",
41
- "HtmlToText": "processcube-html-to-text/processcube-html-to-text.js"
44
+ "EmailReceiver": "dist/email-receiver/email-receiver.js",
45
+ "EmailSender": "dist/email-sender/email-sender.js",
46
+ "HtmlToText": "dist/html-to-text/html-to-text.js"
42
47
  },
43
48
  "examples": "examples"
44
49
  },
45
50
  "dependencies": {
46
51
  "html-to-text": "^9.0.5",
47
- "mailparser": "^3.6.8",
52
+ "mailparser": "^3.7.4",
48
53
  "node-imap": "^0.9.6",
49
54
  "nodemailer": "^7.0.6",
50
55
  "utf7": "^1.0.2"
51
56
  },
52
57
  "devDependencies": {
58
+ "@types/chai": "^5.2.2",
59
+ "@types/mailparser": "^3.4.6",
60
+ "@types/mocha": "^10.0.10",
61
+ "@types/node": "^24.5.2",
62
+ "@types/node-imap": "^0.9.3",
63
+ "@types/node-red": "^1.3.5",
64
+ "@types/node-red-node-test-helper": "^0.3.4",
65
+ "@types/nodemailer": "^7.0.1",
53
66
  "chai": "^4.3.4",
67
+ "cpx2": "^8.0.0",
54
68
  "mocha": "^11.7.2",
55
69
  "node-red": "^4.0.9",
56
- "node-red-node-test-helper": "^0.3.5"
70
+ "node-red-node-test-helper": "^0.3.5",
71
+ "npm-run-all": "^4.1.5",
72
+ "ts-node": "^10.9.2",
73
+ "typescript": "^5.9.2"
57
74
  },
58
75
  "overrides": {
59
76
  "semver": ">=7.5.2",
60
77
  "axios": ">=1.12.0",
61
78
  "html-to-text": "^9.0.5",
62
- "mailparser": "^3.6.8",
63
79
  "node-imap": "^0.9.6",
64
80
  "nodemailer": "^7.0.6",
65
81
  "utf7": "^1.0.2"
@@ -0,0 +1,45 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('my-new-node-name', {
3
+ category: 'ProcessCube Tools',
4
+ color: '#02AFD6',
5
+ defaults: {
6
+ name: { value: '' },
7
+ // Füge hier deine Konfigurations-Eigenschaften hinzu
8
+ // Beispiel:
9
+ // myProperty: { value: 'standardwert', required: true, validate: RED.validators.typedInput('myPropertyType') },
10
+ // myPropertyType: { value: 'str' }
11
+ },
12
+ inputs: 1, // Anzahl der Eingangsports
13
+ outputs: 1, // Anzahl der Ausgangsports
14
+ icon: 'font-awesome/fa-tag', // Symbol für die Node
15
+ label: function () {
16
+ return this.name || 'Meine Neue Node';
17
+ },
18
+ oneditprepare: function () {
19
+ // Hier konfigurierst du die TypedInput-Felder, die du im defaults-Block definiert hast.
20
+ // $('#node-input-myProperty').typedInput({
21
+ // default: 'str',
22
+ // types: ['str', 'msg', 'flow', 'global', 'env'],
23
+ // typeField: '#node-input-myPropertyType',
24
+ // });
25
+ },
26
+ });
27
+ </script>
28
+
29
+ <script type="text/html" data-template-name="my-new-node-name">
30
+ <div class="form-row">
31
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
32
+ <input type="text" id="node-input-name" placeholder="Name" />
33
+ </div>
34
+
35
+ </script>
36
+
37
+ <script type="text/html" data-help-name="my-new-node-name">
38
+ <p>Eine einfache Node-RED-Node-Vorlage, die eine grundlegende Struktur für neue Nodes bietet.</p>
39
+
40
+ <h3>Konfiguration</h3>
41
+ <dl class="message-properties">
42
+ <dt>Name</dt>
43
+ <dd>Der Name deiner Node, wie er im Flow angezeigt wird.</dd>
44
+ </dl>
45
+ </script>
@@ -0,0 +1,69 @@
1
+ import { NodeInitializer, Node, NodeDef, NodeMessage } from 'node-red';
2
+ // Wenn du eine externe Bibliothek benötigst, importiere sie hier.
3
+ // const myExternalLibrary = require('my-external-library');
4
+
5
+ // Definiere ein Interface für die Konfigurations-Eigenschaften deines Nodes.
6
+ // Ersetze 'MyNewNode' durch den Namen deiner Node.
7
+ interface MyNewNodeProperties extends NodeDef {
8
+ // Füge hier deine benutzerdefinierten Konfigurationseigenschaften hinzu.
9
+ // Zum Beispiel:
10
+ // myConfigProperty: string;
11
+ }
12
+
13
+ // Definiere ein Interface für die Nachrichten-Payload, falls sie einen bestimmten Typ haben muss.
14
+ // Ersetze 'MyNewNode' durch den Namen deiner Node.
15
+ interface MyNewNodeMessage extends NodeMessage {
16
+ payload: any; // Ändere 'any' in den erwarteten Typ (z.B. 'string', 'number').
17
+ }
18
+
19
+ // Die Initialisierungsfunktion, die von Node-RED geladen wird.
20
+ // Ersetze 'MyNewNode' durch den Namen deiner Node.
21
+ const MyNewNode: NodeInitializer = function(RED) {
22
+ // Die Hauptfunktion für deine Node.
23
+ // Ersetze 'MyNewNode' durch den Namen deiner Node.
24
+ function MyNewNode(this: Node, config: MyNewNodeProperties) {
25
+ RED.nodes.createNode(this, config);
26
+ const node = this;
27
+
28
+ // Speichere die Konfiguration in der Node-Instanz.
29
+ // node.myConfigProperty = config.myConfigProperty;
30
+
31
+ // Registriere einen Listener für eingehende Nachrichten.
32
+ (node as any).on("input", (msg: MyNewNodeMessage, send?: Function, done?: Function) => {
33
+ // Stelle sicher, dass 'send' und 'done' verfügbar sind.
34
+ send = send || function(m: NodeMessage | NodeMessage[]) { node.send(m); };
35
+ done = done || function(err?: Error) { if (err) node.error(err, msg); };
36
+
37
+ try {
38
+ // --- Hier kommt deine eigentliche Node-Logik rein ---
39
+ // Beispiel:
40
+ // if (typeof msg.payload === 'string') {
41
+ // msg.payload = "Hallo, das ist eine neue Node!";
42
+ // }
43
+ // --------------------------------------------------
44
+
45
+ // Sende die Nachricht weiter an die nächste Node.
46
+ send(msg);
47
+
48
+ // Signalisiere, dass die Verarbeitung abgeschlossen ist.
49
+ done();
50
+
51
+ } catch (error) {
52
+ // Bei einem Fehler, rufe `done` mit dem Fehlerobjekt auf.
53
+ done(error instanceof Error ? error : new Error(String(error)));
54
+ }
55
+ });
56
+
57
+ // Optional: Füge hier einen 'close'-Handler hinzu, um Ressourcen freizugeben.
58
+ // (node as any).on("close", (done) => {
59
+ // // Aufräumarbeiten (z.B. Verbindung schließen)
60
+ // done();
61
+ // });
62
+ }
63
+
64
+ // Registriere den Node-Typ bei Node-RED.
65
+ // Der String hier muss dem Namen in deiner .html-Datei entsprechen.
66
+ RED.nodes.registerType('my-new-node-name', MyNewNode);
67
+ };
68
+
69
+ export = MyNewNode;
@@ -0,0 +1,439 @@
1
+ import { NodeInitializer, Node, NodeDef, NodeMessageInFlow, NodeMessage } from 'node-red';
2
+ import Imap, { ImapMessageAttributes } from 'node-imap';
3
+ import { simpleParser, ParsedMail, Attachment } from 'mailparser';
4
+ import type { ImapConnectionConfig } from '../interfaces/ImapConnectionConfig';
5
+ import type { FetchState } from '../interfaces/FetchState';
6
+ import type { EmailReceiverMessage } from '../interfaces/EmailReceiverMessage';
7
+
8
+ interface EmailReceiverNodeProperties extends NodeDef {
9
+ host: string;
10
+ hostType: string;
11
+ port: number;
12
+ portType: string;
13
+ tls: boolean;
14
+ tlsType: string;
15
+ user: string;
16
+ userType: string;
17
+ password: string;
18
+ passwordType: string;
19
+ folder: string | string[];
20
+ folderType: string;
21
+ markseen: boolean;
22
+ markseenType: string;
23
+ sendstatus: boolean | string;
24
+ }
25
+
26
+ interface EmailReceiverNodeMessage extends NodeMessageInFlow {}
27
+
28
+ // Dependency injection interface
29
+ interface Dependencies {
30
+ ImapClient: typeof Imap;
31
+ mailParser: typeof simpleParser;
32
+ }
33
+
34
+ // Default dependencies - production values
35
+ const defaultDependencies: Dependencies = {
36
+ ImapClient: Imap,
37
+ mailParser: simpleParser,
38
+ };
39
+
40
+ function toBoolean(val: any, defaultValue = false) {
41
+ if (typeof val === 'boolean') return val;
42
+ if (typeof val === 'number') return val !== 0;
43
+ if (typeof val === 'string') {
44
+ const v = val.trim().toLowerCase();
45
+ if (['true', '1', 'yes', 'on'].includes(v)) return true;
46
+ if (['false', '0', 'no', 'off'].includes(v)) return false;
47
+ }
48
+ return defaultValue;
49
+ }
50
+
51
+ const nodeInit: NodeInitializer = (RED, dependencies: Dependencies = defaultDependencies) => {
52
+ function EmailReceiverNode(this: Node, config: EmailReceiverNodeProperties) {
53
+ RED.nodes.createNode(this, config);
54
+ const node = this;
55
+
56
+ // Store configuration validation error without failing construction
57
+ let configError: Error | null = null;
58
+
59
+ try {
60
+ // Validate folder configuration first
61
+ if (typeof config.folder === 'number') {
62
+ throw new Error("The 'folders' property must be an array of strings.");
63
+ }
64
+
65
+ if (Array.isArray(config.folder)) {
66
+ if (!config.folder.every((f) => typeof f === 'string')) {
67
+ throw new Error("The 'folders' property must be an array of strings.");
68
+ }
69
+ } else if (typeof config.folder !== 'string' && config.folder !== undefined) {
70
+ throw new Error("The 'folders' property must be an array of strings.");
71
+ }
72
+
73
+ // Validate required fields - check both explicit types and default string values
74
+ const requiredFields: Array<{
75
+ key: keyof EmailReceiverNodeProperties;
76
+ typeKey: keyof EmailReceiverNodeProperties;
77
+ }> = [
78
+ { key: 'host', typeKey: 'hostType' },
79
+ { key: 'user', typeKey: 'userType' },
80
+ { key: 'password', typeKey: 'passwordType' },
81
+ { key: 'port', typeKey: 'portType' },
82
+ ];
83
+
84
+ const missingFields: string[] = [];
85
+ requiredFields.forEach(({ key, typeKey }) => {
86
+ const value = config[key];
87
+ const type = config[typeKey] || 'str'; // Default to 'str' if type not specified
88
+
89
+ // Check for missing or empty values when type is string
90
+ if (type === 'str' && (!value || value === '' || (typeof value === 'string' && value.trim() === ''))) {
91
+ missingFields.push(key as string);
92
+ }
93
+ // Also check for null/undefined regardless of type
94
+ if (value === null || value === undefined) {
95
+ missingFields.push(key as string);
96
+ }
97
+ });
98
+
99
+ if (missingFields.length > 0) {
100
+ throw new Error(`Missing required IMAP config: ${missingFields.join(', ')}. Aborting.`);
101
+ }
102
+ } catch (error) {
103
+ configError = error instanceof Error ? error : new Error(String(error));
104
+ node.status({ fill: 'red', shape: 'ring', text: 'config error' });
105
+
106
+ // Store error for test framework to detect
107
+ (node as any).configError = configError;
108
+
109
+ // Emit error immediately during construction for test framework
110
+ // Use setImmediate to ensure node is fully constructed first
111
+ setImmediate(() => {
112
+ node.error(configError!.message);
113
+ });
114
+ }
115
+
116
+ node.on('input', (msg: EmailReceiverNodeMessage, send: Function, done: Function) => {
117
+ send =
118
+ send ||
119
+ function (m: NodeMessage | NodeMessage[]) {
120
+ node.send(m);
121
+ };
122
+ done =
123
+ done ||
124
+ function (err?: Error) {
125
+ if (err) node.error(err, msg);
126
+ };
127
+
128
+ // If there's a configuration error, report it and don't proceed
129
+ if (configError) {
130
+ node.error(configError.message);
131
+ done(configError);
132
+ return;
133
+ }
134
+
135
+ try {
136
+ const imap_host = RED.util.evaluateNodeProperty(config.host, config.hostType, node, msg);
137
+ const imap_port = RED.util.evaluateNodeProperty(String(config.port), config.portType, node, msg);
138
+ const imap_tls = RED.util.evaluateNodeProperty(String(config.tls), config.tlsType, node, msg);
139
+ const imap_user = RED.util.evaluateNodeProperty(config.user, config.userType, node, msg);
140
+ const imap_password = RED.util.evaluateNodeProperty(config.password, config.passwordType, node, msg);
141
+ const sendstatus = config.sendstatus === true || config.sendstatus === 'true';
142
+
143
+ const imap_markSeen = RED.util.evaluateNodeProperty(
144
+ String(config.markseen),
145
+ config.markseenType,
146
+ node,
147
+ msg,
148
+ );
149
+ const imap_folder = RED.util.evaluateNodeProperty(String(config.folder), config.folderType, node, msg);
150
+ let folders: string[];
151
+
152
+ if (Array.isArray(imap_folder)) {
153
+ folders = imap_folder as string[];
154
+ } else if (typeof imap_folder === 'string') {
155
+ folders = imap_folder
156
+ .split(',')
157
+ .map((f) => f.trim())
158
+ .filter((f) => f.length > 0);
159
+ } else {
160
+ throw new Error("The 'folders' property must be an array of strings.");
161
+ }
162
+
163
+ const finalConfig: ImapConnectionConfig = {
164
+ host: imap_host as string,
165
+ port: typeof imap_port === 'string' ? parseInt(imap_port, 10) : (imap_port as number),
166
+ tls: imap_tls as boolean,
167
+ user: imap_user as string,
168
+ password: imap_password as string,
169
+ folders: folders,
170
+ markSeen: toBoolean(imap_markSeen, true),
171
+ connTimeout: (msg as any).imap_connTimeout || 10000,
172
+ authTimeout: (msg as any).imap_authTimeout || 5000,
173
+ keepalive: (msg as any).imap_keepalive ?? true,
174
+ autotls: (msg as any).imap_autotls || 'never',
175
+ tlsOptions: (msg as any).imap_tlsOptions || { rejectUnauthorized: false },
176
+ };
177
+
178
+ // Enhanced validation after property evaluation
179
+ if (
180
+ !finalConfig.user ||
181
+ !finalConfig.password ||
182
+ !finalConfig.port ||
183
+ !finalConfig.host ||
184
+ !finalConfig.folders ||
185
+ finalConfig.folders.length === 0
186
+ ) {
187
+ const missingFields: string[] = [];
188
+ if (!finalConfig.user) missingFields.push('user');
189
+ if (!finalConfig.password) missingFields.push('password');
190
+ if (!finalConfig.port) missingFields.push('port');
191
+ if (!finalConfig.host) missingFields.push('host');
192
+ if (!finalConfig.folders || finalConfig.folders.length === 0) missingFields.push('folders');
193
+ throw new Error(`Missing required IMAP config: ${missingFields.join(', ')}. Aborting.`);
194
+ }
195
+
196
+ const fetchEmails = (
197
+ fetchConfig: ImapConnectionConfig,
198
+ onMail: (mail: EmailReceiverMessage) => void,
199
+ ) => {
200
+ // Use injected dependency instead of direct import
201
+ const imap = new dependencies.ImapClient({
202
+ user: fetchConfig.user,
203
+ password: fetchConfig.password,
204
+ host: fetchConfig.host,
205
+ port: fetchConfig.port,
206
+ tls: fetchConfig.tls,
207
+ connTimeout: fetchConfig.connTimeout,
208
+ authTimeout: fetchConfig.authTimeout,
209
+ keepalive: fetchConfig.keepalive,
210
+ autotls: fetchConfig.autotls as 'always' | 'never' | 'required',
211
+ tlsOptions: fetchConfig.tlsOptions,
212
+ });
213
+
214
+ const state: FetchState = {
215
+ totalFolders: fetchConfig.folders.length,
216
+ processedFolders: 0,
217
+ folderCount: {},
218
+ successes: 0,
219
+ failures: 0,
220
+ totalMails: 0,
221
+ errors: [],
222
+ };
223
+
224
+ const updateStatus = (color: string, text: string) => {
225
+ node.status({ fill: color as any, shape: 'dot', text });
226
+ };
227
+
228
+ const finalizeSession = (error: Error | null = null) => {
229
+ if (error) {
230
+ state.errors.push(error);
231
+ node.error('IMAP session terminated: ' + error.message);
232
+ node.status({ fill: 'red', shape: 'ring', text: 'connection error' });
233
+ if (sendstatus) {
234
+ node.send([
235
+ null,
236
+ {
237
+ payload: {
238
+ status: 'error',
239
+ message: error.message,
240
+ errors: state.errors,
241
+ },
242
+ },
243
+ ]);
244
+ }
245
+ done(error);
246
+ } else if (state.failures > 0) {
247
+ node.status({
248
+ fill: 'red',
249
+ shape: 'dot',
250
+ text: `Done, ${state.totalMails} mails from ${state.successes}/${state.totalFolders} folders. ${state.failures} failed.`,
251
+ });
252
+ if (sendstatus) {
253
+ node.send([
254
+ null,
255
+ {
256
+ payload: {
257
+ status: 'warning',
258
+ total: state.totalMails,
259
+ successes: state.successes,
260
+ failures: state.failures,
261
+ totalFolders: state.totalFolders,
262
+ errors: state.errors,
263
+ },
264
+ },
265
+ ]);
266
+ }
267
+ } else {
268
+ node.status({
269
+ fill: 'green',
270
+ shape: 'dot',
271
+ text: `Done, fetched ${state.totalMails} mails from ${fetchConfig.folders.join(', ')}.`,
272
+ });
273
+
274
+ if (sendstatus) {
275
+ node.send([
276
+ null,
277
+ {
278
+ payload: {
279
+ status: 'success',
280
+ total: state.totalMails,
281
+ folderCount: state.folderCount,
282
+ folders: folders.join(', '),
283
+ },
284
+ },
285
+ ]);
286
+ }
287
+ }
288
+ if (imap && imap.state !== 'disconnected') {
289
+ imap.end();
290
+ }
291
+ };
292
+
293
+ const fetchFromFolder = (folder: string) => {
294
+ updateStatus('yellow', `Fetching from "${folder}"...`);
295
+
296
+ imap.openBox(folder, false, (err: Error | null, box: Imap.Box | null) => {
297
+ if (err) {
298
+ node.error(`Could not open folder "${folder}": ${err.message}`);
299
+ state.failures++;
300
+ state.processedFolders++;
301
+ return startNextFolder();
302
+ }
303
+
304
+ state.folderCount[folder] = 0;
305
+
306
+ imap.search(['UNSEEN'], (err: Error | null, results: number[]) => {
307
+ if (err) {
308
+ node.error(`Search failed in folder "${folder}": ${err.message}`);
309
+ state.failures++;
310
+ state.processedFolders++;
311
+ return startNextFolder();
312
+ }
313
+
314
+ if (!results || !results.length) {
315
+ state.successes++;
316
+ state.processedFolders++;
317
+ return startNextFolder();
318
+ }
319
+
320
+ state.totalMails += results.length;
321
+
322
+ const fetch = imap.fetch(results, { bodies: '', markSeen: finalConfig.markSeen });
323
+
324
+ fetch.on('message', (msg: Imap.ImapMessage, seqno: number) => {
325
+ msg.on('body', (stream: NodeJS.ReadableStream) => {
326
+ // Use injected dependency instead of direct import
327
+ dependencies.mailParser(
328
+ stream as any,
329
+ (err: Error | null, parsed: ParsedMail) => {
330
+ if (err) {
331
+ node.error(
332
+ `Parse error for email from folder "${folder}": ${err.message}`,
333
+ );
334
+ return;
335
+ }
336
+
337
+ const outMsg: EmailReceiverMessage = {
338
+ topic: parsed.subject || '',
339
+ payload: parsed.text || '',
340
+ html: parsed.html || '',
341
+ from: parsed.replyTo?.text || parsed.from?.text || '',
342
+ date: parsed.date,
343
+ folder,
344
+ header: parsed.headers,
345
+ attachments: (parsed.attachments || []).map((att: Attachment) => ({
346
+ contentType: att.contentType,
347
+ fileName: att.filename,
348
+ contentDisposition: att.contentDisposition as string,
349
+ generatedFileName: att.cid || att.checksum,
350
+ contentId: att.cid,
351
+ checksum: att.checksum as string,
352
+ length: att.size as number,
353
+ content: att.content as Buffer,
354
+ })),
355
+ };
356
+ state.folderCount[folder] = (state.folderCount[folder] || 0) + 1;
357
+ onMail(outMsg);
358
+ },
359
+ );
360
+ });
361
+
362
+ if (fetchConfig.markSeen) {
363
+ msg.once('attributes', (attrs: ImapMessageAttributes) => {
364
+ imap.addFlags(attrs.uid, 'SEEN', (err: Error | null) => {
365
+ if (err) {
366
+ node.error(
367
+ `Failed to mark message UID ${attrs.uid} as seen: ${err.message}`,
368
+ );
369
+ }
370
+ });
371
+ });
372
+ }
373
+ });
374
+
375
+ fetch.once('error', (err: Error) => {
376
+ node.error(`Fetch error in folder "${folder}": ${err.message}`);
377
+ });
378
+
379
+ fetch.once('end', () => {
380
+ state.successes++;
381
+ state.processedFolders++;
382
+ updateStatus('green', `Fetched ${results.length} from "${folder}".`);
383
+ startNextFolder();
384
+ });
385
+ });
386
+ });
387
+ };
388
+
389
+ const startNextFolder = () => {
390
+ if (state.processedFolders >= state.totalFolders) {
391
+ finalizeSession();
392
+ } else {
393
+ fetchFromFolder(fetchConfig.folders[state.processedFolders]);
394
+ }
395
+ };
396
+
397
+ imap.once('ready', () => {
398
+ node.status({ fill: 'green', shape: 'dot', text: 'connected' });
399
+ startNextFolder();
400
+ });
401
+
402
+ imap.once('error', (err: Error) => {
403
+ finalizeSession(err);
404
+ });
405
+
406
+ imap.once('end', () => {
407
+ updateStatus('green', 'IMAP connection ended.');
408
+ });
409
+
410
+ try {
411
+ updateStatus('yellow', 'Connecting to IMAP...');
412
+ imap.connect();
413
+ } catch (err: any) {
414
+ const error = err instanceof Error ? err : new Error(String(err));
415
+ updateStatus('red', 'Connection error: ' + error.message);
416
+ done(error);
417
+ return;
418
+ }
419
+ };
420
+
421
+ fetchEmails(finalConfig, (mail) => {
422
+ send(mail as any);
423
+ });
424
+ done();
425
+ } catch (error) {
426
+ node.status({ fill: 'red', shape: 'ring', text: 'config error' });
427
+ done(error instanceof Error ? error : new Error(String(error)));
428
+ }
429
+ });
430
+ node.on('close', () => {});
431
+ }
432
+ RED.nodes.registerType('email-receiver', EmailReceiverNode, {
433
+ credentials: {
434
+ password: { type: 'password' },
435
+ },
436
+ });
437
+ };
438
+
439
+ export = nodeInit;