Skip to content

Commit cd61805

Browse files
author
Derek Zen
committed
feat: implement improvements based on analysis of other implementations
- Add dedicated 'Get Single Email' operation optimized for performance - Implement ParameterValidator utility class for consistent validation - Improve error handling with more specific error messages - Add better type safety and validation across operations - Optimize content extraction using source: true for better performance - Version bump to 2.12.0
1 parent 98b8e89 commit cd61805

File tree

12 files changed

+520
-545
lines changed

12 files changed

+520
-545
lines changed

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
## [2.12.0](https://github.com/callzhang/n8n-nodes-imap/compare/v2.11.10...v2.12.0) (2025-01-XX)
2+
3+
### New Features
4+
5+
* **Single Email Operation**: Added dedicated "Get Single Email" operation optimized for fetching individual emails with full content
6+
* **Parameter Validation**: Implemented consistent parameter validation using ParameterValidator utility class
7+
* **Enhanced Error Handling**: Improved error messages and validation across all operations
8+
9+
### Improvements
10+
11+
* **Performance Optimization**: Single email operation uses `source: true` for better content extraction
12+
* **Better Type Safety**: Added proper TypeScript interfaces and validation
13+
* **Cleaner Code Structure**: Separated concerns with dedicated utility classes
14+
* **Consistent Validation**: Standardized parameter validation across all operations
15+
116
## [2.11.10](https://github.com/callzhang/n8n-nodes-imap/compare/v2.11.9...v2.11.10) (2025-01-XX)
217

318
### Bug Fixes

getEmail.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { ImapFlow } from 'imapflow';
2+
import { ParsedMail, simpleParser } from 'mailparser';
3+
import { IExecuteFunctions, NodeApiError } from 'n8n-workflow';
4+
import { ParameterValidator } from '../utils/helpers';
5+
import { IEmailData, IImapOperation } from '../utils/types';
6+
7+
export class GetEmailOperation implements IImapOperation {
8+
async execute(
9+
executeFunctions: IExecuteFunctions,
10+
client: ImapFlow,
11+
itemIndex: number,
12+
): Promise<IEmailData> {
13+
const mailboxParam = executeFunctions.getNodeParameter('mailbox', itemIndex) as string | { mode: string; value: string };
14+
const mailbox = ParameterValidator.extractMailboxName(mailboxParam);
15+
const emailUid = executeFunctions.getNodeParameter('emailUid', itemIndex) as string;
16+
17+
ParameterValidator.validateMailbox(mailboxParam);
18+
ParameterValidator.validateUid(emailUid);
19+
20+
try {
21+
await client.mailboxOpen(mailbox);
22+
} catch (error) {
23+
throw new NodeApiError(executeFunctions.getNode(), {
24+
message: `Failed to open folder ${mailbox}: ${(error as Error).message}`,
25+
});
26+
}
27+
28+
let message: any;
29+
try {
30+
// First, search for the specific UID to verify it exists
31+
const searchResults = await client.search({ uid: emailUid.toString() });
32+
if (searchResults.length === 0) {
33+
throw new NodeApiError(executeFunctions.getNode(), {
34+
message: `Email with UID ${emailUid} not found in ${mailbox}`,
35+
});
36+
}
37+
38+
// Fetch full email content but skip attachment processing
39+
const messageGenerator = client.fetch(emailUid.toString(), {
40+
source: true, // Full email source for text/html content
41+
uid: true,
42+
flags: true,
43+
size: true
44+
}, { uid: true });
45+
46+
// Get the first (and only) message from the generator
47+
for await (const msg of messageGenerator) {
48+
message = msg;
49+
break;
50+
}
51+
52+
if (!message) {
53+
throw new Error(`No message data received for UID ${emailUid}`);
54+
}
55+
} catch (error) {
56+
throw new NodeApiError(executeFunctions.getNode(), {
57+
message: `Failed to fetch email with UID ${emailUid}: ${(error as Error).message}`,
58+
});
59+
}
60+
61+
if (!message) {
62+
throw new NodeApiError(executeFunctions.getNode(), {
63+
message: `Email with UID ${emailUid} not found in ${mailbox}`,
64+
});
65+
}
66+
67+
// Parse email content but skip attachments
68+
if (!message.source) {
69+
throw new NodeApiError(executeFunctions.getNode(), {
70+
message: `Email source not available for UID ${emailUid}`,
71+
});
72+
}
73+
74+
let parsed: ParsedMail;
75+
try {
76+
// Parse email content (will include attachments info but we'll ignore them)
77+
parsed = await simpleParser(message.source);
78+
console.log('Email parsed successfully:', {
79+
subject: parsed.subject,
80+
fromCount: parsed.from ? 1 : 0,
81+
textLength: typeof parsed.text === 'string' ? parsed.text.length : 0,
82+
htmlLength: typeof parsed.html === 'string' ? parsed.html.length : 0
83+
});
84+
} catch (error) {
85+
console.error('Email parsing failed:', error);
86+
throw new NodeApiError(executeFunctions.getNode(), {
87+
message: `Failed to parse email content: ${(error as Error).message}`,
88+
});
89+
}
90+
91+
// Helper function to normalize address objects to arrays
92+
const normalizeAddresses = (addresses: any): any[] => {
93+
if (!addresses) return [];
94+
return Array.isArray(addresses) ? addresses : [addresses];
95+
};
96+
97+
// Return email data with content but NO attachments
98+
const emailData: IEmailData = {
99+
uid: typeof message.uid === 'string' ? parseInt(message.uid, 10) : message.uid,
100+
subject: parsed.subject || '',
101+
from: parsed.from || {},
102+
to: normalizeAddresses(parsed.to),
103+
cc: normalizeAddresses(parsed.cc),
104+
bcc: normalizeAddresses(parsed.bcc),
105+
date: parsed.date || null,
106+
text: parsed.text || '', // Full text content
107+
html: parsed.html || '', // Full HTML content
108+
attachments: [], // NO attachments - use downloadAttachment operation instead
109+
flags: message.flags || new Set(),
110+
seen: message.flags?.has('\\Seen') || false,
111+
size: message.size
112+
};
113+
114+
console.log('Email data prepared for return:', {
115+
uid: emailData.uid,
116+
subject: emailData.subject,
117+
from: emailData.from,
118+
textLength: emailData.text?.length || 0,
119+
htmlLength: emailData.html?.length || 0,
120+
flagsCount: emailData.flags.size
121+
});
122+
123+
return emailData;
124+
}
125+
}

