@5minds/node-red-contrib-processcube-tools 1.2.0-feature-37541f-mg92jkdw → 1.2.0-feature-8f3d72-mg9cplxi

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 (61) hide show
  1. package/.env.template +7 -1
  2. package/email-receiver/email-receiver.js +304 -0
  3. package/email-sender/email-sender.js +178 -0
  4. package/examples/.gitkeep +0 -0
  5. package/file-storage/file-storage.html +203 -0
  6. package/file-storage/file-storage.js +148 -0
  7. package/package.json +17 -26
  8. package/{src/html-to-text/html-to-text.html → processcube-html-to-text/processcube-html-to-text.html} +3 -3
  9. package/processcube-html-to-text/processcube-html-to-text.js +22 -0
  10. package/storage/providers/fs.js +117 -0
  11. package/storage/providers/postgres.js +160 -0
  12. package/storage/storage-core.js +77 -0
  13. package/test/helpers/email-receiver.mocks.js +447 -0
  14. package/test/helpers/email-sender.mocks.js +368 -0
  15. package/test/integration/email-receiver.integration.test.js +515 -0
  16. package/test/integration/email-sender.integration.test.js +239 -0
  17. package/test/unit/email-receiver.unit.test.js +304 -0
  18. package/test/unit/email-sender.unit.test.js +570 -0
  19. package/.mocharc.json +0 -5
  20. package/src/custom-node-template/custom-node-template.html.template +0 -45
  21. package/src/custom-node-template/custom-node-template.ts.template +0 -69
  22. package/src/email-receiver/email-receiver.ts +0 -439
  23. package/src/email-sender/email-sender.ts +0 -210
  24. package/src/html-to-text/html-to-text.ts +0 -53
  25. package/src/index.ts +0 -12
  26. package/src/interfaces/EmailReceiverMessage.ts +0 -22
  27. package/src/interfaces/EmailSenderNodeProperties.ts +0 -37
  28. package/src/interfaces/FetchState.ts +0 -9
  29. package/src/interfaces/ImapConnectionConfig.ts +0 -14
  30. package/src/test/framework/advanced-test-patterns.ts +0 -224
  31. package/src/test/framework/generic-node-test-suite.ts +0 -58
  32. package/src/test/framework/index.ts +0 -17
  33. package/src/test/framework/integration-assertions.ts +0 -67
  34. package/src/test/framework/integration-scenario-builder.ts +0 -77
  35. package/src/test/framework/integration-test-runner.ts +0 -101
  36. package/src/test/framework/node-assertions.ts +0 -63
  37. package/src/test/framework/node-test-runner.ts +0 -260
  38. package/src/test/framework/test-scenario-builder.ts +0 -74
  39. package/src/test/framework/types.ts +0 -61
  40. package/src/test/helpers/email-receiver-test-configs.ts +0 -67
  41. package/src/test/helpers/email-receiver-test-flows.ts +0 -16
  42. package/src/test/helpers/email-sender-test-configs.ts +0 -123
  43. package/src/test/helpers/email-sender-test-flows.ts +0 -16
  44. package/src/test/integration/email-receiver.integration.test.ts +0 -41
  45. package/src/test/integration/email-sender.integration.test.ts +0 -129
  46. package/src/test/interfaces/email-data.ts +0 -10
  47. package/src/test/interfaces/email-receiver-config.ts +0 -12
  48. package/src/test/interfaces/email-sender-config.ts +0 -26
  49. package/src/test/interfaces/imap-config.ts +0 -9
  50. package/src/test/interfaces/imap-mailbox.ts +0 -5
  51. package/src/test/interfaces/mail-options.ts +0 -20
  52. package/src/test/interfaces/parsed-email.ts +0 -11
  53. package/src/test/interfaces/send-mail-result.ts +0 -7
  54. package/src/test/mocks/imap-mock.ts +0 -147
  55. package/src/test/mocks/mailparser-mock.ts +0 -82
  56. package/src/test/mocks/nodemailer-mock.ts +0 -118
  57. package/src/test/unit/email-receiver.unit.test.ts +0 -471
  58. package/src/test/unit/email-sender.unit.test.ts +0 -550
  59. package/tsconfig.json +0 -23
  60. /package/{src/email-receiver → email-receiver}/email-receiver.html +0 -0
  61. /package/{src/email-sender → email-sender}/email-sender.html +0 -0
