mail page rest
This commit is contained in:
parent
a6b38ee56e
commit
58fc60904e
@ -27,7 +27,6 @@ import {
|
|||||||
AlertOctagon, Archive, RefreshCw
|
AlertOctagon, Archive, RefreshCw
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { useSession } from 'next-auth/react';
|
|
||||||
|
|
||||||
interface Account {
|
interface Account {
|
||||||
id: number;
|
id: number;
|
||||||
@ -41,7 +40,7 @@ interface Email {
|
|||||||
id: number;
|
id: number;
|
||||||
accountId: number;
|
accountId: number;
|
||||||
from: string;
|
from: string;
|
||||||
fromName: string;
|
fromName?: string;
|
||||||
to: string;
|
to: string;
|
||||||
subject: string;
|
subject: string;
|
||||||
body: string;
|
body: string;
|
||||||
@ -61,31 +60,6 @@ interface Attachment {
|
|||||||
encoding: string;
|
encoding: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ParsedEmailContent {
|
|
||||||
text: string | null;
|
|
||||||
html: string | null;
|
|
||||||
attachments: Array<{
|
|
||||||
filename: string;
|
|
||||||
contentType: string;
|
|
||||||
encoding: string;
|
|
||||||
content: string;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ParsedEmailMetadata {
|
|
||||||
subject: string;
|
|
||||||
from: string;
|
|
||||||
to: string;
|
|
||||||
date: string;
|
|
||||||
contentType: string;
|
|
||||||
text: string | null;
|
|
||||||
html: string | null;
|
|
||||||
raw: {
|
|
||||||
headers: string;
|
|
||||||
body: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Improved MIME Decoder Implementation for Infomaniak
|
// Improved MIME Decoder Implementation for Infomaniak
|
||||||
function extractBoundary(headers: string): string | null {
|
function extractBoundary(headers: string): string | null {
|
||||||
const boundaryMatch = headers.match(/boundary="?([^"\r\n;]+)"?/i) ||
|
const boundaryMatch = headers.match(/boundary="?([^"\r\n;]+)"?/i) ||
|
||||||
@ -122,144 +96,113 @@ function decodeQuotedPrintable(text: string, charset: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseFullEmail(emailRaw: string): ParsedEmailContent {
|
function parseFullEmail(emailRaw: string) {
|
||||||
console.log('=== parseFullEmail Debug ===');
|
// Check if this is a multipart message by looking for boundary definition
|
||||||
console.log('Input email length:', emailRaw.length);
|
const boundaryMatch = emailRaw.match(/boundary="?([^"\r\n;]+)"?/i) ||
|
||||||
console.log('First 200 chars:', emailRaw.substring(0, 200));
|
emailRaw.match(/boundary=([^\r\n;]+)/i);
|
||||||
|
|
||||||
// Split headers and body
|
if (boundaryMatch) {
|
||||||
const headerBodySplit = emailRaw.split(/\r?\n\r?\n/);
|
const boundary = boundaryMatch[1].trim();
|
||||||
const headers = headerBodySplit[0];
|
|
||||||
const body = headerBodySplit.slice(1).join('\n\n');
|
|
||||||
|
|
||||||
// Parse content type from headers
|
|
||||||
const contentTypeMatch = headers.match(/Content-Type:\s*([^;]+)/i);
|
|
||||||
const contentType = contentTypeMatch ? contentTypeMatch[1].trim().toLowerCase() : 'text/plain';
|
|
||||||
|
|
||||||
// Initialize result
|
|
||||||
const result: ParsedEmailContent = {
|
|
||||||
text: null,
|
|
||||||
html: null,
|
|
||||||
attachments: []
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle multipart content
|
|
||||||
if (contentType.includes('multipart')) {
|
|
||||||
const boundaryMatch = emailRaw.match(/boundary="?([^"\r\n;]+)"?/i) ||
|
|
||||||
emailRaw.match(/boundary=([^\r\n;]+)/i);
|
|
||||||
|
|
||||||
if (boundaryMatch) {
|
// Check if there's a preamble before the first boundary
|
||||||
const boundary = boundaryMatch[1].trim();
|
let mainHeaders = '';
|
||||||
const parts = emailRaw.split(new RegExp(`--${boundary}(?:--)?(\\r?\\n|$)`));
|
let mainContent = emailRaw;
|
||||||
|
|
||||||
|
// Extract the headers before the first boundary if they exist
|
||||||
|
const firstBoundaryPos = emailRaw.indexOf('--' + boundary);
|
||||||
|
if (firstBoundaryPos > 0) {
|
||||||
|
const headerSeparatorPos = emailRaw.indexOf('\r\n\r\n');
|
||||||
|
if (headerSeparatorPos > 0 && headerSeparatorPos < firstBoundaryPos) {
|
||||||
|
mainHeaders = emailRaw.substring(0, headerSeparatorPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return processMultipartEmail(emailRaw, boundary, mainHeaders);
|
||||||
|
} else {
|
||||||
|
// This is a single part message
|
||||||
|
return processSinglePartEmail(emailRaw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processMultipartEmail(emailRaw: string, boundary: string, mainHeaders: string = ''): {
|
||||||
|
text: string;
|
||||||
|
html: string;
|
||||||
|
attachments: { filename: string; contentType: string; encoding: string; content: string; }[];
|
||||||
|
headers?: string;
|
||||||
|
} {
|
||||||
|
const result = {
|
||||||
|
text: '',
|
||||||
|
html: '',
|
||||||
|
attachments: [] as { filename: string; contentType: string; encoding: string; content: string; }[],
|
||||||
|
headers: mainHeaders
|
||||||
|
};
|
||||||
|
|
||||||
|
// Split by boundary (more robust pattern)
|
||||||
|
const boundaryRegex = new RegExp(`--${boundary}(?:--)?(\\r?\\n|$)`, 'g');
|
||||||
|
|
||||||
|
// Get all boundary positions
|
||||||
|
const matches = Array.from(emailRaw.matchAll(boundaryRegex));
|
||||||
|
const boundaryPositions = matches.map(match => match.index!);
|
||||||
|
|
||||||
|
// Extract content between boundaries
|
||||||
|
for (let i = 0; i < boundaryPositions.length - 1; i++) {
|
||||||
|
const startPos = boundaryPositions[i] + matches[i][0].length;
|
||||||
|
const endPos = boundaryPositions[i + 1];
|
||||||
|
|
||||||
|
if (endPos > startPos) {
|
||||||
|
const partContent = emailRaw.substring(startPos, endPos).trim();
|
||||||
|
|
||||||
for (const part of parts) {
|
if (partContent) {
|
||||||
if (!part.trim()) continue;
|
const decoded = processSinglePartEmail(partContent);
|
||||||
|
|
||||||
const partHeaderBodySplit = part.split(/\r?\n\r?\n/);
|
if (decoded.contentType.includes('text/plain')) {
|
||||||
const partHeaders = partHeaderBodySplit[0];
|
result.text = decoded.text || '';
|
||||||
const partBody = partHeaderBodySplit.slice(1).join('\n\n');
|
} else if (decoded.contentType.includes('text/html')) {
|
||||||
|
result.html = cleanHtml(decoded.html || '');
|
||||||
const partContentTypeMatch = partHeaders.match(/Content-Type:\s*([^;]+)/i);
|
} else if (
|
||||||
const partContentType = partContentTypeMatch ? partContentTypeMatch[1].trim().toLowerCase() : 'text/plain';
|
decoded.contentType.startsWith('image/') ||
|
||||||
|
decoded.contentType.startsWith('application/')
|
||||||
if (partContentType.includes('text/plain')) {
|
) {
|
||||||
result.text = decodeEmailBody(partBody, partContentType);
|
const filename = extractFilename(partContent);
|
||||||
} else if (partContentType.includes('text/html')) {
|
|
||||||
result.html = decodeEmailBody(partBody, partContentType);
|
|
||||||
} else if (partContentType.startsWith('image/') || partContentType.startsWith('application/')) {
|
|
||||||
const filenameMatch = partHeaders.match(/filename="?([^"\r\n;]+)"?/i);
|
|
||||||
const filename = filenameMatch ? filenameMatch[1] : 'attachment';
|
|
||||||
|
|
||||||
result.attachments.push({
|
result.attachments.push({
|
||||||
filename,
|
filename,
|
||||||
contentType: partContentType,
|
contentType: decoded.contentType,
|
||||||
encoding: 'base64',
|
encoding: decoded.raw?.headers ? parseEmailHeaders(decoded.raw.headers).encoding : '7bit',
|
||||||
content: partBody
|
content: decoded.raw?.body || ''
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Single part content
|
|
||||||
if (contentType.includes('text/html')) {
|
|
||||||
result.html = decodeEmailBody(body, contentType);
|
|
||||||
} else {
|
|
||||||
result.text = decodeEmailBody(body, contentType);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no content was found, try to extract content directly
|
|
||||||
if (!result.text && !result.html) {
|
|
||||||
// Try to extract HTML content
|
|
||||||
const htmlMatch = emailRaw.match(/<html[^>]*>[\s\S]*?<\/html>/i);
|
|
||||||
if (htmlMatch) {
|
|
||||||
result.html = decodeEmailBody(htmlMatch[0], 'text/html');
|
|
||||||
} else {
|
|
||||||
// Try to extract plain text
|
|
||||||
const textContent = emailRaw
|
|
||||||
.replace(/<[^>]+>/g, '')
|
|
||||||
.replace(/ /g, ' ')
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/\r\n/g, '\n')
|
|
||||||
.replace(/=\n/g, '')
|
|
||||||
.replace(/=3D/g, '=')
|
|
||||||
.replace(/=09/g, '\t')
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
if (textContent) {
|
|
||||||
result.text = textContent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function decodeEmailBody(content: string, contentType: string): string {
|
function processSinglePartEmail(rawEmail: string) {
|
||||||
try {
|
// Split headers and body
|
||||||
// Remove email client-specific markers
|
const headerBodySplit = rawEmail.split(/\r?\n\r?\n/);
|
||||||
content = content.replace(/\r\n/g, '\n')
|
const headers = headerBodySplit[0];
|
||||||
.replace(/=\n/g, '')
|
const body = headerBodySplit.slice(1).join('\n\n');
|
||||||
.replace(/=3D/g, '=')
|
|
||||||
.replace(/=09/g, '\t');
|
// Parse headers to get content type, encoding, etc.
|
||||||
|
const emailInfo = parseEmailHeaders(headers);
|
||||||
// If it's HTML content
|
|
||||||
if (contentType.includes('text/html')) {
|
// Decode the body based on its encoding
|
||||||
return extractTextFromHtml(content);
|
const decodedBody = decodeMIME(body, emailInfo.encoding, emailInfo.charset);
|
||||||
|
|
||||||
|
return {
|
||||||
|
subject: extractHeader(headers, 'Subject'),
|
||||||
|
from: extractHeader(headers, 'From'),
|
||||||
|
to: extractHeader(headers, 'To'),
|
||||||
|
date: extractHeader(headers, 'Date'),
|
||||||
|
contentType: emailInfo.contentType,
|
||||||
|
text: emailInfo.contentType.includes('html') ? null : decodedBody,
|
||||||
|
html: emailInfo.contentType.includes('html') ? decodedBody : null,
|
||||||
|
raw: {
|
||||||
|
headers,
|
||||||
|
body
|
||||||
}
|
}
|
||||||
|
};
|
||||||
return content;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error decoding email body:', error);
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractTextFromHtml(html: string): string {
|
|
||||||
// Remove scripts and style tags
|
|
||||||
html = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
||||||
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');
|
|
||||||
|
|
||||||
// Convert <br> and <p> to newlines
|
|
||||||
html = html.replace(/<br[^>]*>/gi, '\n')
|
|
||||||
.replace(/<p[^>]*>/gi, '\n')
|
|
||||||
.replace(/<\/p>/gi, '\n');
|
|
||||||
|
|
||||||
// Remove all other HTML tags
|
|
||||||
html = html.replace(/<[^>]+>/g, '');
|
|
||||||
|
|
||||||
// Decode HTML entities
|
|
||||||
html = html.replace(/ /g, ' ')
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"');
|
|
||||||
|
|
||||||
// Clean up whitespace
|
|
||||||
return html.replace(/\n\s*\n/g, '\n\n').trim();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractHeader(headers: string, headerName: string): string {
|
function extractHeader(headers: string, headerName: string): string {
|
||||||
@ -437,7 +380,7 @@ function cleanHtml(html: string): string {
|
|||||||
function decodeMimeContent(content: string): string {
|
function decodeMimeContent(content: string): string {
|
||||||
if (!content) return '';
|
if (!content) return '';
|
||||||
|
|
||||||
// Check if this is a multipart message
|
// Check if this is an Infomaniak multipart message
|
||||||
if (content.includes('Content-Type: multipart/')) {
|
if (content.includes('Content-Type: multipart/')) {
|
||||||
const boundary = content.match(/boundary="([^"]+)"/)?.[1];
|
const boundary = content.match(/boundary="([^"]+)"/)?.[1];
|
||||||
if (boundary) {
|
if (boundary) {
|
||||||
@ -468,96 +411,29 @@ function decodeMimeContent(content: string): string {
|
|||||||
return cleanHtml(content);
|
return cleanHtml(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderEmailContent(email: Email) {
|
// Add this helper function
|
||||||
console.log('=== renderEmailContent Debug ===');
|
const renderEmailContent = (email: Email) => {
|
||||||
console.log('Email ID:', email.id);
|
const decodedContent = decodeMimeContent(email.body);
|
||||||
console.log('Subject:', email.subject);
|
if (email.body.includes('Content-Type: text/html')) {
|
||||||
console.log('Body length:', email.body.length);
|
return <div dangerouslySetInnerHTML={{ __html: decodedContent }} />;
|
||||||
console.log('First 100 chars:', email.body.substring(0, 100));
|
|
||||||
|
|
||||||
try {
|
|
||||||
// First try to parse the full email
|
|
||||||
const parsed = parseFullEmail(email.body);
|
|
||||||
console.log('Parsed content:', {
|
|
||||||
hasText: !!parsed.text,
|
|
||||||
hasHtml: !!parsed.html,
|
|
||||||
hasAttachments: parsed.attachments.length > 0
|
|
||||||
});
|
|
||||||
|
|
||||||
// Determine content and type
|
|
||||||
let content = '';
|
|
||||||
let isHtml = false;
|
|
||||||
|
|
||||||
if (parsed.html) {
|
|
||||||
// Use our existing MIME decoding for HTML content
|
|
||||||
content = decodeMIME(parsed.html, 'quoted-printable', 'utf-8');
|
|
||||||
isHtml = true;
|
|
||||||
} else if (parsed.text) {
|
|
||||||
// Use our existing MIME decoding for plain text content
|
|
||||||
content = decodeMIME(parsed.text, 'quoted-printable', 'utf-8');
|
|
||||||
isHtml = false;
|
|
||||||
} else {
|
|
||||||
// Try to extract content directly from body using our existing functions
|
|
||||||
const htmlMatch = email.body.match(/<html[^>]*>[\s\S]*?<\/html>/i);
|
|
||||||
if (htmlMatch) {
|
|
||||||
content = decodeMIME(htmlMatch[0], 'quoted-printable', 'utf-8');
|
|
||||||
isHtml = true;
|
|
||||||
} else {
|
|
||||||
// Use our existing text extraction function
|
|
||||||
content = extractTextFromHtml(email.body);
|
|
||||||
isHtml = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!content) {
|
|
||||||
console.log('No content available after all attempts');
|
|
||||||
return <div className="text-gray-500">No content available</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle attachments
|
|
||||||
const attachmentElements = parsed.attachments.map((attachment, index) => (
|
|
||||||
<div key={index} className="mt-4 p-4 border rounded-lg bg-gray-50">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Paperclip className="h-5 w-5 text-gray-400 mr-2" />
|
|
||||||
<span className="text-sm text-gray-600">{attachment.filename}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="prose max-w-none dark:prose-invert">
|
|
||||||
{isHtml ? (
|
|
||||||
<div
|
|
||||||
className="prose prose-sm sm:prose lg:prose-lg xl:prose-xl dark:prose-invert max-w-none"
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: content
|
|
||||||
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
||||||
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
||||||
.replace(/<base[^>]*>/gi, '')
|
|
||||||
.replace(/<meta[^>]*>/gi, '')
|
|
||||||
.replace(/<link[^>]*>/gi, '')
|
|
||||||
.replace(/<title[^>]*>[\s\S]*?<\/title>/gi, '')
|
|
||||||
.replace(/<head[^>]*>[\s\S]*?<\/head>/gi, '')
|
|
||||||
.replace(/<body[^>]*>/gi, '')
|
|
||||||
.replace(/<\/body>/gi, '')
|
|
||||||
.replace(/<html[^>]*>/gi, '')
|
|
||||||
.replace(/<\/html>/gi, '')
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="whitespace-pre-wrap font-sans text-base leading-relaxed">
|
|
||||||
{content.split('\n').map((line, i) => (
|
|
||||||
<p key={i} className="mb-2">{line}</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{attachmentElements}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error parsing email:', e);
|
|
||||||
return <div className="text-gray-500">Error displaying email content</div>;
|
|
||||||
}
|
}
|
||||||
|
return <div className="whitespace-pre-wrap">{decodedContent}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add this helper function
|
||||||
|
const decodeEmailContent = (content: string, charset: string = 'utf-8') => {
|
||||||
|
return convertCharset(content, charset);
|
||||||
|
};
|
||||||
|
|
||||||
|
function cleanEmailContent(content: string): string {
|
||||||
|
// Remove or fix malformed URLs
|
||||||
|
return content.replace(/=3D"(http[^"]+)"/g, (match, url) => {
|
||||||
|
try {
|
||||||
|
return `"${decodeURIComponent(url)}"`;
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define the exact folder names from IMAP
|
// Define the exact folder names from IMAP
|
||||||
@ -594,9 +470,8 @@ const initialSidebarItems = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function CourrierPage() {
|
export default function MailPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data: session } = useSession();
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [accounts, setAccounts] = useState<Account[]>([
|
const [accounts, setAccounts] = useState<Account[]>([
|
||||||
{ id: 0, name: 'All', email: '', color: 'bg-gray-500' },
|
{ id: 0, name: 'All', email: '', color: 'bg-gray-500' },
|
||||||
@ -639,46 +514,7 @@ export default function CourrierPage() {
|
|||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [hasMore, setHasMore] = useState(true);
|
const [hasMore, setHasMore] = useState(true);
|
||||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||||
const [isLoadingInitial, setIsLoadingInitial] = useState(true);
|
const emailsPerPage = 24;
|
||||||
const [isLoadingSearch, setIsLoadingSearch] = useState(false);
|
|
||||||
const [isLoadingCompose, setIsLoadingCompose] = useState(false);
|
|
||||||
const [isLoadingReply, setIsLoadingReply] = useState(false);
|
|
||||||
const [isLoadingForward, setIsLoadingForward] = useState(false);
|
|
||||||
const [isLoadingDelete, setIsLoadingDelete] = useState(false);
|
|
||||||
const [isLoadingMove, setIsLoadingMove] = useState(false);
|
|
||||||
const [isLoadingStar, setIsLoadingStar] = useState(false);
|
|
||||||
const [isLoadingUnstar, setIsLoadingUnstar] = useState(false);
|
|
||||||
const [isLoadingMarkRead, setIsLoadingMarkRead] = useState(false);
|
|
||||||
const [isLoadingMarkUnread, setIsLoadingMarkUnread] = useState(false);
|
|
||||||
const [isLoadingRefresh, setIsLoadingRefresh] = useState(false);
|
|
||||||
const emailsPerPage = 20;
|
|
||||||
const [isSearching, setIsSearching] = useState(false);
|
|
||||||
const [searchResults, setSearchResults] = useState<Email[]>([]);
|
|
||||||
const [showSearchResults, setShowSearchResults] = useState(false);
|
|
||||||
const [isComposing, setIsComposing] = useState(false);
|
|
||||||
const [composeEmail, setComposeEmail] = useState({
|
|
||||||
to: '',
|
|
||||||
subject: '',
|
|
||||||
body: '',
|
|
||||||
});
|
|
||||||
const [isSending, setIsSending] = useState(false);
|
|
||||||
const [isReplying, setIsReplying] = useState(false);
|
|
||||||
const [isForwarding, setIsForwarding] = useState(false);
|
|
||||||
const [replyToEmail, setReplyToEmail] = useState<Email | null>(null);
|
|
||||||
const [forwardEmail, setForwardEmail] = useState<Email | null>(null);
|
|
||||||
const [replyBody, setReplyBody] = useState('');
|
|
||||||
const [forwardBody, setForwardBody] = useState('');
|
|
||||||
const [replyAttachments, setReplyAttachments] = useState<File[]>([]);
|
|
||||||
const [forwardAttachments, setForwardAttachments] = useState<File[]>([]);
|
|
||||||
const [isSendingReply, setIsSendingReply] = useState(false);
|
|
||||||
const [isSendingForward, setIsSendingForward] = useState(false);
|
|
||||||
const [isDeleting, setIsDeleting] = useState(false);
|
|
||||||
const [isMoving, setIsMoving] = useState(false);
|
|
||||||
const [isStarring, setIsStarring] = useState(false);
|
|
||||||
const [isUnstarring, setIsUnstarring] = useState(false);
|
|
||||||
const [isMarkingRead, setIsMarkingRead] = useState(false);
|
|
||||||
const [isMarkingUnread, setIsMarkingUnread] = useState(false);
|
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
||||||
|
|
||||||
// Debug logging for email distribution
|
// Debug logging for email distribution
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -748,7 +584,7 @@ export default function CourrierPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Process emails keeping exact folder names
|
// Process emails keeping exact folder names
|
||||||
const processedEmails = (data.emails || []).map((email: any) => ({
|
const processedEmails = data.emails.map((email: any) => ({
|
||||||
id: Number(email.id),
|
id: Number(email.id),
|
||||||
accountId: 1,
|
accountId: 1,
|
||||||
from: email.from || '',
|
from: email.from || '',
|
||||||
@ -818,61 +654,25 @@ export default function CourrierPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Update handleEmailSelect to set selectedEmail correctly
|
// Update handleEmailSelect to set selectedEmail correctly
|
||||||
const handleEmailSelect = async (emailId: number) => {
|
const handleEmailSelect = (emailId: number) => {
|
||||||
const email = emails.find(e => e.id === emailId);
|
const email = emails.find(e => e.id === emailId);
|
||||||
if (!email) {
|
if (email) {
|
||||||
console.error('Email not found in list');
|
setSelectedEmail(email);
|
||||||
return;
|
if (!email.read) {
|
||||||
}
|
// Mark as read in state
|
||||||
|
setEmails(emails.map(e =>
|
||||||
// Set the selected email first to show preview immediately
|
e.id === emailId ? { ...e, read: true } : e
|
||||||
setSelectedEmail(email);
|
));
|
||||||
|
|
||||||
// Fetch the full email content
|
// Update read status on server
|
||||||
const response = await fetch(`/api/mail/${emailId}`);
|
fetch('/api/mail/mark-read', {
|
||||||
if (!response.ok) {
|
method: 'POST',
|
||||||
throw new Error('Failed to fetch full email content');
|
headers: { 'Content-Type': 'application/json' },
|
||||||
}
|
body: JSON.stringify({ emailId })
|
||||||
|
}).catch(error => {
|
||||||
const fullEmail = await response.json();
|
console.error('Error marking email as read:', error);
|
||||||
|
});
|
||||||
// Update the email in the list and selected email with full content
|
|
||||||
setEmails(prevEmails => prevEmails.map(email =>
|
|
||||||
email.id === emailId
|
|
||||||
? { ...email, body: fullEmail.body }
|
|
||||||
: email
|
|
||||||
));
|
|
||||||
|
|
||||||
setSelectedEmail(prev => prev ? { ...prev, body: fullEmail.body } : prev);
|
|
||||||
|
|
||||||
// Try to mark as read in the background
|
|
||||||
try {
|
|
||||||
const markReadResponse = await fetch(`/api/mail/mark-read`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${session?.user?.id}` // Add session token
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
emailId,
|
|
||||||
isRead: true,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (markReadResponse.ok) {
|
|
||||||
// Only update the emails list if the API call was successful
|
|
||||||
setEmails((prevEmails: Email[]) =>
|
|
||||||
prevEmails.map((email: Email): Email =>
|
|
||||||
email.id === emailId
|
|
||||||
? { ...email, read: true }
|
|
||||||
: email
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.error('Failed to mark email as read:', await markReadResponse.text());
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Error marking email as read:', error);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1050,20 +850,20 @@ export default function CourrierPage() {
|
|||||||
// Update the email count in the header to show filtered count
|
// Update the email count in the header to show filtered count
|
||||||
const renderEmailListHeader = () => (
|
const renderEmailListHeader = () => (
|
||||||
<div className="border-b border-gray-100">
|
<div className="border-b border-gray-100">
|
||||||
<div className="px-4 py-1">
|
<div className="px-4 py-2">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute left-2 top-2 h-4 w-4 text-gray-400" />
|
<Search className="absolute left-2 top-2.5 h-4 w-4 text-gray-400" />
|
||||||
<Input
|
<Input
|
||||||
type="search"
|
type="search"
|
||||||
placeholder="Search in folder..."
|
placeholder="Search in folder..."
|
||||||
className="pl-8 h-8 bg-gray-50"
|
className="pl-8 h-9 bg-gray-50"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between px-4 h-10">
|
<div className="flex items-center justify-between px-4 h-14">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-3">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={filteredEmails.length > 0 && selectedEmails.length === filteredEmails.length}
|
checked={filteredEmails.length > 0 && selectedEmails.length === filteredEmails.length}
|
||||||
onCheckedChange={toggleSelectAll}
|
onCheckedChange={toggleSelectAll}
|
||||||
@ -1228,7 +1028,39 @@ export default function CourrierPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="prose max-w-none">
|
<div className="prose max-w-none">
|
||||||
{renderEmailContent(selectedEmail)}
|
{(() => {
|
||||||
|
try {
|
||||||
|
const parsed = parseFullEmail(selectedEmail.body);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Display HTML content if available, otherwise fallback to text */}
|
||||||
|
<div dangerouslySetInnerHTML={{
|
||||||
|
__html: parsed.html || parsed.text || decodeMimeContent(selectedEmail.body)
|
||||||
|
}} />
|
||||||
|
|
||||||
|
{/* Display attachments if present */}
|
||||||
|
{parsed.attachments && parsed.attachments.length > 0 && (
|
||||||
|
<div className="mt-6 border-t border-gray-200 pt-6">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-900 mb-4">Attachments</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
{parsed.attachments.map((attachment, index) => (
|
||||||
|
<div key={index} className="flex items-center space-x-2 p-2 border rounded">
|
||||||
|
<Paperclip className="h-4 w-4 text-gray-400" />
|
||||||
|
<span className="text-sm text-gray-600 truncate">
|
||||||
|
{attachment.filename}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error parsing email:', e);
|
||||||
|
return selectedEmail.body;
|
||||||
|
}
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</>
|
</>
|
||||||
@ -1261,142 +1093,111 @@ export default function CourrierPage() {
|
|||||||
}, [availableFolders]);
|
}, [availableFolders]);
|
||||||
|
|
||||||
// Update the email list item to match header checkbox alignment
|
// Update the email list item to match header checkbox alignment
|
||||||
const renderEmailListItem = (email: Email) => {
|
const renderEmailListItem = (email: Email) => (
|
||||||
console.log('=== Email List Item Debug ===');
|
<div
|
||||||
console.log('Email ID:', email.id);
|
key={email.id}
|
||||||
console.log('Subject:', email.subject);
|
className={`flex items-center gap-3 px-4 py-2 hover:bg-gray-50/80 cursor-pointer ${
|
||||||
console.log('Body length:', email.body.length);
|
selectedEmail?.id === email.id ? 'bg-blue-50/50' : ''
|
||||||
console.log('First 100 chars of body:', email.body.substring(0, 100));
|
} ${!email.read ? 'bg-blue-50/20' : ''}`}
|
||||||
|
onClick={() => handleEmailSelect(email.id)}
|
||||||
const preview = generateEmailPreview(email);
|
>
|
||||||
console.log('Generated preview:', preview);
|
<Checkbox
|
||||||
|
checked={selectedEmails.includes(email.id.toString())}
|
||||||
return (
|
onCheckedChange={(checked) => {
|
||||||
<div
|
const e = { target: { checked }, stopPropagation: () => {} } as React.ChangeEvent<HTMLInputElement>;
|
||||||
key={email.id}
|
handleEmailCheckbox(e, email.id);
|
||||||
className={`flex items-center gap-3 px-4 py-2 hover:bg-gray-50/80 cursor-pointer ${
|
}}
|
||||||
selectedEmail?.id === email.id ? 'bg-blue-50/50' : ''
|
onClick={(e) => e.stopPropagation()}
|
||||||
} ${!email.read ? 'bg-blue-50/20' : ''}`}
|
className="mt-0.5"
|
||||||
onClick={() => handleEmailSelect(email.id)}
|
/>
|
||||||
>
|
<div className="flex-1 min-w-0">
|
||||||
<Checkbox
|
<div className="flex items-center justify-between gap-2">
|
||||||
checked={selectedEmails.includes(email.id.toString())}
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
onCheckedChange={(checked) => {
|
<span className={`text-sm truncate ${!email.read ? 'font-semibold text-gray-900' : 'text-gray-600'}`}>
|
||||||
const e = { target: { checked }, stopPropagation: () => {} } as React.ChangeEvent<HTMLInputElement>;
|
{currentView === 'Sent' ? email.to : (
|
||||||
handleEmailCheckbox(e, email.id);
|
(() => {
|
||||||
}}
|
const fromMatch = email.from.match(/^([^<]+)\s*<([^>]+)>$/);
|
||||||
onClick={(e) => e.stopPropagation()}
|
return fromMatch ? fromMatch[1].trim() : email.from;
|
||||||
className="mt-0.5"
|
})()
|
||||||
/>
|
)}
|
||||||
<div className="flex-1 min-w-0">
|
</span>
|
||||||
<div className="flex items-center justify-between gap-2">
|
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
|
||||||
<span className={`text-sm truncate ${!email.read ? 'font-semibold text-gray-900' : 'text-gray-600'}`}>
|
|
||||||
{currentView === 'Sent' ? email.to : (
|
|
||||||
(() => {
|
|
||||||
const fromMatch = email.from.match(/^([^<]+)\s*<([^>]+)>$/);
|
|
||||||
return fromMatch ? fromMatch[1].trim() : email.from;
|
|
||||||
})()
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 flex-shrink-0">
|
|
||||||
<span className="text-xs text-gray-500 whitespace-nowrap">
|
|
||||||
{formatDate(email.date)}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-6 w-6 text-gray-400 hover:text-yellow-400"
|
|
||||||
onClick={(e) => toggleStarred(email.id, e)}
|
|
||||||
>
|
|
||||||
<Star className={`h-4 w-4 ${email.starred ? 'fill-yellow-400 text-yellow-400' : ''}`} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-sm text-gray-900 truncate">
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
{email.subject || '(No subject)'}
|
<span className="text-xs text-gray-500 whitespace-nowrap">
|
||||||
</h3>
|
{formatDate(email.date)}
|
||||||
<div className="text-xs text-gray-500 truncate">
|
</span>
|
||||||
{preview}
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6 text-gray-400 hover:text-yellow-400"
|
||||||
|
onClick={(e) => toggleStarred(email.id, e)}
|
||||||
|
>
|
||||||
|
<Star className={`h-4 w-4 ${email.starred ? 'fill-yellow-400 text-yellow-400' : ''}`} />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<h3 className="text-sm text-gray-900 truncate">
|
||||||
|
{email.subject || '(No subject)'}
|
||||||
|
</h3>
|
||||||
|
<div className="text-xs text-gray-500 truncate">
|
||||||
|
{(() => {
|
||||||
|
// Get clean preview of the actual message content
|
||||||
|
let preview = '';
|
||||||
|
try {
|
||||||
|
const parsed = parseFullEmail(email.body);
|
||||||
|
|
||||||
|
// Try to get content from parsed email
|
||||||
|
preview = (parsed.text || parsed.html || '')
|
||||||
|
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
||||||
|
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
||||||
|
.replace(/<[^>]+>/g, '')
|
||||||
|
.replace(/ |‌|»|«|>/g, ' ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
// If no preview from parsed content, try direct body
|
||||||
|
if (!preview) {
|
||||||
|
preview = email.body
|
||||||
|
.replace(/<[^>]+>/g, '')
|
||||||
|
.replace(/ |‌|»|«|>/g, ' ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove email artifacts and clean up
|
||||||
|
preview = preview
|
||||||
|
.replace(/^>+/gm, '')
|
||||||
|
.replace(/Content-Type:[^\n]+/g, '')
|
||||||
|
.replace(/Content-Transfer-Encoding:[^\n]+/g, '')
|
||||||
|
.replace(/--[a-zA-Z0-9]+(-[a-zA-Z0-9]+)?/g, '')
|
||||||
|
.replace(/boundary=[^\n]+/g, '')
|
||||||
|
.replace(/charset=[^\n]+/g, '')
|
||||||
|
.replace(/[\r\n]+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
// Take first 100 characters
|
||||||
|
preview = preview.substring(0, 100);
|
||||||
|
|
||||||
|
// Try to end at a complete word
|
||||||
|
if (preview.length === 100) {
|
||||||
|
const lastSpace = preview.lastIndexOf(' ');
|
||||||
|
if (lastSpace > 80) {
|
||||||
|
preview = preview.substring(0, lastSpace);
|
||||||
|
}
|
||||||
|
preview += '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error generating preview:', e);
|
||||||
|
preview = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return preview || 'No preview available';
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
};
|
);
|
||||||
|
|
||||||
const generateEmailPreview = (email: Email): string => {
|
|
||||||
console.log('=== generateEmailPreview Debug ===');
|
|
||||||
console.log('Email ID:', email.id);
|
|
||||||
console.log('Subject:', email.subject);
|
|
||||||
console.log('Body length:', email.body.length);
|
|
||||||
console.log('First 200 chars of body:', email.body.substring(0, 200));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parsed = parseFullEmail(email.body);
|
|
||||||
console.log('Parsed content:', {
|
|
||||||
hasText: !!parsed.text,
|
|
||||||
hasHtml: !!parsed.html,
|
|
||||||
textPreview: parsed.text?.substring(0, 100) || 'No text',
|
|
||||||
htmlPreview: parsed.html?.substring(0, 100) || 'No HTML'
|
|
||||||
});
|
|
||||||
|
|
||||||
let preview = '';
|
|
||||||
if (parsed.text) {
|
|
||||||
preview = parsed.text;
|
|
||||||
console.log('Using text content for preview');
|
|
||||||
} else if (parsed.html) {
|
|
||||||
preview = parsed.html
|
|
||||||
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
||||||
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
||||||
.replace(/<[^>]+>/g, ' ')
|
|
||||||
.replace(/\s+/g, ' ')
|
|
||||||
.trim();
|
|
||||||
console.log('Using HTML content for preview');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!preview) {
|
|
||||||
console.log('No preview from parsed content, using raw body');
|
|
||||||
preview = email.body
|
|
||||||
.replace(/<[^>]+>/g, ' ')
|
|
||||||
.replace(/ |‌|»|«|>/g, ' ')
|
|
||||||
.replace(/\s+/g, ' ')
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Final preview before cleaning:', preview.substring(0, 100) + '...');
|
|
||||||
|
|
||||||
// Clean up the preview
|
|
||||||
preview = preview
|
|
||||||
.replace(/^>+/gm, '')
|
|
||||||
.replace(/Content-Type:[^\n]+/g, '')
|
|
||||||
.replace(/Content-Transfer-Encoding:[^\n]+/g, '')
|
|
||||||
.replace(/--[a-zA-Z0-9]+(-[a-zA-Z0-9]+)?/g, '')
|
|
||||||
.replace(/boundary=[^\n]+/g, '')
|
|
||||||
.replace(/charset=[^\n]+/g, '')
|
|
||||||
.replace(/[\r\n]+/g, ' ')
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
// Take first 100 characters
|
|
||||||
preview = preview.substring(0, 100);
|
|
||||||
|
|
||||||
// Try to end at a complete word
|
|
||||||
if (preview.length === 100) {
|
|
||||||
const lastSpace = preview.lastIndexOf(' ');
|
|
||||||
if (lastSpace > 80) {
|
|
||||||
preview = preview.substring(0, lastSpace);
|
|
||||||
}
|
|
||||||
preview += '...';
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Final preview:', preview);
|
|
||||||
return preview;
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error generating preview:', e);
|
|
||||||
return 'No preview available';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Render the sidebar navigation
|
// Render the sidebar navigation
|
||||||
const renderSidebarNav = () => (
|
const renderSidebarNav = () => (
|
||||||
@ -2075,4 +1876,4 @@ export default function CourrierPage() {
|
|||||||
{renderDeleteConfirmDialog()}
|
{renderDeleteConfirmDialog()}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -90,4 +90,4 @@ function extractTextFromHtml(html: string): string {
|
|||||||
|
|
||||||
// Clean up whitespace
|
// Clean up whitespace
|
||||||
return html.replace(/\n\s*\n/g, '\n\n').trim();
|
return html.replace(/\n\s*\n/g, '\n\n').trim();
|
||||||
}
|
}
|
||||||
|
|||||||
42
lib/imap.ts
42
lib/imap.ts
@ -1,36 +1,26 @@
|
|||||||
import { ImapFlow } from 'imapflow';
|
import { ImapFlow } from 'imapflow';
|
||||||
import { getServerSession } from 'next-auth';
|
import { getServerSession } from 'next-auth';
|
||||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
||||||
import { prisma } from '@/lib/prisma';
|
|
||||||
|
let client: ImapFlow | null = null;
|
||||||
|
|
||||||
export async function getImapClient() {
|
export async function getImapClient() {
|
||||||
|
if (client) return client;
|
||||||
|
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.email) {
|
||||||
throw new Error('No authenticated user');
|
throw new Error('No authenticated user');
|
||||||
}
|
}
|
||||||
|
|
||||||
const credentials = await prisma.mailCredentials.findUnique({
|
client = new ImapFlow({
|
||||||
where: {
|
host: process.env.IMAP_HOST || 'imap.gmail.com',
|
||||||
userId: session.user.id
|
port: parseInt(process.env.IMAP_PORT || '993', 10),
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!credentials) {
|
|
||||||
throw new Error('No mail credentials found. Please configure your email account.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = new ImapFlow({
|
|
||||||
host: credentials.host,
|
|
||||||
port: credentials.port,
|
|
||||||
secure: true,
|
secure: true,
|
||||||
auth: {
|
auth: {
|
||||||
user: credentials.email,
|
user: session.user.email,
|
||||||
pass: credentials.password,
|
pass: session.user.accessToken
|
||||||
},
|
},
|
||||||
logger: false,
|
logger: false
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: false
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await client.connect();
|
await client.connect();
|
||||||
@ -44,7 +34,7 @@ export async function moveEmails(emailIds: number[], targetFolder: string) {
|
|||||||
for (const id of emailIds) {
|
for (const id of emailIds) {
|
||||||
const message = await imap.fetchOne(id.toString(), { uid: true });
|
const message = await imap.fetchOne(id.toString(), { uid: true });
|
||||||
if (message) {
|
if (message) {
|
||||||
await imap.messageMove(message.uid.toString(), targetFolder);
|
await imap.messageMove(message.uid, targetFolder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@ -59,14 +49,10 @@ export async function markAsRead(emailIds: number[], isRead: boolean) {
|
|||||||
for (const id of emailIds) {
|
for (const id of emailIds) {
|
||||||
const message = await imap.fetchOne(id.toString(), { uid: true });
|
const message = await imap.fetchOne(id.toString(), { uid: true });
|
||||||
if (message) {
|
if (message) {
|
||||||
if (isRead) {
|
await imap.messageFlagsAdd(message.uid, isRead ? ['\\Seen'] : [], { uid: true });
|
||||||
await imap.messageFlagsAdd(message.uid.toString(), ['\\Seen'], { uid: true });
|
|
||||||
} else {
|
|
||||||
await imap.messageFlagsRemove(message.uid.toString(), ['\\Seen'], { uid: true });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
lock.release();
|
lock.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user