Skip to content

Commit 98b8e89

Browse files
author
Derek Zen
committed
fix: add raw message fallback for servers with limited IMAP support
- Add fallback mechanism for servers that don't return structured IMAP data - Fix body content extraction when bodyStructure is missing - Add raw message parsing to extract envelope data when IMAP envelope is missing - Improve compatibility with IMAP servers that have limited structured data support - Version bump to 2.11.10
1 parent d186f2b commit 98b8e89

File tree

6 files changed

+625
-3
lines changed

6 files changed

+625
-3
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
## [2.11.10](https://github.com/callzhang/n8n-nodes-imap/compare/v2.11.9...v2.11.10) (2025-01-XX)
2+
3+
### Bug Fixes
4+
5+
* **Raw Message Fallback**: Added fallback mechanism for servers that don't return structured IMAP data
6+
* **Body Content Extraction**: Fixed issue where body content was null when IMAP server doesn't provide bodyStructure
7+
* **Envelope Data Fallback**: Added raw message parsing to extract envelope data when IMAP envelope is missing
8+
* **Server Compatibility**: Improved compatibility with IMAP servers that have limited structured data support
9+
110
## [2.11.9](https://github.com/callzhang/n8n-nodes-imap/compare/v2.11.8...v2.11.9) (2025-01-XX)
211