@@ -1,53 +0,0 @@
1
- import { NodeInitializer, Node, NodeDef, NodeMessage } from 'node-red';
2
- const { compile } = require('html-to-text');
3
-
4
- interface HtmlToTextNodeProperties extends NodeDef {
5
- // Add your custom properties here if needed
6
- }
7
-
8
- interface HtmlToTextNodeMessage extends NodeMessage {
9
- payload: string;
10
- }
11
-
12
- const HtmlToTextNode: NodeInitializer = function (RED) {
13
- function HtmlToText(this: Node, config: HtmlToTextNodeProperties) {
14
- RED.nodes.createNode(this, config);
15
- const node = this;
16
-
17
- const options = {
18
- wordwrap: 130,
19
- };
20
-
21
- const compiledConvert = compile(options);
22
-
23
- (node as any).on('input', (msg: HtmlToTextNodeMessage, send?: Function, done?: Function) => {
24
- // Provide default functions if not available (for older Node-RED versions)
25
- send =
26
- send ||
27
- function (m: NodeMessage | NodeMessage[]) {
28
- node.send(m);
29
- };
30
- done =
31
- done ||
32
- function (err?: Error) {
33
- if (err) node.error(err, msg);
34
- };
35
-
36
- try {
37
- if (typeof msg.payload !== 'string') {
38
- throw new Error('Payload is not a string!');
39
- }
40
-
41
- msg.payload = compiledConvert(msg.payload);
42
- send(msg);
43
- done();
44
- } catch (error) {
45
- done(error instanceof Error ? error : new Error(String(error)));
46
- }
47
- });
48
- }
49
-
50
- RED.nodes.registerType('html-to-text', HtmlToText);
51
- };
52
-
53
- export = HtmlToTextNode;
package/src/index.ts DELETED
@@ -1,12 +0,0 @@
1
- // Importiere die Registrierungsfunktion für deine Nodes.
2
- import registerEmailReceiverNode from './email-receiver/email-receiver';
3
- import registerEmailSenderNode from './email-sender/email-sender';
4
- import registerHtmlToTextNode from './html-to-text/html-to-text';
5
-
6
- // Exportiere eine Funktion, die alle Nodes registriert.
7
- export = function (RED: any) {
8
- // Rufe die Registrierungsfunktionen für jede Node auf.
9
- registerEmailReceiverNode(RED);
10
- registerEmailSenderNode(RED);
11
- registerHtmlToTextNode(RED);
12
- };
@@ -1,22 +0,0 @@
1
- import type { ParsedMail } from 'mailparser';
2
-
3
- // Custom type for the output message
4
- export interface EmailReceiverMessage {
5
- topic: string | undefined;
6
- payload: string | undefined;
7
- html: string | boolean | undefined;
8
- from: string | undefined;
9
- date: Date | undefined;
10
- folder: string;
11
- header: ParsedMail['headers'];
12
- attachments: Array<{
13
- contentType: string;
14
- fileName: string | undefined;
15
- contentDisposition: string;
16
- generatedFileName: string | undefined;
17
- contentId: string | undefined;
18
- checksum: string;
19
- length: number;
20
- content: Buffer;
21
- }>;
22
- }
@@ -1,37 +0,0 @@
1
- import { NodeDef } from 'node-red';
2
-
3
- export interface EmailSenderNodeProperties extends NodeDef {
4
- // Mail configuration properties
5
- sender: string;
6
- senderType: string;
7
- address: string;
8
- addressType: string;
9
- to: string;
10
- toType: string;
11
- cc: string;
12
- ccType: string;
13
- bcc: string;
14
- bccType: string;
15
- replyTo: string;
16
- replyToType: string;
17
- subject: string;
18
- subjectType: string;
19
- htmlContent: string;
20
- htmlContentType: string;
21
- attachments: string;
22
- attachmentsType: string;
23
-
24
- // SMTP configuration properties
25
- host: string;
26
- hostType: string;
27
- port: string;
28
- portType: string;
29
- user: string;
30
- userType: string;
31
- password: string;
32
- passwordType: string;
33
- secure: string;
34
- secureType: string;
35
- rejectUnauthorized: string;
36
- rejectUnauthorizedType: string;
37
- }
@@ -1,9 +0,0 @@
1
- export interface FetchState {
2
- totalFolders: number;
3
- processedFolders: number;
4
- successes: number;
5
- failures: number;
6
- totalMails: number;
7
- errors: Error[];
8
- folderCount: { [folder: string]: number };
9
- }
@@ -1,14 +0,0 @@
1
- export interface ImapConnectionConfig {
2
- host: string;
3
- port: number;
4
- tls: boolean;
5
- user: string;
6
- password: string;
7
- folders: string[];
8
- markSeen: boolean;
9
- connTimeout: number;
10
- authTimeout: number;
11
- keepalive: boolean;
12
- autotls: string;
13
- tlsOptions: { rejectUnauthorized: boolean };
14
- }
@@ -1,224 +0,0 @@
1
- // ============================================================================
2
- // ADVANCED TEST PATTERNS FOR NODE-RED CUSTOM NODES
3
- // ============================================================================
4
-
5
- import type { TestScenario } from './types';
6
-
7
- // ============================================================================
8
- // ERROR RESILIENCE PATTERNS
9
- // ============================================================================
10
-
11
- export class ErrorResilienceTestBuilder {
12
- private scenarios: TestScenario[] = [];
13
-
14
- addNetworkErrorScenario(name: string, config: any): this {
15
- return this.addScenario({
16
- name: `${name} - network error`,
17
- config: { ...config, host: 'unreachable.invalid.test' },
18
- input: { payload: 'test' },
19
- expectedError: /network|connection|timeout/i,
20
- timeout: 3000,
21
- });
22
- }
23
-
24
- addMalformedInputScenario(name: string, config: any): this {
25
- const malformedInputs = [
26
- null,
27
- undefined,
28
- { payload: null },
29
- { payload: '' },
30
- { payload: { malformed: true, circular: null } },
31
- 'not-an-object',
32
- ];
33
-
34
- malformedInputs.forEach((input, index) => {
35
- this.addScenario({
36
- name: `${name} - malformed input ${index + 1}`,
37
- config,
38
- input,
39
- timeout: 2000,
40
- });
41
- });
42
-
43
- return this;
44
- }
45
-
46
- addResourceExhaustionScenario(name: string, config: any): this {
47
- return this.addScenario({
48
- name: `${name} - resource exhaustion`,
49
- config,
50
- input: {
51
- payload: 'x'.repeat(10 * 1024 * 1024), // 10MB payload
52
- largeArray: Array.from({ length: 100000 }, (_, i) => ({ id: i, data: 'test' })),
53
- },
54
- timeout: 5000,
55
- });
56
- }
57
-
58
- addRapidFireScenario(name: string, config: any, messageCount: number = 1000): this {
59
- return this.addScenario({
60
- name: `${name} - rapid fire messages`,
61
- config,
62
- input: Array.from({ length: messageCount }, (_, i) => ({
63
- payload: `rapid-message-${i}`,
64
- sequence: i,
65
- })),
66
- timeout: Math.max(5000, messageCount * 5),
67
- });
68
- }
69
-
70
- private addScenario(scenario: TestScenario): this {
71
- this.scenarios.push(scenario);
72
- return this;
73
- }
74
-
75
- getScenarios(): TestScenario[] {
76
- return [...this.scenarios];
77
- }
78
- }
79
- // ============================================================================
80
- // EDGE CASE PATTERNS
81
- // ============================================================================
82
-
83
- export class EdgeCaseTestBuilder {
84
- private scenarios: TestScenario[] = [];
85
-
86
- addEmptyDataScenarios(name: string, config: any): this {
87
- const emptyDataCases = [
88
- { name: 'empty object', data: {} },
89
- { name: 'empty array', data: [] },
90
- { name: 'empty string', data: '' },
91
- { name: 'null payload', data: null },
92
- { name: 'undefined payload', data: undefined },
93
- { name: 'zero value', data: 0 },
94
- { name: 'false value', data: false },
95
- ];
96
-
97
- emptyDataCases.forEach((testCase) => {
98
- this.scenarios.push({
99
- name: `${name} - ${testCase.name}`,
100
- config,
101
- input: { payload: testCase.data },
102
- timeout: 2000,
103
- });
104
- });
105
-
106
- return this;
107
- }
108
-
109
- addSpecialCharacterScenarios(name: string, config: any): this {
110
- const specialCases = [
111
- { name: 'unicode characters', data: '🚀💡🌟' },
112
- { name: 'newlines and tabs', data: 'line1\nline2\tindented' },
113
- { name: 'special symbols', data: '!@#$%^&*()_+-=[]{}|;:,.<>?' },
114
- { name: 'very long string', data: 'a'.repeat(10000) },
115
- { name: 'mixed encoding', data: 'Ñiño café résumé 北京' },
116
- ];
117
-
118
- specialCases.forEach((testCase) => {
119
- this.scenarios.push({
120
- name: `${name} - ${testCase.name}`,
121
- config,
122
- input: { payload: testCase.data },
123
- timeout: 3000,
124
- });
125
- });
126
-
127
- return this;
128
- }
129
-
130
- addLargeDataScenarios(name: string, config: any): this {
131
- const largeCases = [
132
- {
133
- name: 'large object',
134
- data: Object.fromEntries(Array.from({ length: 1000 }, (_, i) => [`key${i}`, `value${i}`])),
135
- },
136
- {
137
- name: 'deeply nested object',
138
- data: Array.from({ length: 100 }, () => ({})).reduce((acc, _, i) => ({ [`level${i}`]: acc }), {
139
- deepest: true,
140
- }),
141
- },
142
- {
143
- name: 'large array',
144
- data: Array.from({ length: 10000 }, (_, i) => ({ id: i, data: `item${i}` })),
145
- },
146
- ];
147
-
148
- largeCases.forEach((testCase) => {
149
- this.scenarios.push({
150
- name: `${name} - ${testCase.name}`,
151
- config,
152
- input: { payload: testCase.data },
153
- timeout: 5000,
154
- });
155
- });
156
-
157
- return this;
158
- }
159
-
160
- getScenarios(): TestScenario[] {
161
- return [...this.scenarios];
162
- }
163
- }
164
-
165
- // ============================================================================
166
- // SECURITY TESTING PATTERNS
167
- // ============================================================================
168
-
169
- export class SecurityTestBuilder {
170
- private scenarios: TestScenario[] = [];
171
-
172
- addInjectionTestScenarios(name: string, config: any): this {
173
- const injectionPayloads = [
174
- { name: 'SQL injection', payload: "'; DROP TABLE users; --" },
175
- { name: 'XSS attempt', payload: '<script>alert("xss")</script>' },
176
- { name: 'Command injection', payload: '; rm -rf / ;' },
177
- { name: 'Path traversal', payload: '../../../etc/passwd' },
178
- { name: 'JSON injection', payload: '{"__proto__":{"isAdmin":true}}' },
179
- ];
180
-
181
- injectionPayloads.forEach((attack) => {
182
- this.scenarios.push({
183
- name: `${name} - ${attack.name}`,
184
- config,
185
- input: { payload: attack.payload },
186
- timeout: 2000,
187
- });
188
- });
189
-
190
- return this;
191
- }
192
-
193
- addOversizedPayloadScenarios(name: string, config: any): this {
194
- const oversizedCases = [
195
- { name: '1MB payload', size: 1024 * 1024 },
196
- { name: '10MB payload', size: 10 * 1024 * 1024 },
197
- { name: 'deeply nested payload', depth: 1000 },
198
- ];
199
-
200
- oversizedCases.forEach((testCase) => {
201
- let payload;
202
- if (testCase.size) {
203
- payload = 'x'.repeat(testCase.size);
204
- } else if (testCase.depth) {
205
- payload = Array.from({ length: testCase.depth }, () => ({})).reduce((acc) => ({ nested: acc }), {
206
- bottom: true,
207
- });
208
- }
209
-
210
- this.scenarios.push({
211
- name: `${name} - ${testCase.name}`,
212
- config,
213
- input: { payload },
214
- timeout: 10000,
215
- });
216
- });
217
-
218
- return this;
219
- }
220
-
221
- getScenarios(): TestScenario[] {
222
- return [...this.scenarios];
223
- }
224
- }
@@ -1,58 +0,0 @@
1
- import { expect } from 'chai';
2
- import { TestScenarioBuilder } from './test-scenario-builder';
3
- import { NodeTestRunner } from './node-test-runner';
4
- import { NodeAssertions } from './node-assertions';
5
- import type { TestScenario } from './types';
6
-
7
- /**
8
- * Generic test suite generator for Node-RED custom nodes
9
- */
10
- export function createNodeTestSuite(nodeName: string, nodeConstructor: Function, testConfigs: Record<string, any>) {
11
- describe(`${nodeName} - Generic Test Suite`, function () {
12
- this.timeout(10000);
13
-
14
- describe('Node Registration', function () {
15
- it('should register without errors', async function () {
16
- const scenario: TestScenario = {
17
- name: 'registration',
18
- config: testConfigs.valid || testConfigs.minimal || {},
19
- };
20
-
21
- const context = await NodeTestRunner.runScenario(nodeConstructor, scenario);
22
- expect(context.nodeInstance).to.exist;
23
- expect(context.mockRED.nodes.lastRegisteredType).to.exist;
24
- expect(context.mockRED.nodes.lastRegisteredConstructor).to.be.a('function');
25
- });
26
- });
27
-
28
- describe('Configuration Validation', function () {
29
- const validationTests = new TestScenarioBuilder();
30
-
31
- if (testConfigs.valid) {
32
- validationTests.addValidScenario('valid config', testConfigs.valid);
33
- }
34
-
35
- if (testConfigs.minimal) {
36
- validationTests.addValidScenario('minimal config', testConfigs.minimal);
37
- }
38
-
39
- if (testConfigs.invalid) {
40
- validationTests.addErrorScenario('invalid config', testConfigs.invalid, /error|invalid|missing/i);
41
- }
42
-
43
- validationTests.getScenarios().forEach((scenario) => {
44
- it(`should handle ${scenario.name}`, async function () {
45
- const context = await NodeTestRunner.runScenario(nodeConstructor, scenario);
46
-
47
- expect(context.nodeInstance).to.exist;
48
-
49
- if (scenario.expectedError) {
50
- NodeAssertions.expectError(context, scenario.expectedError);
51
- } else {
52
- NodeAssertions.expectNoErrors(context);
53
- }
54
- });
55
- });
56
- });
57
- });
58
- }
@@ -1,17 +0,0 @@
1
- // Main exports for the Node-RED test framework
2
-
3
- // Unit Testing Framework
4
- export { TestScenarioBuilder } from './test-scenario-builder';
5
- export { NodeTestRunner } from './node-test-runner';
6
- export { NodeAssertions } from './node-assertions';
7
- export { createNodeTestSuite } from './generic-node-test-suite';
8
-
9
- // Integration Testing Framework
10
- export { IntegrationTestRunner } from './integration-test-runner';
11
- export { IntegrationAssertions } from './integration-assertions';
12
- export { IntegrationScenarioBuilder } from './integration-scenario-builder';
13
-
14
- export { ErrorResilienceTestBuilder, EdgeCaseTestBuilder, SecurityTestBuilder } from './advanced-test-patterns';
15
-
16
- // Types
17
- export * from './types';
@@ -1,67 +0,0 @@
1
- import { expect } from 'chai';
2
- import type { IntegrationTestContext } from './integration-test-runner';
3
-
4
- export class IntegrationAssertions {
5
- static expectNodeExists(context: IntegrationTestContext, nodeId: string): void {
6
- expect(context.nodes[nodeId], `Node ${nodeId} should exist`).to.exist;
7
- }
8
-
9
- static expectNodeProperty(context: IntegrationTestContext, nodeId: string, property: string, value: any): void {
10
- this.expectNodeExists(context, nodeId);
11
- expect(context.nodes[nodeId], `Node ${nodeId} should have property ${property}`).to.have.property(
12
- property,
13
- value,
14
- );
15
- }
16
-
17
- static expectMessageReceived(context: IntegrationTestContext, nodeId: string): void {
18
- const messages = context.messages.filter((m) => m.nodeId === nodeId);
19
- expect(messages, `Node ${nodeId} should have received at least one message`).to.have.length.greaterThan(0);
20
- }
21
-
22
- static expectMessageCount(context: IntegrationTestContext, nodeId: string, count: number): void {
23
- const messages = context.messages.filter((m) => m.nodeId === nodeId);
24
- expect(messages, `Node ${nodeId} should have received ${count} messages`).to.have.length(count);
25
- }
26
-
27
- static expectMessageContent(context: IntegrationTestContext, nodeId: string, expectedContent: any): void {
28
- const messages = context.messages.filter((m) => m.nodeId === nodeId);
29
- expect(messages, `Node ${nodeId} should have received messages`).to.have.length.greaterThan(0);
30
-
31
- const lastMessage = messages[messages.length - 1].message;
32
- if (typeof expectedContent === 'object') {
33
- Object.keys(expectedContent).forEach((key) => {
34
- expect(lastMessage, `Message should have property ${key}`).to.have.property(key);
35
- if (expectedContent[key] !== undefined) {
36
- expect(lastMessage[key], `Message property ${key} should match`).to.deep.equal(
37
- expectedContent[key],
38
- );
39
- }
40
- });
41
- } else {
42
- expect(lastMessage.payload).to.equal(expectedContent);
43
- }
44
- }
45
-
46
- static expectNoMessages(context: IntegrationTestContext, nodeId: string): void {
47
- const messages = context.messages.filter((m) => m.nodeId === nodeId);
48
- expect(messages, `Node ${nodeId} should not have received any messages`).to.have.length(0);
49
- }
50
-
51
- static expectAllNodesExist(context: IntegrationTestContext, nodeIds: string[]): void {
52
- nodeIds.forEach((nodeId) => {
53
- this.expectNodeExists(context, nodeId);
54
- });
55
- }
56
-
57
- static expectMessageOrder(context: IntegrationTestContext, nodeIds: string[]): void {
58
- const sortedMessages = [...context.messages].sort((a, b) => a.timestamp - b.timestamp);
59
-
60
- nodeIds.forEach((expectedNodeId, index) => {
61
- expect(sortedMessages[index], `Message ${index} should be from node ${expectedNodeId}`).to.have.property(
62
- 'nodeId',
63
- expectedNodeId,
64
- );
65
- });
66
- }
67
- }
@@ -1,77 +0,0 @@
1
- import { expect } from 'chai';
2
- import type { IntegrationTestScenario } from './integration-test-runner';
3
- import type { Node } from 'node-red';
4
-
5
- export class IntegrationScenarioBuilder {
6
- private scenarios: IntegrationTestScenario[] = [];
7
-
8
- addScenario(scenario: IntegrationTestScenario): this {
9
- this.scenarios.push(scenario);
10
- return this;
11
- }
12
-
13
- addLoadingScenario(name: string, flow: any[], nodeId: string): this {
14
- return this.addScenario({
15
- name,
16
- flow,
17
- nodeId,
18
- timeout: 2000,
19
- });
20
- }
21
-
22
- addMessageFlowScenario(
23
- name: string,
24
- flow: any[],
25
- sourceNodeId: string,
26
- input: any,
27
- expectedMessages: Array<{ nodeId: string; expectedMsg: any }>,
28
- ): this {
29
- return this.addScenario({
30
- name,
31
- flow,
32
- nodeId: sourceNodeId,
33
- input,
34
- expectedMessages,
35
- timeout: 3000,
36
- });
37
- }
38
-
39
- addConnectionScenario(name: string, flow: any[], nodeIds: string[]): this {
40
- return this.addScenario({
41
- name,
42
- flow,
43
- nodeId: nodeIds[0], // Primary node
44
- setup: (nodes) => {
45
- // Verify all nodes are connected
46
- nodeIds.forEach((nodeId) => {
47
- expect(nodes[nodeId], `Node ${nodeId} should exist in connection test`).to.exist;
48
- });
49
- },
50
- });
51
- }
52
-
53
- addLifecycleScenario(
54
- name: string,
55
- flow: any[],
56
- nodeId: string,
57
- operations: Array<(nodes: Record<string, Node>) => void>,
58
- ): this {
59
- return this.addScenario({
60
- name,
61
- flow,
62
- nodeId,
63
- setup: (nodes) => {
64
- operations.forEach((operation) => operation(nodes));
65
- },
66
- });
67
- }
68
-
69
- getScenarios(): IntegrationTestScenario[] {
70
- return [...this.scenarios];
71
- }
72
-
73
- clear(): this {
74
- this.scenarios = [];
75
- return this;
76
- }
77
- }