@5minds/node-red-contrib-processcube-tools 1.2.0-feature-7ac247-mg92hchl → 1.2.0-feature-608421-mg9cjskq
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/.env.template +7 -1
- package/email-receiver/email-receiver.js +304 -0
- package/email-sender/email-sender.js +178 -0
- package/examples/.gitkeep +0 -0
- package/file-storage/file-storage.html +203 -0
- package/file-storage/file-storage.js +148 -0
- package/package.json +17 -26
- package/{src/html-to-text/html-to-text.html → processcube-html-to-text/processcube-html-to-text.html} +3 -3
- package/processcube-html-to-text/processcube-html-to-text.js +22 -0
- package/storage/providers/fs.js +117 -0
- package/storage/providers/postgres.js +160 -0
- package/storage/storage-core.js +77 -0
- package/test/helpers/email-receiver.mocks.js +447 -0
- package/test/helpers/email-sender.mocks.js +368 -0
- package/test/integration/email-receiver.integration.test.js +515 -0
- package/test/integration/email-sender.integration.test.js +239 -0
- package/test/unit/email-receiver.unit.test.js +304 -0
- package/test/unit/email-sender.unit.test.js +570 -0
- package/.mocharc.json +0 -5
- package/src/custom-node-template/custom-node-template.html.template +0 -45
- package/src/custom-node-template/custom-node-template.ts.template +0 -69
- package/src/email-receiver/email-receiver.ts +0 -439
- package/src/email-sender/email-sender.ts +0 -210
- package/src/html-to-text/html-to-text.ts +0 -53
- package/src/index.ts +0 -12
- package/src/interfaces/EmailReceiverMessage.ts +0 -22
- package/src/interfaces/EmailSenderNodeProperties.ts +0 -37
- package/src/interfaces/FetchState.ts +0 -9
- package/src/interfaces/ImapConnectionConfig.ts +0 -14
- package/src/test/framework/advanced-test-patterns.ts +0 -224
- package/src/test/framework/generic-node-test-suite.ts +0 -58
- package/src/test/framework/index.ts +0 -17
- package/src/test/framework/integration-assertions.ts +0 -67
- package/src/test/framework/integration-scenario-builder.ts +0 -77
- package/src/test/framework/integration-test-runner.ts +0 -101
- package/src/test/framework/node-assertions.ts +0 -63
- package/src/test/framework/node-test-runner.ts +0 -260
- package/src/test/framework/test-scenario-builder.ts +0 -74
- package/src/test/framework/types.ts +0 -61
- package/src/test/helpers/email-receiver-test-configs.ts +0 -67
- package/src/test/helpers/email-receiver-test-flows.ts +0 -16
- package/src/test/helpers/email-sender-test-configs.ts +0 -123
- package/src/test/helpers/email-sender-test-flows.ts +0 -16
- package/src/test/integration/email-receiver.integration.test.ts +0 -41
- package/src/test/integration/email-sender.integration.test.ts +0 -129
- package/src/test/interfaces/email-data.ts +0 -10
- package/src/test/interfaces/email-receiver-config.ts +0 -12
- package/src/test/interfaces/email-sender-config.ts +0 -26
- package/src/test/interfaces/imap-config.ts +0 -9
- package/src/test/interfaces/imap-mailbox.ts +0 -5
- package/src/test/interfaces/mail-options.ts +0 -20
- package/src/test/interfaces/parsed-email.ts +0 -11
- package/src/test/interfaces/send-mail-result.ts +0 -7
- package/src/test/mocks/imap-mock.ts +0 -147
- package/src/test/mocks/mailparser-mock.ts +0 -82
- package/src/test/mocks/nodemailer-mock.ts +0 -118
- package/src/test/unit/email-receiver.unit.test.ts +0 -471
- package/src/test/unit/email-sender.unit.test.ts +0 -550
- package/tsconfig.json +0 -23
- /package/{src/email-receiver → email-receiver}/email-receiver.html +0 -0
- /package/{src/email-sender → email-sender}/email-sender.html +0 -0
|
@@ -1,439 +0,0 @@
|
|
|
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;
|
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
import { NodeInitializer, Node, NodeMessage } from 'node-red';
|
|
2
|
-
import nodemailer from 'nodemailer';
|
|
3
|
-
import { EmailSenderNodeProperties } from '../interfaces/EmailSenderNodeProperties';
|
|
4
|
-
|
|
5
|
-
interface EmailSenderNodeMessage extends NodeMessage {}
|
|
6
|
-
|
|
7
|
-
// Dependency injection interface
|
|
8
|
-
interface Dependencies {
|
|
9
|
-
nodemailer: typeof nodemailer;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// Default dependencies - production values
|
|
13
|
-
const defaultDependencies: Dependencies = {
|
|
14
|
-
nodemailer: nodemailer,
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const EmailSenderNode: NodeInitializer = (RED, dependencies: Dependencies = defaultDependencies) => {
|
|
18
|
-
function EmailSender(this: Node, config: EmailSenderNodeProperties) {
|
|
19
|
-
RED.nodes.createNode(this, config);
|
|
20
|
-
const node = this;
|
|
21
|
-
|
|
22
|
-
const validateRequiredProperties = (cfg: EmailSenderNodeProperties): string | null => {
|
|
23
|
-
const requiredFields = [
|
|
24
|
-
{ name: 'sender', value: cfg.sender },
|
|
25
|
-
{ name: 'address', value: cfg.address },
|
|
26
|
-
{ name: 'to', value: cfg.to },
|
|
27
|
-
{ name: 'subject', value: cfg.subject },
|
|
28
|
-
{ name: 'htmlContent', value: cfg.htmlContent },
|
|
29
|
-
{ name: 'host', value: cfg.host },
|
|
30
|
-
{ name: 'port', value: cfg.port },
|
|
31
|
-
{ name: 'user', value: cfg.user },
|
|
32
|
-
{ name: 'password', value: cfg.password },
|
|
33
|
-
{ name: 'secure', value: cfg.secure },
|
|
34
|
-
{ name: 'rejectUnauthorized', value: cfg.rejectUnauthorized },
|
|
35
|
-
];
|
|
36
|
-
|
|
37
|
-
for (const field of requiredFields) {
|
|
38
|
-
if (field.value === undefined || field.value === null || field.value === '') {
|
|
39
|
-
return `Required property '${field.name}' is missing`;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return null;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const validationError = validateRequiredProperties(config);
|
|
47
|
-
if (validationError) {
|
|
48
|
-
node.status({ fill: 'red', shape: 'dot', text: 'configuration error' });
|
|
49
|
-
setImmediate(() => {
|
|
50
|
-
node.error(validationError);
|
|
51
|
-
});
|
|
52
|
-
return; // Stop initialization if config is invalid
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const safeEvaluatePropertyAttachment = (cfg: EmailSenderNodeProperties, n: Node, m: NodeMessage) => {
|
|
56
|
-
if (cfg.attachments && cfg.attachments.trim() !== '') {
|
|
57
|
-
try {
|
|
58
|
-
return RED.util.evaluateNodeProperty(cfg.attachments, cfg.attachmentsType, n, m);
|
|
59
|
-
} catch (e: any) {
|
|
60
|
-
n.error('Failed to evaluate attachments property: ' + e.message, m);
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return null;
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
(node as any).on('input', async (msg: EmailSenderNodeMessage, send: Function, done: Function) => {
|
|
68
|
-
send =
|
|
69
|
-
send ||
|
|
70
|
-
function () {
|
|
71
|
-
node.send.apply(node, arguments as any);
|
|
72
|
-
};
|
|
73
|
-
done =
|
|
74
|
-
done ||
|
|
75
|
-
function (err?: Error) {
|
|
76
|
-
if (err) node.error(err, msg);
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
// Retrieve and evaluate all configuration values
|
|
81
|
-
const sender = String(RED.util.evaluateNodeProperty(config.sender, config.senderType, node, msg));
|
|
82
|
-
const address = String(RED.util.evaluateNodeProperty(config.address, config.addressType, node, msg));
|
|
83
|
-
const to = String(RED.util.evaluateNodeProperty(config.to, config.toType, node, msg) || '');
|
|
84
|
-
const cc = String(RED.util.evaluateNodeProperty(config.cc, config.ccType, node, msg) || '');
|
|
85
|
-
const bcc = String(RED.util.evaluateNodeProperty(config.bcc, config.bccType, node, msg) || '');
|
|
86
|
-
const replyTo = String(
|
|
87
|
-
RED.util.evaluateNodeProperty(config.replyTo, config.replyToType, node, msg) || '',
|
|
88
|
-
);
|
|
89
|
-
const subject = String(
|
|
90
|
-
RED.util.evaluateNodeProperty(config.subject, config.subjectType, node, msg) ||
|
|
91
|
-
msg.topic ||
|
|
92
|
-
'Message from Node-RED',
|
|
93
|
-
);
|
|
94
|
-
const htmlContent = String(
|
|
95
|
-
RED.util.evaluateNodeProperty(config.htmlContent, config.htmlContentType, node, msg),
|
|
96
|
-
);
|
|
97
|
-
const attachments = safeEvaluatePropertyAttachment(config, node, msg);
|
|
98
|
-
|
|
99
|
-
// SMTP Configuration
|
|
100
|
-
const host = String(RED.util.evaluateNodeProperty(config.host, config.hostType, node, msg));
|
|
101
|
-
const port = Number(RED.util.evaluateNodeProperty(config.port, config.portType, node, msg));
|
|
102
|
-
const user = String(RED.util.evaluateNodeProperty(config.user, config.userType, node, msg));
|
|
103
|
-
const password = String(RED.util.evaluateNodeProperty(config.password, config.passwordType, node, msg));
|
|
104
|
-
const secure = Boolean(RED.util.evaluateNodeProperty(config.secure, config.secureType, node, msg));
|
|
105
|
-
const rejectUnauthorized = Boolean(
|
|
106
|
-
RED.util.evaluateNodeProperty(config.rejectUnauthorized, config.rejectUnauthorizedType, node, msg),
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
// Process attachments
|
|
110
|
-
let processedAttachments: any[] = [];
|
|
111
|
-
let parsedAttachments = attachments;
|
|
112
|
-
if (typeof parsedAttachments === 'string' && parsedAttachments.trim().startsWith('[')) {
|
|
113
|
-
try {
|
|
114
|
-
parsedAttachments = JSON.parse(parsedAttachments);
|
|
115
|
-
} catch (e) {
|
|
116
|
-
throw new Error('Failed to parse attachments JSON: ' + (e as Error).message);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (parsedAttachments) {
|
|
121
|
-
const attachmentArray = Array.isArray(parsedAttachments) ? parsedAttachments : [parsedAttachments];
|
|
122
|
-
for (const attachment of attachmentArray) {
|
|
123
|
-
if (typeof attachment === 'object' && attachment !== null) {
|
|
124
|
-
if (attachment.filename && attachment.content !== undefined) {
|
|
125
|
-
processedAttachments.push({
|
|
126
|
-
filename: attachment.filename,
|
|
127
|
-
content: attachment.content,
|
|
128
|
-
});
|
|
129
|
-
} else {
|
|
130
|
-
throw new Error(
|
|
131
|
-
`Attachment object is missing 'filename' or 'content' property. Got: ${JSON.stringify(attachment)}`,
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
} else {
|
|
135
|
-
throw new Error(`Invalid attachment format. Expected object, got: ${typeof attachment}`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Create and send email
|
|
141
|
-
const transporter = dependencies.nodemailer.createTransport({
|
|
142
|
-
host,
|
|
143
|
-
port,
|
|
144
|
-
secure,
|
|
145
|
-
auth: { user, pass: password },
|
|
146
|
-
tls: { rejectUnauthorized },
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
const mailOptions = {
|
|
150
|
-
from: { name: sender, address: address },
|
|
151
|
-
to,
|
|
152
|
-
cc,
|
|
153
|
-
bcc,
|
|
154
|
-
replyTo,
|
|
155
|
-
subject,
|
|
156
|
-
html: Buffer.from(htmlContent, 'utf-8'),
|
|
157
|
-
attachments: processedAttachments,
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
transporter.sendMail(mailOptions, (error, info) => {
|
|
161
|
-
if (error) {
|
|
162
|
-
node.status({ fill: 'red', shape: 'dot', text: 'error sending' });
|
|
163
|
-
if (
|
|
164
|
-
error.message &&
|
|
165
|
-
error.message.includes('SSL routines') &&
|
|
166
|
-
error.message.includes('wrong version number')
|
|
167
|
-
) {
|
|
168
|
-
done(
|
|
169
|
-
new Error(
|
|
170
|
-
'SSL/TLS connection failed: Wrong version number. This usually means the wrong port or security settings are used. For SMTP: use port 587 with secure=false (STARTTLS) or port 465 with secure=true (SSL/TLS).',
|
|
171
|
-
),
|
|
172
|
-
);
|
|
173
|
-
} else {
|
|
174
|
-
done(error);
|
|
175
|
-
}
|
|
176
|
-
} else {
|
|
177
|
-
node.log('Email sent: ' + info.response);
|
|
178
|
-
(msg as any).payload = info;
|
|
179
|
-
|
|
180
|
-
if (info.accepted && info.accepted.length > 0) {
|
|
181
|
-
node.status({ fill: 'green', shape: 'dot', text: 'sent' });
|
|
182
|
-
send(msg);
|
|
183
|
-
done();
|
|
184
|
-
} else if (info.rejected && info.rejected.length > 0) {
|
|
185
|
-
done(new Error('Email rejected: ' + info.rejected.join(', ')));
|
|
186
|
-
node.status({ fill: 'red', shape: 'dot', text: 'rejected' });
|
|
187
|
-
} else if (info.pending && info.pending.length > 0) {
|
|
188
|
-
done(new Error('Email pending: ' + info.pending.join(', ')));
|
|
189
|
-
node.status({ fill: 'yellow', shape: 'dot', text: 'pending' });
|
|
190
|
-
} else {
|
|
191
|
-
done(new Error('Unknown error while sending email.'));
|
|
192
|
-
node.status({ fill: 'red', shape: 'dot', text: 'unknown error' });
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
} catch (error) {
|
|
197
|
-
done(error instanceof Error ? error : new Error(String(error)));
|
|
198
|
-
node.status({ fill: 'red', shape: 'dot', text: 'error' });
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
RED.nodes.registerType('email-sender', EmailSender, {
|
|
204
|
-
credentials: {
|
|
205
|
-
password: { type: 'password' },
|
|
206
|
-
},
|
|
207
|
-
});
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
export = EmailSenderNode;
|