@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
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { ImapConfig } from '../interfaces/imap-config';
|
|
3
|
+
import { ImapMailbox } from '../interfaces/imap-mailbox';
|
|
4
|
+
|
|
5
|
+
export class MockImap extends EventEmitter {
|
|
6
|
+
private config: ImapConfig;
|
|
7
|
+
private isConnected = false;
|
|
8
|
+
private currentBox: string | null = null;
|
|
9
|
+
public state: string = 'disconnected';
|
|
10
|
+
|
|
11
|
+
constructor(config: ImapConfig) {
|
|
12
|
+
super();
|
|
13
|
+
this.config = config;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
connect(): void {
|
|
17
|
+
setTimeout(() => {
|
|
18
|
+
if (this.isConnectionInvalid()) {
|
|
19
|
+
const error = new Error('Connection failed') as Error & { code: string };
|
|
20
|
+
error.code = 'ENOTFOUND';
|
|
21
|
+
this.emit('error', error);
|
|
22
|
+
} else {
|
|
23
|
+
this.isConnected = true;
|
|
24
|
+
this.state = 'authenticated';
|
|
25
|
+
this.emit('ready');
|
|
26
|
+
}
|
|
27
|
+
}, 10);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
openBox(folder: string, readOnly: boolean, callback: (err: Error | null, box?: ImapMailbox) => void): void {
|
|
31
|
+
if (!this.isConnected) {
|
|
32
|
+
callback(new Error('Not connected'));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
setTimeout(() => {
|
|
37
|
+
this.currentBox = folder;
|
|
38
|
+
callback(null, {
|
|
39
|
+
messages: { total: this.getMessageCount(folder) },
|
|
40
|
+
name: folder,
|
|
41
|
+
readOnly,
|
|
42
|
+
});
|
|
43
|
+
}, 5);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
search(criteria: any[], callback: (err: Error | null, results?: number[]) => void): void {
|
|
47
|
+
setTimeout(() => {
|
|
48
|
+
const messageIds = this.getFixedMessageIds();
|
|
49
|
+
callback(null, messageIds);
|
|
50
|
+
}, 10);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fetch(results: number[], options?: any) {
|
|
54
|
+
const fetchEmitter = new EventEmitter();
|
|
55
|
+
|
|
56
|
+
setTimeout(() => {
|
|
57
|
+
results.forEach((id, index) => {
|
|
58
|
+
setTimeout(() => {
|
|
59
|
+
const mockMessage = this.createMockMessage(id);
|
|
60
|
+
fetchEmitter.emit('message', mockMessage, id);
|
|
61
|
+
}, index * 5);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Emit end after all messages are processed
|
|
65
|
+
setTimeout(() => fetchEmitter.emit('end'), results.length * 10 + 50);
|
|
66
|
+
}, 10);
|
|
67
|
+
|
|
68
|
+
return fetchEmitter;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
end(): void {
|
|
72
|
+
this.isConnected = false;
|
|
73
|
+
this.state = 'disconnected';
|
|
74
|
+
setTimeout(() => this.emit('end'), 5);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
addFlags(source: number | number[], flags: string[], callback: (err: Error | null) => void): void {
|
|
78
|
+
setTimeout(() => callback(null), 5);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Private helper methods
|
|
82
|
+
private isConnectionInvalid(): boolean {
|
|
83
|
+
return (
|
|
84
|
+
!this.config.host ||
|
|
85
|
+
this.config.host.includes('invalid') ||
|
|
86
|
+
this.config.host.includes('nonexistent') ||
|
|
87
|
+
this.config.host.includes('unreachable') ||
|
|
88
|
+
!this.config.user ||
|
|
89
|
+
!this.config.password
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private getMessageCount(folder: string): number {
|
|
94
|
+
const counts: Record<string, number> = {
|
|
95
|
+
INBOX: 5,
|
|
96
|
+
SENT: 2,
|
|
97
|
+
DRAFTS: 1,
|
|
98
|
+
JUNK: 0,
|
|
99
|
+
};
|
|
100
|
+
return counts[folder.toUpperCase()] || 3;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private getFixedMessageIds(): number[] {
|
|
104
|
+
return this.currentBox === 'INBOX' ? [123, 456, 789, 1011, 1213] : [123, 456, 789];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private createMockMessage(id: number) {
|
|
108
|
+
const message = new EventEmitter();
|
|
109
|
+
|
|
110
|
+
setTimeout(() => {
|
|
111
|
+
const emailContent = this.generateEmailContent(id);
|
|
112
|
+
// Create a proper readable stream
|
|
113
|
+
const { Readable } = require('stream');
|
|
114
|
+
const mockStream = new Readable({
|
|
115
|
+
read() {
|
|
116
|
+
this.push(Buffer.from(emailContent));
|
|
117
|
+
this.push(null); // End the stream
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
message.emit('body', mockStream);
|
|
121
|
+
}, 5);
|
|
122
|
+
|
|
123
|
+
setTimeout(() => {
|
|
124
|
+
message.emit('attributes', {
|
|
125
|
+
uid: id,
|
|
126
|
+
flags: Math.random() > 0.5 ? ['\\Seen'] : [],
|
|
127
|
+
date: new Date(),
|
|
128
|
+
size: Math.floor(Math.random() * 10000) + 500,
|
|
129
|
+
});
|
|
130
|
+
}, 10);
|
|
131
|
+
|
|
132
|
+
return message;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private generateEmailContent(id: number): string {
|
|
136
|
+
return [
|
|
137
|
+
`Message-ID: <${id}@test.com>`,
|
|
138
|
+
`From: sender${id}@test.com`,
|
|
139
|
+
`To: recipient@test.com`,
|
|
140
|
+
`Subject: Test Email ${id}`,
|
|
141
|
+
`Date: ${new Date().toUTCString()}`,
|
|
142
|
+
``,
|
|
143
|
+
`This is test email content for message ${id}.`,
|
|
144
|
+
`Generated for testing purposes.`,
|
|
145
|
+
].join('\r\n');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { EmailData } from '../interfaces/email-data';
|
|
2
|
+
|
|
3
|
+
export function createMockMailparser() {
|
|
4
|
+
return function mockMailParser(stream: NodeJS.ReadableStream, callback: (err: Error | null, parsed?: any) => void) {
|
|
5
|
+
// Read the stream data
|
|
6
|
+
let emailData = '';
|
|
7
|
+
|
|
8
|
+
stream.on('data', (chunk) => {
|
|
9
|
+
emailData += chunk.toString();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
stream.on('end', () => {
|
|
13
|
+
// Parse the email content using your existing helper
|
|
14
|
+
const parsedData = parseEmailContent(emailData);
|
|
15
|
+
const parsedMail = {
|
|
16
|
+
subject: parsedData.subject || 'Mock Email Subject',
|
|
17
|
+
text: parsedData.text || 'Mock email content',
|
|
18
|
+
html: parsedData.html || '<p>Mock email content</p>',
|
|
19
|
+
from: {
|
|
20
|
+
text: parsedData.from || 'sender@test.com',
|
|
21
|
+
value: [{ address: parsedData.from || parsedData.from || 'sender@test.com' }],
|
|
22
|
+
},
|
|
23
|
+
replyTo: {
|
|
24
|
+
text: parsedData.from || 'sender@test.com',
|
|
25
|
+
value: [{ address: parsedData.to || parsedData.to || 'recipient@test.com' }],
|
|
26
|
+
},
|
|
27
|
+
date: parsedData.date || new Date(),
|
|
28
|
+
messageId: parsedData.messageId || '<mock@test.com>',
|
|
29
|
+
headers: new Map([
|
|
30
|
+
['message-id', parsedData.messageId || '<mock@test.com>'],
|
|
31
|
+
['subject', parsedData.subject || 'Mock Email Subject'],
|
|
32
|
+
['from', parsedData.from || 'sender@test.com'],
|
|
33
|
+
]),
|
|
34
|
+
attachments: parsedData.attachments || [],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Call the callback asynchronously to simulate real parsing
|
|
38
|
+
setTimeout(() => {
|
|
39
|
+
callback(null, parsedMail);
|
|
40
|
+
}, 5);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
stream.on('error', (err) => {
|
|
44
|
+
callback(err);
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Helper to parse basic email content
|
|
50
|
+
function parseEmailContent(content: string): Partial<EmailData> {
|
|
51
|
+
const lines = content.split('\r\n');
|
|
52
|
+
const result: Partial<EmailData> = {};
|
|
53
|
+
let bodyStart = false;
|
|
54
|
+
let bodyLines: string[] = [];
|
|
55
|
+
|
|
56
|
+
for (const line of lines) {
|
|
57
|
+
if (!bodyStart) {
|
|
58
|
+
if (line === '') {
|
|
59
|
+
bodyStart = true;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (line.startsWith('Subject: ')) {
|
|
64
|
+
result.subject = line.substring(9);
|
|
65
|
+
} else if (line.startsWith('From: ')) {
|
|
66
|
+
result.from = line.substring(6);
|
|
67
|
+
} else if (line.startsWith('To: ')) {
|
|
68
|
+
result.to = line.substring(4);
|
|
69
|
+
} else if (line.startsWith('Message-ID: ')) {
|
|
70
|
+
result.messageId = line.substring(12);
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
bodyLines.push(line);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (bodyLines.length > 0) {
|
|
78
|
+
result.text = bodyLines.join('\n');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { EnhancedMockNodeREDOptions } from '../framework/node-test-runner';
|
|
2
|
+
import { MailOptions, MockNodemailerOptions } from '../interfaces/mail-options';
|
|
3
|
+
import { SendMailResult } from '../interfaces/send-mail-result';
|
|
4
|
+
|
|
5
|
+
class MockNodemailer {
|
|
6
|
+
constructor(private options: MockNodemailerOptions = {}) {}
|
|
7
|
+
|
|
8
|
+
sendMail(mailOptions: MailOptions, callback: (err: Error | null, result?: SendMailResult) => void): void {
|
|
9
|
+
if (this.options.onSendMail) {
|
|
10
|
+
this.options.onSendMail(mailOptions);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (this.options.shouldFail) {
|
|
14
|
+
console.log('💥 Triggering failure');
|
|
15
|
+
const error = new Error(
|
|
16
|
+
this.options.failureMessage || 'Invalid login: 535 Authentication credentials invalid',
|
|
17
|
+
) as Error & { code: string };
|
|
18
|
+
error.code = this.options.failureCode || 'EAUTH';
|
|
19
|
+
return callback(error);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Process recipient status
|
|
23
|
+
const recipients = this.normalizeRecipients(mailOptions.to);
|
|
24
|
+
const result = this.categorizeRecipients(recipients);
|
|
25
|
+
|
|
26
|
+
// Simulate realistic delays
|
|
27
|
+
setTimeout(() => {
|
|
28
|
+
callback(null, {
|
|
29
|
+
messageId: this.generateMessageId(),
|
|
30
|
+
response: this.getResponseMessage(result),
|
|
31
|
+
accepted: result.accepted,
|
|
32
|
+
rejected: result.rejected,
|
|
33
|
+
pending: result.pending,
|
|
34
|
+
});
|
|
35
|
+
}, 10);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
verify(callback?: (err: Error | null, success?: boolean) => void): Promise<boolean> | void {
|
|
39
|
+
if (callback) {
|
|
40
|
+
setTimeout(() => {
|
|
41
|
+
if (this.options.shouldFailVerify) {
|
|
42
|
+
const error = new Error('Mock verify error') as Error & { code: string };
|
|
43
|
+
error.code = 'ECONNREFUSED';
|
|
44
|
+
callback(error);
|
|
45
|
+
} else {
|
|
46
|
+
callback(null, true);
|
|
47
|
+
}
|
|
48
|
+
}, 10);
|
|
49
|
+
} else {
|
|
50
|
+
// Return a promise if no callback provided
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
if (this.options.shouldFailVerify) {
|
|
53
|
+
const error = new Error('Mock verify error') as Error & { code: string };
|
|
54
|
+
error.code = 'ECONNREFUSED';
|
|
55
|
+
reject(error);
|
|
56
|
+
} else {
|
|
57
|
+
resolve(true);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private normalizeRecipients(to: string | string[]): string[] {
|
|
64
|
+
if (Array.isArray(to)) return to;
|
|
65
|
+
if (typeof to === 'string') return to.split(',').map((email) => email.trim());
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private categorizeRecipients(recipients: string[]): { accepted: string[]; rejected: string[]; pending: string[] } {
|
|
70
|
+
const result = { accepted: [] as string[], rejected: [] as string[], pending: [] as string[] };
|
|
71
|
+
|
|
72
|
+
recipients.forEach((email) => {
|
|
73
|
+
if (this.options.rejectedEmails?.includes(email)) {
|
|
74
|
+
result.rejected.push(email);
|
|
75
|
+
} else if (this.options.pendingEmails?.includes(email)) {
|
|
76
|
+
result.pending.push(email);
|
|
77
|
+
} else if (this.options.acceptedEmails?.length) {
|
|
78
|
+
if (this.options.acceptedEmails.includes(email)) {
|
|
79
|
+
result.accepted.push(email);
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
// Default: accept all emails not explicitly rejected or pending
|
|
83
|
+
result.accepted.push(email);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private getResponseMessage(result: { accepted: string[]; rejected: string[]; pending: string[] }): string {
|
|
91
|
+
if (result.rejected.length > 0) return '550 Mailbox unavailable';
|
|
92
|
+
if (result.pending.length > 0) return '451 Requested action aborted: local error';
|
|
93
|
+
return '250 OK: Message accepted';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private generateMessageId(): string {
|
|
97
|
+
const timestamp = Date.now();
|
|
98
|
+
const random = Math.random().toString(36).substr(2, 9);
|
|
99
|
+
return `<${timestamp}.${random}@test.com>`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function createMockNodemailer(options: MockNodemailerOptions = {}) {
|
|
104
|
+
return {
|
|
105
|
+
createTransport: (config?: any) => new MockNodemailer(options),
|
|
106
|
+
restore: () => {
|
|
107
|
+
// Cleanup method for compatibility
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function withNodemailerMock(options: MockNodemailerOptions): Partial<EnhancedMockNodeREDOptions> {
|
|
113
|
+
return {
|
|
114
|
+
dependencies: {
|
|
115
|
+
nodemailer: createMockNodemailer(options),
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|