@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
@@ -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
+ }