312
### Bug Fixes

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

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ export const getEmailsListOperation: IResourceOperationDef = {
264264

265265
// get enhanced fields parameter
266266
const enhancedFields = context.getNodeParameter('enhancedFields', itemIndex) as boolean;
267-
267+
268268
// if enhanced fields are enabled, we need bodyStructure to extract content
269269
if (enhancedFields) {
270270
fetchQuery.bodyStructure = true;
@@ -314,6 +314,41 @@ export const getEmailsListOperation: IResourceOperationDef = {
314314
item_json.size = email.size;
315315
}
316316

317+
// Fallback: If structured data is missing, try to parse raw message
318+
if (!email.envelope && !email.flags && enhancedFields) {
319+
context.logger?.debug(`Structured data missing for email ${email.uid}, trying raw message parsing...`);
320+
try {
321+
const rawMessage = await client.download(email.uid.toString(), 'TEXT', { uid: true });
322+
if (rawMessage.content) {
323+
const rawContent = await streamToString(rawMessage.content);
324+
const parsed = await simpleParser(rawContent);
325+
326+
// Create envelope-like structure from parsed data
327+
item_json.envelope = {
328+
subject: parsed.subject || '',
329+
from: parsed.from ? [{ address: (parsed.from as any).value?.[0]?.address || '', name: (parsed.from as any).value?.[0]?.name || '' }] : [],
330+
to: parsed.to ? (parsed.to as any).value?.map((addr: any) => ({ address: addr.address || '', name: addr.name || '' })) || [] : [],
331+
cc: parsed.cc ? (parsed.cc as any).value?.map((addr: any) => ({ address: addr.address || '', name: addr.name || '' })) || [] : [],
332+
bcc: parsed.bcc ? (parsed.bcc as any).value?.map((addr: any) => ({ address: addr.address || '', name: addr.name || '' })) || [] : [],
333+
replyTo: parsed.replyTo ? (parsed.replyTo as any).value?.map((addr: any) => ({ address: addr.address || '', name: addr.name || '' })) || [] : [],
334+
date: parsed.date || new Date(),
335+
messageId: parsed.messageId || '',
336+
inReplyTo: parsed.inReplyTo || ''
337+
};
338+
339+
// Set flags to empty array if not available
340+
item_json.labels = [];
341+
342+
// Set size from parsed data
343+
item_json.size = rawContent.length;
344+
345+
context.logger?.debug(`Raw message parsing successful for email ${email.uid}`);
346+
}
347+
} catch (error) {
348+
context.logger?.warn(`Failed to parse raw message for email ${email.uid}: ${error.message}`);
349+
}
350+
}
351+
317352
// Note: All envelope fields are already included in the envelope object above
318353

319354
// process the headers
@@ -339,7 +374,7 @@ export const getEmailsListOperation: IResourceOperationDef = {
339374
var textPartId = null;
340375
var htmlPartId = null;
341376
var attachmentsInfo = [];
342-
377+
343378
context.logger?.debug(`Analyzing body structure for email ${email.uid}: ${analyzeBodyStructure}`);
344379

345380

@@ -438,6 +473,49 @@ export const getEmailsListOperation: IResourceOperationDef = {
438473
}
439474
}
440475

476+
// Fallback: If body content is missing and enhanced fields are enabled, try raw message parsing
477+
if (enhancedFields && (!item_json.text && !item_json.html) && (!textPartId && !htmlPartId)) {
478+
context.logger?.debug(`Body content missing for email ${email.uid}, trying raw message parsing...`);
479+
try {
480+
const rawMessage = await client.download(email.uid.toString(), 'TEXT', { uid: true });
481+
if (rawMessage.content) {
482+
const rawContent = await streamToString(rawMessage.content);
483+
const parsed = await simpleParser(rawContent);
484+
485+
// Extract content from parsed data
486+
if (parsed.text) {
487+
item_json.text = parsed.text;
488+
item_json.markdown = parsed.text; // Plain text is already readable
489+
item_json.html = textToSimplifiedHtml(parsed.text);
490+
}
491+
492+
if (parsed.html) {
493+
item_json.html = parsed.html;
494+
item_json.markdown = htmlToMarkdown(parsed.html);
495+
// if we don't have text content, create plain text from the HTML content
496+
if (!item_json.text) {
497+
item_json.text = htmlToText(parsed.html);
498+
}
499+
}
500+
501+
// If still no content, set empty values
502+
if (!item_json.text && !item_json.html) {
503+
item_json.text = '';
504+
item_json.markdown = '';
505+
item_json.html = '';
506+
}
507+
508+
context.logger?.debug(`Raw message body parsing successful for email ${email.uid}`);
509+
}
510+
} catch (error) {
511+
context.logger?.warn(`Failed to parse raw message body for email ${email.uid}: ${error.message}`);
512+
// Set empty values as fallback
513+
item_json.text = '';
514+
item_json.markdown = '';
515+
item_json.html = '';
516+
}
517+
}
518+
441519
returnData.push({
442520
json: item_json,
443521
});

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "n8n-nodes-imap-enhanced",
3-
"version": "2.11.9",
3+
"version": "2.11.10",
44
"description": "Enhanced IMAP node with custom labels support, limit parameters, and structured email fields for n8n workflows.",
55
"keywords": [
66
"n8n-community-node-package"

test-body-fetching.js

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
const { ImapFlow } = require('imapflow');
2+
const { NodeHtmlMarkdown } = require('node-html-markdown');
3+
4+
// Test credentials
5+
const config = {
6+
host: 'imap.qiye.aliyun.com',
7+
port: 993,
8+
secure: true,
9+
auth: {
10+
11+
pass: 'wTYaIgdcdeLDeG0V'
12+
}
13+
};
14+
15+
// HTML conversion functions
16+
const nhm = new NodeHtmlMarkdown();
17+
18+
function htmlToMarkdown(html) {
19+
if (!html) return '';
20+
return nhm.translate(html);
21+
}
22+
23+
function htmlToText(html) {
24+
if (!html) return '';
25+
let text = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '');
26+
text = text.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');
27+
text = text
28+
.replace(/<br[^>]*>/gi, '\n')
29+
.replace(/<p[^>]*>(.*?)<\/p>/gi, '\n\n$1\n\n')
30+
.replace(/<h[1-6][^>]*>(.*?)<\/h[1-6]>/gi, '\n\n$1\n\n')
31+
.replace(/<div[^>]*>(.*?)<\/div>/gi, '\n$1\n')
32+
.replace(/<ul[^>]*>(.*?)<\/ul>/gi, '\n$1\n')
33+
.replace(/<ol[^>]*>(.*?)<\/ol>/gi, '\n$1\n')
34+
.replace(/<li[^>]*>(.*?)<\/li>/gi, '• $1\n')
35+
.replace(/<blockquote[^>]*>(.*?)<\/blockquote>/gi, '\n> $1\n')
36+
.replace(/<a[^>]*href=["']([^"']*)["'][^>]*>(.*?)<\/a>/gi, '$2 ($1)')
37+
.replace(/<[^>]*>/g, '')
38+
.replace(/&nbsp;/g, ' ')
39+
.replace(/&amp;/g, '&')
40+
.replace(/&lt;/g, '<')
41+
.replace(/&gt;/g, '>')
42+
.replace(/&quot;/g, '"')
43+
.replace(/&#39;/g, "'")
44+
.replace(/&apos;/g, "'")
45+
.replace(/\n\s*\n\s*\n/g, '\n\n')
46+
.replace(/^\s+|\s+$/g, '')
47+
.replace(/[ \t]+/g, ' ')
48+
.replace(/\n /g, '\n')
49+
.replace(/ \n/g, '\n');
50+
return text.trim();
51+
}
52+
53+
async function streamToString(stream) {
54+
const chunks = [];
55+
for await (const chunk of stream) {
56+
chunks.push(chunk);
57+
}
58+
return Buffer.concat(chunks).toString('utf8');
59+
}
60+
61+
async function testBodyFetching() {
62+
console.log('🔌 Connecting to IMAP server...');
63+
const client = new ImapFlow(config);
64+
65+
try {
66+
await client.connect();
67+
console.log('✅ Connected successfully!');
68+
69+
// Open INBOX
70+
console.log('📁 Opening INBOX...');
71+
await client.mailboxOpen('INBOX');
72+
console.log('✅ INBOX opened successfully!');
73+
74+
// Search for recent emails
75+
console.log('🔍 Searching for recent emails...');
76+
const searchResult = await client.search({ seen: false }, { uid: true });
77+
console.log(`Found ${searchResult.length} unread emails`);
78+
79+
if (searchResult.length === 0) {
80+
console.log('📧 No unread emails found, searching for any recent emails...');
81+
const allEmails = await client.search({ all: true }, { uid: true });
82+
console.log(`Found ${allEmails.length} total emails`);
83+
84+
if (allEmails.length === 0) {
85+
console.log('❌ No emails found in mailbox');
86+
return;
87+
}
88+
89+
// Use the most recent email
90+
searchResult.push(allEmails[allEmails.length - 1]);
91+
}
92+
93+
// Test with the first email
94+
const emailUid = searchResult[0];
95+
console.log(`📧 Testing with email UID: ${emailUid}`);
96+
97+
// Fetch email with body structure
98+
console.log('📥 Fetching email with body structure...');
99+
const fetchQuery = {
100+
uid: true,
101+
envelope: true,
102+
bodyStructure: true,
103+
flags: true
104+
};
105+
106+
const email = await client.fetchOne(emailUid, fetchQuery);
107+
console.log('✅ Email fetched successfully!');
108+
109+
// Log envelope info
110+
console.log('\n📋 Email Envelope:');
111+
console.log(` Subject: ${email.envelope?.subject || 'No subject'}`);
112+
console.log(` From: ${email.envelope?.from?.[0]?.address || 'No from'}`);
113+
console.log(` Date: ${email.envelope?.date || 'No date'}`);
114+
115+
// Log body structure
116+
console.log('\n🏗️ Body Structure:');
117+
console.log(JSON.stringify(email.bodyStructure, null, 2));
118+
119+
// Analyze body structure
120+
if (email.bodyStructure) {
121+
console.log('\n🔍 Analyzing body structure...');
122+
123+
// Simple body structure analysis
124+
function analyzeBodyStructure(bodyStructure, partId = '') {
125+
const parts = [];
126+
127+
if (bodyStructure.type) {
128+
const currentPart = {
129+
partId: partId || 'TEXT',
130+
type: bodyStructure.type,
131+
subtype: bodyStructure.subtype,
132+
disposition: bodyStructure.disposition,
133+
filename: bodyStructure.dispositionParameters?.filename
134+
};
135+
parts.push(currentPart);
136+
}
137+
138+
if (bodyStructure.childNodes) {
139+
bodyStructure.childNodes.forEach((child, index) => {
140+
const childPartId = partId ? `${partId}.${index + 1}` : `${index + 1}`;
141+
parts.push(...analyzeBodyStructure(child, childPartId));
142+
});
143+
}
144+
145+
return parts;
146+
}
147+
148+
const partsInfo = analyzeBodyStructure(email.bodyStructure);
149+
console.log('📦 Parts found:');
150+
partsInfo.forEach(part => {
151+
console.log(` - Part ${part.partId}: ${part.type}/${part.subtype} (${part.disposition || 'inline'})`);
152+
if (part.filename) {
153+
console.log(` Filename: ${part.filename}`);
154+
}
155+
});
156+
157+
// Find text and HTML parts
158+
const textPart = partsInfo.find(part => part.type === 'text' && part.subtype === 'plain');
159+
const htmlPart = partsInfo.find(part => part.type === 'text' && part.subtype === 'html');
160+
161+
console.log(`\n📝 Text part: ${textPart ? `Part ${textPart.partId}` : 'Not found'}`);
162+
console.log(`🌐 HTML part: ${htmlPart ? `Part ${htmlPart.partId}` : 'Not found'}`);
163+
164+
// Try to download content
165+
if (textPart) {
166+
console.log('\n📥 Downloading text content...');
167+
try {
168+
const textContent = await client.download(emailUid, textPart.partId, { uid: true });
169+
if (textContent.content) {
170+
const text = await streamToString(textContent.content);
171+
console.log('✅ Text content downloaded:');
172+
console.log(`Length: ${text.length} characters`);
173+
console.log(`Preview: ${text.substring(0, 200)}...`);
174+
} else {
175+
console.log('❌ No text content found');
176+
}
177+
} catch (error) {
178+
console.log(`❌ Error downloading text content: ${error.message}`);
179+
}
180+
}
181+
182+
if (htmlPart) {
183+
console.log('\n📥 Downloading HTML content...');
184+
try {
185+
const htmlContent = await client.download(emailUid, htmlPart.partId, { uid: true });
186+
if (htmlContent.content) {
187+
const html = await streamToString(htmlContent.content);
188+
console.log('✅ HTML content downloaded:');
189+
console.log(`Length: ${html.length} characters`);
190+
console.log(`Preview: ${html.substring(0, 200)}...`);
191+
192+
// Test conversions
193+
console.log('\n🔄 Testing conversions...');
194+
const markdown = htmlToMarkdown(html);
195+
const text = htmlToText(html);
196+
197+
console.log(`Markdown length: ${markdown.length} characters`);
198+
console.log(`Text length: ${text.length} characters`);
199+
console.log(`Markdown preview: ${markdown.substring(0, 200)}...`);
200+
console.log(`Text preview: ${text.substring(0, 200)}...`);
201+
} else {
202+
console.log('❌ No HTML content found');
203+
}
204+
} catch (error) {
205+
console.log(`❌ Error downloading HTML content: ${error.message}`);
206+
}
207+
}
208+
209+
// If no specific parts found, try downloading the whole message
210+
if (!textPart && !htmlPart) {
211+
console.log('\n📥 No specific parts found, trying to download whole message...');
212+
try {
213+
const wholeMessage = await client.download(emailUid, 'TEXT', { uid: true });
214+
if (wholeMessage.content) {
215+
const content = await streamToString(wholeMessage.content);
216+
console.log('✅ Whole message downloaded:');
217+
console.log(`Length: ${content.length} characters`);
218+
console.log(`Preview: ${content.substring(0, 500)}...`);
219+
} else {
220+
console.log('❌ No whole message content found');
221+
}
222+
} catch (error) {
223+
console.log(`❌ Error downloading whole message: ${error.message}`);
224+
}
225+
}
226+
227+
} else {
228+
console.log('❌ No body structure found');
229+
}
230+
231+
} catch (error) {
232+
console.error('❌ Error:', error.message);
233+
console.error('Stack:', error.stack);
234+
} finally {
235+
console.log('\n🔌 Closing connection...');
236+
await client.logout();
237+
console.log('✅ Connection closed');
238+
}
239+
}
240+
241+
// Run the test
242+
console.log('🚀 Starting body fetching test...');
243+
testBodyFetching().catch(console.error);

0 commit comments

Comments
 (0)