@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.
- package/.mocharc.json +5 -0
- package/package.json +26 -10
- package/src/custom-node-template/custom-node-template.html.template +45 -0
- package/src/custom-node-template/custom-node-template.ts.template +69 -0
- package/src/email-receiver/email-receiver.ts +439 -0
- package/src/email-sender/email-sender.ts +210 -0
- package/{processcube-html-to-text/processcube-html-to-text.html → src/html-to-text/html-to-text.html} +3 -3
- package/src/html-to-text/html-to-text.ts +53 -0
- package/src/index.ts +12 -0
- package/src/interfaces/EmailReceiverMessage.ts +22 -0
- package/src/interfaces/EmailSenderNodeProperties.ts +37 -0
- package/src/interfaces/FetchState.ts +9 -0
- package/src/interfaces/ImapConnectionConfig.ts +14 -0
- package/src/test/framework/advanced-test-patterns.ts +224 -0
- package/src/test/framework/generic-node-test-suite.ts +58 -0
- package/src/test/framework/index.ts +17 -0
- package/src/test/framework/integration-assertions.ts +67 -0
- package/src/test/framework/integration-scenario-builder.ts +77 -0
- package/src/test/framework/integration-test-runner.ts +101 -0
- package/src/test/framework/node-assertions.ts +63 -0
- package/src/test/framework/node-test-runner.ts +260 -0
- package/src/test/framework/test-scenario-builder.ts +74 -0
- package/src/test/framework/types.ts +61 -0
- package/src/test/helpers/email-receiver-test-configs.ts +67 -0
- package/src/test/helpers/email-receiver-test-flows.ts +16 -0
- package/src/test/helpers/email-sender-test-configs.ts +123 -0
- package/src/test/helpers/email-sender-test-flows.ts +16 -0
- package/src/test/integration/email-receiver.integration.test.ts +41 -0
- package/src/test/integration/email-sender.integration.test.ts +129 -0
- package/src/test/interfaces/email-data.ts +10 -0
- package/src/test/interfaces/email-receiver-config.ts +12 -0
- package/src/test/interfaces/email-sender-config.ts +26 -0
- package/src/test/interfaces/imap-config.ts +9 -0
- package/src/test/interfaces/imap-mailbox.ts +5 -0
- package/src/test/interfaces/mail-options.ts +20 -0
- package/src/test/interfaces/parsed-email.ts +11 -0
- package/src/test/interfaces/send-mail-result.ts +7 -0
- package/src/test/mocks/imap-mock.ts +147 -0
- package/src/test/mocks/mailparser-mock.ts +82 -0
- package/src/test/mocks/nodemailer-mock.ts +118 -0
- package/src/test/unit/email-receiver.unit.test.ts +471 -0
- package/src/test/unit/email-sender.unit.test.ts +550 -0
- package/tsconfig.json +23 -0
- package/email-receiver/email-receiver.js +0 -304
- package/email-sender/email-sender.js +0 -178
- package/examples/.gitkeep +0 -0
- package/processcube-html-to-text/processcube-html-to-text.js +0 -22
- package/test/helpers/email-receiver.mocks.js +0 -447
- package/test/helpers/email-sender.mocks.js +0 -368
- package/test/integration/email-receiver.integration.test.js +0 -515
- package/test/integration/email-sender.integration.test.js +0 -239
- package/test/unit/email-receiver.unit.test.js +0 -304
- package/test/unit/email-sender.unit.test.js +0 -570
- /package/{email-receiver → src/email-receiver}/email-receiver.html +0 -0
- /package/{email-sender → src/email-sender}/email-sender.html +0 -0
package/.mocharc.json
ADDED
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-
|
|
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
|
-
"
|
|
8
|
-
"
|
|
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": "
|
|
42
|
+
"version": "^4.1.0",
|
|
38
43
|
"nodes": {
|
|
39
|
-
"EmailReceiver": "email-receiver/email-receiver.js",
|
|
40
|
-
"EmailSender": "email-sender/email-sender.js",
|
|
41
|
-
"HtmlToText": "
|
|
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.
|
|
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;
|