const path = require('path'); const fs = require('fs').promises; const simpleParser = require('mailparser').simpleParser; const { models } = require(path.resolve(process.env.ROOT_PATH, './db/db.js')); const BaseController = require(path.resolve(process.env.ROOT_PATH, './controllers/BaseController.js')); class MessageController extends BaseController { constructor() { super(); // Protected endpoints this.protected('getMessages', 'getMessage', 'deleteMessage'); } /** * Get all messages for a specific mailbox * * @swagger * /mailboxes/{mailboxId}/messages: * get: * summary: Get all messages * description: Retrieves all messages for a specific mailbox * tags: [Messages] * security: * - bearerAuth: [] * parameters: * - in: path * name: mailboxId * required: true * schema: * type: integer * description: Mailbox ID * responses: * 200: * description: List of messages * content: * application/json: * schema: * type: array * items: * type: object * properties: * id: * type: string * description: Message ID * subject: * type: string * description: Email subject * from: * type: string * description: Sender information * date: * type: string * format: date-time * description: Message date * size: * type: integer * description: Message size in bytes * hasAttachments: * type: boolean * description: Whether the message has attachments * 404: * description: Mailbox not found * 500: * description: Server error */ async getMessages(req, res) { try { const { mailboxId } = req.params; // Verify mailbox exists const mailbox = await models.Mailbox.query().findById(mailboxId); if (!mailbox) { return res.status(404).json({ error: 'Mailbox not found' }); } // Get the physical path to the mailbox const mailboxPath = path.join('/var/mail', mailbox.domain, mailbox.local_part, 'new'); try { // Check if directory exists await fs.access(mailboxPath); // Read all files in the directory const files = await fs.readdir(mailboxPath); // Process each file to extract basic email info const messages = await Promise.all( files.map(async (file) => { const filePath = path.join(mailboxPath, file); const stats = await fs.stat(filePath); try { // Read file content const content = await fs.readFile(filePath, { encoding: 'utf-8' }); // Parse email content const parsed = await simpleParser(content); return { id: file, subject: parsed.subject || '(No Subject)', from: parsed.from?.text || '(Unknown Sender)', date: parsed.date || stats.mtime, size: stats.size, hasAttachments: parsed.attachments?.length > 0 || false }; } catch (err) { console.error(`Error parsing email ${file}:`, err); return { id: file, subject: '(Error: Could not parse email)', from: '(Unknown)', date: stats.mtime, size: stats.size, hasAttachments: false, error: true }; } }) ); // Sort messages by date (newest first) messages.sort((a, b) => new Date(b.date) - new Date(a.date)); return res.json(messages); } catch (err) { // If directory doesn't exist or is empty if (err.code === 'ENOENT') { return res.json([]); } throw err; } } catch (error) { console.error('Error fetching messages:', error); return res.status(500).json({ error: 'Failed to fetch messages' }); } } /** * Get a specific message from a mailbox * * @swagger * /mailboxes/{mailboxId}/messages/{messageId}: * get: * summary: Get message details * description: Retrieves a specific message from a mailbox * tags: [Messages] * security: * - bearerAuth: [] * parameters: * - in: path * name: mailboxId * required: true * schema: * type: integer * description: Mailbox ID * - in: path * name: messageId * required: true * schema: * type: string * description: Message ID * responses: * 200: * description: Message details * content: * application/json: * schema: * type: object * properties: * id: * type: string * description: Message ID * subject: * type: string * description: Email subject * from: * type: string * description: Sender information * to: * type: string * description: Recipient information * cc: * type: string * description: CC recipients * date: * type: string * format: date-time * description: Message date * textBody: * type: string * description: Plain text content * htmlBody: * type: string * description: HTML content * attachments: * type: array * description: List of attachments * items: * type: object * properties: * filename: * type: string * contentType: * type: string * size: * type: integer * contentId: * type: string * 404: * description: Mailbox or message not found * 500: * description: Server error */ async getMessage(req, res) { try { const { mailboxId, messageId } = req.params; // Verify mailbox exists const mailbox = await models.Mailbox.query().findById(mailboxId); if (!mailbox) { return res.status(404).json({ error: 'Mailbox not found' }); } // Check both new and cur directories for the message const directories = ['new', 'cur']; let messageContent = null; let messageFound = false; for (const dir of directories) { const messagePath = path.join('/var/mail', mailbox.domain, mailbox.local_part, dir, messageId); try { // Check if file exists await fs.access(messagePath); // Read file content const content = await fs.readFile(messagePath, { encoding: 'utf-8' }); // Parse email content messageContent = await simpleParser(content); messageFound = true; // Move message from 'new' to 'cur' if it's in 'new' if (dir === 'new') { const curDir = path.join('/var/mail', mailbox.domain, mailbox.local_part, 'cur'); // Ensure cur directory exists await fs.mkdir(curDir, { recursive: true }); // Move the file const curPath = path.join(curDir, `${messageId}:2,S`); // Mark as seen await fs.rename(messagePath, curPath); } break; } catch (err) { // Continue to next directory if file not found if (err.code === 'ENOENT') { continue; } throw err; } } if (!messageFound) { return res.status(404).json({ error: 'Message not found' }); } // Format the response const response = { id: messageId, subject: messageContent.subject || '(No Subject)', from: messageContent.from?.text || '(Unknown Sender)', to: messageContent.to?.text || '', cc: messageContent.cc?.text || '', date: messageContent.date, textBody: messageContent.text, htmlBody: messageContent.html, attachments: messageContent.attachments?.map(attachment => ({ filename: attachment.filename, contentType: attachment.contentType, size: attachment.size, contentId: attachment.contentId })) || [] }; return res.json(response); } catch (error) { console.error('Error fetching message:', error); return res.status(500).json({ error: 'Failed to fetch message' }); } } /** * Delete a specific message from a mailbox * * @swagger * /mailboxes/{mailboxId}/messages/{messageId}: * delete: * summary: Delete message * description: Deletes a specific message from a mailbox * tags: [Messages] * security: * - bearerAuth: [] * parameters: * - in: path * name: mailboxId * required: true * schema: * type: integer * description: Mailbox ID * - in: path * name: messageId * required: true * schema: * type: string * description: Message ID * responses: * 200: * description: Message deleted successfully * content: * application/json: * schema: * type: object * properties: * message: * type: string * example: Message deleted successfully * 404: * description: Mailbox or message not found * 500: * description: Server error */ async deleteMessage(req, res) { try { const { mailboxId, messageId } = req.params; // Verify mailbox exists const mailbox = await models.Mailbox.query().findById(mailboxId); if (!mailbox) { return res.status(404).json({ error: 'Mailbox not found' }); } // Check both new and cur directories for the message const directories = ['new', 'cur']; let messageFound = false; for (const dir of directories) { // In 'cur' directory, the message might have flags appended const dirPath = path.join('/var/mail', mailbox.domain, mailbox.local_part, dir); try { // Check if directory exists await fs.access(dirPath); // Read all files in the directory const files = await fs.readdir(dirPath); // Find files that start with the messageId for (const file of files) { if (file === messageId || file.startsWith(`${messageId}:`)) { const filePath = path.join(dirPath, file); await fs.unlink(filePath); messageFound = true; break; } } if (messageFound) break; } catch (err) { // Continue to next directory if directory not found if (err.code === 'ENOENT') { continue; } throw err; } } if (!messageFound) { return res.status(404).json({ error: 'Message not found' }); } return res.json({ message: 'Message deleted successfully' }); } catch (error) { console.error('Error deleting message:', error); return res.status(500).json({ error: 'Failed to delete message' }); } } /** * Get attachment from a message * * @swagger * /mailboxes/{mailboxId}/messages/{messageId}/attachments/{attachmentId}: * get: * summary: Get message attachment * description: Retrieves a specific attachment from a message * tags: [Messages] * security: * - bearerAuth: [] * parameters: * - in: path * name: mailboxId * required: true * schema: * type: integer * description: Mailbox ID * - in: path * name: messageId * required: true * schema: * type: string * description: Message ID * - in: path * name: attachmentId * required: true * schema: * type: string * description: Attachment ID or filename * responses: * 200: * description: Attachment file * content: * application/octet-stream: * schema: * type: string * format: binary * 404: * description: Mailbox, message, or attachment not found * 500: * description: Server error */ async getAttachment(req, res) { try { const { mailboxId, messageId, attachmentId } = req.params; // Verify mailbox exists const mailbox = await models.Mailbox.query().findById(mailboxId); if (!mailbox) { return res.status(404).json({ error: 'Mailbox not found' }); } // Check both new and cur directories for the message const directories = ['new', 'cur']; let messageContent = null; let messageFound = false; for (const dir of directories) { // In 'cur' directory, the message might have flags appended const dirPath = path.join('/var/mail', mailbox.domain, mailbox.local_part, dir); try { // Check if directory exists await fs.access(dirPath); // Read all files in the directory const files = await fs.readdir(dirPath); // Find files that start with the messageId for (const file of files) { if (file === messageId || file.startsWith(`${messageId}:`)) { const filePath = path.join(dirPath, file); // Read file content const content = await fs.readFile(filePath, { encoding: 'utf-8' }); // Parse email content messageContent = await simpleParser(content); messageFound = true; break; } } if (messageFound) break; } catch (err) { // Continue to next directory if directory not found if (err.code === 'ENOENT') { continue; } throw err; } } if (!messageFound) { return res.status(404).json({ error: 'Message not found' }); } // Find the attachment const attachment = messageContent.attachments?.find( att => att.contentId === attachmentId || att.filename === attachmentId ); if (!attachment) { return res.status(404).json({ error: 'Attachment not found' }); } // Set appropriate headers res.setHeader('Content-Type', attachment.contentType); res.setHeader('Content-Disposition', `attachment; filename="${attachment.filename}"`); // Send the attachment content return res.send(attachment.content); } catch (error) { console.error('Error fetching attachment:', error); return res.status(500).json({ error: 'Failed to fetch attachment' }); } } } module.exports = new MessageController();