Fix email signature layout and enhance email thread fetching

- Move logo to top of email signature for better formatting
- Enhance IMAP search to include CC/BCC fields and multiple folders
- Fix EOI document generation to properly send and extract signing URLs
- Update documentation with all email system fixes
This commit is contained in:
Matt 2025-06-09 23:09:32 +02:00
parent 77b6aa2752
commit f593036d0f
4 changed files with 137 additions and 80 deletions

View File

@ -253,14 +253,14 @@ const getSignaturePreview = () => {
return ` return `
<div style="margin-top: 20px;"> <div style="margin-top: 20px;">
<img src="/Port_Nimara_Logo_2_Colour_New_Transparent.png" alt="Port Nimara" style="height: 60px; max-width: 200px; margin-bottom: 15px;">
<br>
<div style="font-weight: bold;">${sig.name || 'Your Name'}</div> <div style="font-weight: bold;">${sig.name || 'Your Name'}</div>
<div style="color: #666; margin-bottom: 8px;">${sig.title || 'Your Title'}</div> <div style="color: #666; margin-bottom: 8px;">${sig.title || 'Your Title'}</div>
<div style="font-weight: bold; margin-bottom: 12px;">${sig.company || 'Company Name'}</div> <div style="font-weight: bold; margin-bottom: 12px;">${sig.company || 'Company Name'}</div>
${contactLines ? contactLines + '<br>' : ''} ${contactLines ? contactLines + '<br>' : ''}
<a href="mailto:${userEmail}" style="color: #0066cc;">${userEmail}</a> <a href="mailto:${userEmail}" style="color: #0066cc;">${userEmail}</a>
<br><br> <br><br>
<img src="/Port_Nimara_Logo_2_Colour_New_Transparent.png" alt="Port Nimara" style="height: 60px; max-width: 200px;">
<br>
<div style="color: #666; font-size: 12px; margin-top: 10px;"> <div style="color: #666; font-size: 12px; margin-top: 10px;">
The information in this message is confidential and may be privileged.<br> The information in this message is confidential and may be privileged.<br>
It is intended for the addressee alone.<br> It is intended for the addressee alone.<br>

View File

@ -30,6 +30,27 @@
- Updated `update-interest.ts` API to accept both x-tag headers ("094ut234" and "pjnvü1230") - Updated `update-interest.ts` API to accept both x-tag headers ("094ut234" and "pjnvü1230")
- Now both authenticated and unauthenticated users can save interest updates - Now both authenticated and unauthenticated users can save interest updates
### 5. Email Signature Formatting
- **Problem**: Logo was appearing below the signature details
- **Solution**:
- Moved Port Nimara logo to the top of the signature
- Logo now appears above the name with proper spacing
### 6. Email Refresh Not Showing New Emails
- **Problem**: New emails from clients weren't appearing after refresh
- **Solution**:
- Enhanced IMAP search to include CC and BCC fields
- Now searches in multiple folders: INBOX, Sent, Sent Items, Sent Mail
- Better email detection for comprehensive thread retrieval
### 7. EOI Document Generation Issues
- **Problem**: EOI documents were created but stuck in draft status with non-working links
- **Solution**:
- Fixed response structure to match actual Documenso API response
- Added proper document send step to move from draft to active
- Changed `sendEmail` to `true` to ensure recipients receive signing emails
- Correctly extract signing URLs from the response
## Required Environment Variables ## Required Environment Variables
Make sure these are set in your `.env` file: Make sure these are set in your `.env` file:

View File

@ -163,94 +163,116 @@ async function fetchImapEmails(
}; };
imap.once('ready', () => { imap.once('ready', () => {
imap.openBox('INBOX', true, (err, box) => { // Search in both INBOX and Sent folders
if (err) { const foldersToSearch = ['INBOX', 'Sent', 'Sent Items', 'Sent Mail'];
let currentFolderIndex = 0;
const allEmails: EmailMessage[] = [];
const searchNextFolder = () => {
if (currentFolderIndex >= foldersToSearch.length) {
cleanup(); cleanup();
reject(err); resolve(allEmails);
return; return;
} }
const searchCriteria = [ const folderName = foldersToSearch[currentFolderIndex];
['OR', ['FROM', clientEmail], ['TO', clientEmail]] currentFolderIndex++;
];
imap.search(searchCriteria, (err, results) => { imap.openBox(folderName, true, (err, box) => {
if (err) { if (err) {
cleanup(); console.log(`Folder ${folderName} not found, trying next...`);
reject(err); searchNextFolder();
return; return;
} }
if (!results || results.length === 0) { console.log(`Searching in folder: ${folderName}`);
cleanup();
resolve(emails);
return;
}
const messagesToFetch = results.slice(-limit); // Search for emails both sent and received with this client
let messagesProcessed = 0; const searchCriteria = [
'OR',
['FROM', clientEmail],
['TO', clientEmail],
['CC', clientEmail],
['BCC', clientEmail]
];
const fetch = imap.fetch(messagesToFetch, { imap.search(searchCriteria, (err, results) => {
bodies: '', if (err) {
struct: true, console.error(`Search error in ${folderName}:`, err);
envelope: true searchNextFolder();
}); return;
}
fetch.on('message', (msg, seqno) => { if (!results || results.length === 0) {
msg.on('body', (stream, info) => { console.log(`No emails found in ${folderName}`);
simpleParser(stream as any, async (err: any, parsed: any) => { searchNextFolder();
if (err) { return;
console.error('Parse error:', err); }
messagesProcessed++;
if (messagesProcessed === messagesToFetch.length) { console.log(`Found ${results.length} emails in ${folderName}`);
cleanup(); const messagesToFetch = results.slice(-limit);
resolve(emails); let messagesProcessed = 0;
const fetch = imap.fetch(messagesToFetch, {
bodies: '',
struct: true,
envelope: true
});
fetch.on('message', (msg, seqno) => {
msg.on('body', (stream, info) => {
simpleParser(stream as any, async (err: any, parsed: any) => {
if (err) {
console.error('Parse error:', err);
messagesProcessed++;
if (messagesProcessed === messagesToFetch.length) {
searchNextFolder();
}
return;
} }
return;
}
const email: EmailMessage = { const email: EmailMessage = {
id: parsed.messageId || `${Date.now()}-${seqno}`, id: parsed.messageId || `${Date.now()}-${seqno}`,
from: parsed.from?.text || '', from: parsed.from?.text || '',
to: Array.isArray(parsed.to) to: Array.isArray(parsed.to)
? parsed.to.map((addr: any) => addr.text).join(', ') ? parsed.to.map((addr: any) => addr.text).join(', ')
: parsed.to?.text || '', : parsed.to?.text || '',
subject: parsed.subject || '', subject: parsed.subject || '',
body: parsed.text || '', body: parsed.text || '',
html: parsed.html || undefined, html: parsed.html || undefined,
timestamp: parsed.date?.toISOString() || new Date().toISOString(), timestamp: parsed.date?.toISOString() || new Date().toISOString(),
direction: parsed.from?.text.includes(userEmail) ? 'sent' : 'received' direction: parsed.from?.text.toLowerCase().includes(userEmail.toLowerCase()) ? 'sent' : 'received'
}; };
if (parsed.headers.has('in-reply-to')) { if (parsed.headers.has('in-reply-to')) {
email.threadId = parsed.headers.get('in-reply-to') as string; email.threadId = parsed.headers.get('in-reply-to') as string;
} }
emails.push(email); allEmails.push(email);
messagesProcessed++; messagesProcessed++;
if (messagesProcessed === messagesToFetch.length) { if (messagesProcessed === messagesToFetch.length) {
cleanup(); searchNextFolder();
resolve(emails); }
} });
}); });
}); });
});
fetch.once('error', (err) => { fetch.once('error', (err) => {
console.error('Fetch error:', err); console.error('Fetch error:', err);
cleanup(); searchNextFolder();
reject(err); });
});
fetch.once('end', () => { fetch.once('end', () => {
if (messagesProcessed === 0) { if (messagesProcessed === 0) {
cleanup(); searchNextFolder();
resolve(emails); }
} });
}); });
}); });
}); };
searchNextFolder();
}); });
imap.once('error', (err: any) => { imap.once('error', (err: any) => {

View File

@ -10,8 +10,16 @@ interface DocumensoRecipient {
} }
interface DocumensoResponse { interface DocumensoResponse {
id: string; documentId: number;
recipients: DocumensoRecipient[]; recipients: Array<{
recipientId: number;
name: string;
email: string;
token: string;
role: 'SIGNER' | 'APPROVER';
signingOrder: number;
signingUrl: string;
}>;
} }
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
@ -186,27 +194,33 @@ export default defineEventHandler(async (event) => {
}); });
} }
// 3. Setup completion emails // 3. Send document (moves from draft to active and sends emails)
try { try {
const completionResponse = await fetch(`${documensoBaseUrl}/api/v1/documents/${documentResponse.id}/send`, { const sendResponse = await fetch(`${documensoBaseUrl}/api/v1/documents/${documentResponse.documentId}/send`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${documensoApiKey}` 'Authorization': `Bearer ${documensoApiKey}`
}, },
body: JSON.stringify({ body: JSON.stringify({
sendEmail: false, sendEmail: true,
sendCompletionEmails: true sendCompletionEmails: true
}) })
}); });
if (!completionResponse.ok) { if (!sendResponse.ok) {
console.error('Failed to setup completion emails:', await completionResponse.text()); const errorText = await sendResponse.text();
// Don't fail the whole process if this fails console.error('Failed to send document:', errorText);
throw new Error(`Failed to send document: ${sendResponse.statusText}`);
} }
console.log('Document sent successfully');
} catch (error) { } catch (error) {
console.error('Completion email setup error:', error); console.error('Document send error:', error);
// Continue anyway throw createError({
statusCode: 500,
statusMessage: "Document created but failed to send. Please check Documenso dashboard."
});
} }
// Extract signing URLs from recipients // Extract signing URLs from recipients
@ -261,7 +275,7 @@ export default defineEventHandler(async (event) => {
return { return {
success: true, success: true,
documentId: documentResponse.id, documentId: documentResponse.documentId,
clientSigningUrl: signingLinks['Client'] || '', clientSigningUrl: signingLinks['Client'] || '',
signingLinks: signingLinks signingLinks: signingLinks
}; };