markEmail.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { ImapFlow } from 'imapflow';
2+
import { IExecuteFunctions, NodeApiError, INodeExecutionData } from 'n8n-workflow';
3+
import { ParameterValidator } from '../utils/helpers';
4+
import { IImapOperation } from '../utils/types';
5+
6+
export class MarkEmailOperation implements IImapOperation {
7+
async execute(
8+
executeFunctions: IExecuteFunctions,
9+
client: ImapFlow,
10+
itemIndex: number,
11+
): Promise<INodeExecutionData[]> {
12+
const mailboxParam = executeFunctions.getNodeParameter('mailbox', itemIndex) as string | { mode: string; value: string };
13+
const mailbox = ParameterValidator.extractMailboxName(mailboxParam);
14+
const emailUid = executeFunctions.getNodeParameter('emailUid', itemIndex) as string;
15+
const markAs = executeFunctions.getNodeParameter('markAs', itemIndex) as string;
16+
17+
ParameterValidator.validateMailbox(mailboxParam);
18+
ParameterValidator.validateUid(emailUid);
19+
20+
const returnData: INodeExecutionData[] = [];
21+
22+
executeFunctions.logger?.info(`Marking email "${emailUid}" as ${markAs} in "${mailbox}"`);
23+
24+
try {
25+
await client.mailboxOpen(mailbox, { readOnly: false });
26+
} catch (error) {
27+
throw new NodeApiError(executeFunctions.getNode(), {
28+
message: `Failed to open mailbox ${mailbox}: ${(error as Error).message}`,
29+
});
30+
}
31+
32+
try {
33+
let result: boolean;
34+
if (markAs === 'read') {
35+
result = await client.messageFlagsAdd(emailUid, ['\\Seen'], { uid: true });
36+
} else {
37+
result = await client.messageFlagsRemove(emailUid, ['\\Seen'], { uid: true });
38+
}
39+
40+
if (!result) {
41+
throw new NodeApiError(executeFunctions.getNode(), {
42+
message: "Unable to mark email - no response from server",
43+
});
44+
}
45+
46+
const jsonData = {
47+
success: true,
48+
message: `Email marked as ${markAs}`,
49+
uid: emailUid,
50+
};
51+
52+
returnData.push({
53+
json: jsonData,
54+
});
55+
56+
return returnData;
57+
} catch (error) {
58+
throw new NodeApiError(executeFunctions.getNode(), {
59+
message: `Failed to mark email: ${(error as Error).message}`,
60+
});
61+
}
62+
}
63+
}

nodes/Imap/operations/email/OperationsList.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { createDraftOperation } from './functions/EmailCreateDraft';
55
import { downloadOperation } from './functions/EmailDownload';
66
import { downloadAttachmentOperation } from './functions/EmailDownloadAttachment';
77
import { getEmailsListOperation } from './functions/EmailGetList';
8+
import { getSingleEmailOperation } from './functions/EmailGetSingle';
89
import { moveEmailOperation } from './functions/EmailMove';
910
import { setEmailFlagsOperation } from './functions/EmailSetFlags';
1011
import { manageEmailLabelsOperation } from './functions/EmailManageLabels';
@@ -13,6 +14,7 @@ export const emailResourceDefinitions: IResourceDef = {
1314
resource: resourceEmail,
1415
operationDefs: [
1516
getEmailsListOperation,
17+
getSingleEmailOperation,
1618
downloadOperation,
1719
downloadAttachmentOperation,
1820
moveEmailOperation,

nodes/Imap/operations/email/functions/EmailGetList.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ export const getEmailsListOperation: IResourceOperationDef = {
322322
if (rawMessage.content) {
323323
const rawContent = await streamToString(rawMessage.content);
324324
const parsed = await simpleParser(rawContent);
325-
325+
326326
// Create envelope-like structure from parsed data
327327
item_json.envelope = {
328328
subject: parsed.subject || '',
@@ -335,13 +335,13 @@ export const getEmailsListOperation: IResourceOperationDef = {
335335
messageId: parsed.messageId || '',
336336
inReplyTo: parsed.inReplyTo || ''
337337
};
338-
338+
339339
// Set flags to empty array if not available
340340
item_json.labels = [];
341-
341+
342342
// Set size from parsed data
343343
item_json.size = rawContent.length;
344-
344+
345345
context.logger?.debug(`Raw message parsing successful for email ${email.uid}`);
346346
}
347347
} catch (error) {
@@ -481,14 +481,14 @@ export const getEmailsListOperation: IResourceOperationDef = {
481481
if (rawMessage.content) {
482482
const rawContent = await streamToString(rawMessage.content);
483483
const parsed = await simpleParser(rawContent);
484-
484+
485485
// Extract content from parsed data
486486
if (parsed.text) {
487487
item_json.text = parsed.text;
488488
item_json.markdown = parsed.text; // Plain text is already readable
489489
item_json.html = textToSimplifiedHtml(parsed.text);
490490
}
491-
491+
492492
if (parsed.html) {
493493
item_json.html = parsed.html;
494494
item_json.markdown = htmlToMarkdown(parsed.html);
@@ -497,14 +497,14 @@ export const getEmailsListOperation: IResourceOperationDef = {
497497
item_json.text = htmlToText(parsed.html);
498498
}
499499
}
500-
500+
501501
// If still no content, set empty values
502502
if (!item_json.text && !item_json.html) {
503503
item_json.text = '';
504504
item_json.markdown = '';
505505
item_json.html = '';
506506
}
507-
507+
508508
context.logger?.debug(`Raw message body parsing successful for email ${email.uid}`);
509509
}
510510
} catch (error) {

0 commit comments

Comments
 (0)