diff --git a/.gitignore b/.gitignore index ebeea4d..2f57761 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ /frontend/node_modules /backend/package-lock.json /frontend/package-lock.json -/frontend/dist \ No newline at end of file +/frontend/dist +plan.md \ No newline at end of file diff --git a/backend/app.js b/backend/app.js index 5a2d392..1442863 100644 --- a/backend/app.js +++ b/backend/app.js @@ -1,13 +1,21 @@ const express = require('express'); const cors = require('cors'); +const http = require('http'); +const { WebSocketServer } = require('ws'); const db = require('./db'); const { SMTPServer } = require('smtp-server'); const { saveEmail } = require('./saveEmail'); +const emitter = require('./eventEmitter'); +const logger = require('./logger'); +const morgan = require('morgan'); const app = express(); const apiPort = 5182; const smtpPort = 25; +// Setup morgan to log HTTP requests to winston +app.use(morgan('combined', { stream: { write: message => logger.info(message.trim()) } })); + app.use(cors()); app.use(express.json()); @@ -15,24 +23,136 @@ app.use(express.json()); app.get('/api/messages', async (req, res) => { const { recipient } = req.query; if (!recipient) { + logger.warn('Attempted to get messages without a recipient'); return res.status(400).send('Recipient is required'); } try { const [rows] = await db.execute( - 'SELECT id, sender, recipient, subject, body, received_at FROM emails WHERE recipient = ? ORDER BY received_at DESC', - [recipient] + 'SELECT id, sender, recipient, subject, body, received_at FROM emails WHERE recipient LIKE ? ORDER BY received_at DESC', + [`%${recipient}%`] ); res.json(rows); } catch (error) { - console.error('Failed to fetch emails:', error); + logger.error('Failed to fetch emails:', error); res.status(500).send('Failed to fetch emails'); } }); +// API to get attachments for a message +app.get('/api/messages/:id/attachments', async (req, res) => { + const { id } = req.params; + try { + const [rows] = await db.execute( + 'SELECT id, filename, content_type FROM email_attachments WHERE email_id = ?', + [id] + ); + res.json(rows); + } catch (error) { + logger.error('Failed to fetch attachments:', { error, emailId: id }); + res.status(500).send('Failed to fetch attachments'); + } +}); + +// API to download an attachment +app.get('/api/attachments/:attachmentId', async (req, res) => { + const { attachmentId } = req.params; + try { + const [rows] = await db.execute( + 'SELECT filename, content_type, content FROM email_attachments WHERE id = ?', + [attachmentId] + ); + if (rows.length === 0) { + return res.status(404).send('Attachment not found'); + } + const attachment = rows[0]; + res.setHeader('Content-Disposition', `attachment; filename="${attachment.filename}"`); + res.setHeader('Content-Type', attachment.content_type); + res.send(attachment.content); + } catch (error) { + logger.error('Failed to download attachment:', { error, attachmentId }); + res.status(500).send('Failed to download attachment'); + } +}); + +// API to delete a message +app.delete('/api/messages/:id', async (req, res) => { + const { id } = req.params; + try { + const [result] = await db.execute('DELETE FROM emails WHERE id = ?', [id]); + if (result.affectedRows === 0) { + logger.warn('Attempted to delete a message that was not found', { id }); + return res.status(404).send('Message not found'); + } + logger.info('Message deleted successfully', { id }); + res.status(204).send(); // No Content + } catch (error) { + logger.error('Failed to delete message:', { error, id }); + res.status(500).send('Failed to delete message'); + } +}); + +// API to delete all messages for a recipient +app.delete('/api/messages', async (req, res) => { + const { recipient } = req.query; + if (!recipient) { + logger.warn('Attempted to delete all messages without a recipient'); + return res.status(400).send('Recipient is required'); + } + try { + await db.execute('DELETE FROM emails WHERE recipient = ?', [recipient]); + logger.info('All messages for recipient deleted successfully', { recipient }); + res.status(204).send(); // No Content + } catch (error) { + logger.error('Failed to delete all messages:', { error, recipient }); + res.status(500).send('Failed to delete all messages'); + } +}); + +// Create HTTP server +const server = http.createServer(app); + +// Create WebSocket server +const wss = new WebSocketServer({ server }); + +// Map to store connections for each recipient +const clients = new Map(); + +wss.on('connection', (ws, req) => { + const url = new URL(req.url, `http://${req.headers.host}`); + const recipient = url.searchParams.get('recipient'); + + if (recipient) { + logger.info(`WebSocket client connected`, { recipient }); + clients.set(recipient, ws); + + ws.on('close', () => { + logger.info(`WebSocket client disconnected`, { recipient }); + clients.delete(recipient); + }); + + ws.on('error', (error) => { + logger.error(`WebSocket error`, { recipient, error }); + clients.delete(recipient); + }); + } else { + logger.warn('WebSocket client connected without recipient, closing.'); + ws.close(); + } +}); + +emitter.on('newEmail', (email) => { + const recipient = email.recipient; + if (clients.has(recipient)) { + const ws = clients.get(recipient); + logger.info(`Sending new email notification to`, { recipient }); + ws.send(JSON.stringify(email)); + } +}); + // Start API server -app.listen(apiPort, () => { - console.log(`Backend API server listening at http://localhost:${apiPort}`); +server.listen(apiPort, () => { + logger.info(`Backend API and WebSocket server listening at http://localhost:${apiPort}`); }); // Configure and start SMTP server @@ -40,23 +160,23 @@ const smtpServer = new SMTPServer({ authOptional: true, disabledCommands: ['AUTH'], onData(stream, session, callback) { - console.log('Receiving email...'); + logger.info('Receiving email...', { session }); saveEmail(stream) .then(() => { - console.log('Email processed and saved successfully.'); + logger.info('Email processed and saved successfully.'); callback(); // Accept the message }) .catch(err => { - console.error('Error processing email:', err); + logger.error('Error processing email:', err); callback(new Error('Failed to process email.')); }); }, }); smtpServer.on('error', err => { - console.error('SMTP Server Error:', err.message); + logger.error('SMTP Server Error:', err); }); smtpServer.listen(smtpPort, () => { - console.log(`SMTP server listening on port ${smtpPort}`); + logger.info(`SMTP server listening on port ${smtpPort}`); }); diff --git a/backend/db.js b/backend/db.js index dec7378..708a840 100644 --- a/backend/db.js +++ b/backend/db.js @@ -1,4 +1,5 @@ const mysql = require('mysql2'); +const logger = require('./logger'); const pool = mysql.createPool({ host: process.env.DB_HOST, @@ -10,4 +11,18 @@ const pool = mysql.createPool({ queueLimit: 0 }); -module.exports = pool.promise(); +const promisePool = pool.promise(); + +const originalExecute = promisePool.execute; +promisePool.execute = function(sql, params) { + logger.info('Executing SQL', { sql, params }); + return originalExecute.call(this, sql, params); +}; + +const originalQuery = promisePool.query; +promisePool.query = function(sql, params) { + logger.info('Executing SQL', { sql, params }); + return originalQuery.call(this, sql, params); +}; + +module.exports = promisePool; diff --git a/backend/eventEmitter.js b/backend/eventEmitter.js new file mode 100644 index 0000000..b6734e5 --- /dev/null +++ b/backend/eventEmitter.js @@ -0,0 +1,4 @@ +const EventEmitter = require('events'); +class MyEmitter extends EventEmitter {} +const emitter = new MyEmitter(); +module.exports = emitter; diff --git a/backend/logger.js b/backend/logger.js new file mode 100644 index 0000000..03f6241 --- /dev/null +++ b/backend/logger.js @@ -0,0 +1,29 @@ +const winston = require('winston'); + +const logger = winston.createLogger({ + level: 'info', + format: winston.format.combine( + winston.format.timestamp({ + format: 'YYYY-MM-DD HH:mm:ss' + }), + winston.format.errors({ stack: true }), + winston.format.splat(), + winston.format.json() + ), + defaultMeta: { service: 'user-service' }, + transports: [ + new winston.transports.File({ filename: 'error.log', level: 'error' }), + new winston.transports.File({ filename: 'combined.log' }) + ] +}); + +if (process.env.NODE_ENV !== 'production') { + logger.add(new winston.transports.Console({ + format: winston.format.combine( + winston.format.colorize(), + winston.format.simple() + ) + })); +} + +module.exports = logger; diff --git a/backend/package.json b/backend/package.json index 3f23002..4d3a53a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -11,6 +11,9 @@ "mysql2": "^3.14.2", "mailparser": "^3.7.4", "cors": "^2.8.5", - "smtp-server": "^3.13.4" + "smtp-server": "^3.13.4", + "ws": "^8.17.1", + "winston": "^3.13.0", + "morgan": "^1.10.0" } } diff --git a/backend/saveEmail.js b/backend/saveEmail.js index 0d63f49..382e5d5 100644 --- a/backend/saveEmail.js +++ b/backend/saveEmail.js @@ -1,5 +1,6 @@ const { simpleParser } = require('mailparser'); const db = require('./db'); +const emitter = require('./eventEmitter'); // Helper function to convert stream to buffer function streamToBuffer(stream) { @@ -29,18 +30,26 @@ async function saveEmail(stream) { 'INSERT INTO emails (recipient, sender, subject, body, raw) VALUES (?, ?, ?, ?, ?)', [recipient, sender, subject, body, rawEmail] ); + const newEmailId = result.insertId; - console.log(`Email from <${sender}> to <${recipient}> saved with ID: ${result.insertId}`); + console.log(`Email from <${sender}> to <${recipient}> saved with ID: ${newEmailId}`); if (parsed.attachments && parsed.attachments.length > 0) { for (const attachment of parsed.attachments) { await db.execute( 'INSERT INTO email_attachments (email_id, filename, content_type, content) VALUES (?, ?, ?, ?)', - [result.insertId, attachment.filename, attachment.contentType, attachment.content] + [newEmailId, attachment.filename, attachment.contentType, attachment.content] ); console.log(`Attachment ${attachment.filename} saved.`); } } + + // Emit an event with the new email's main information + const [rows] = await db.execute('SELECT id, sender, recipient, subject, body, received_at FROM emails WHERE id = ?', [newEmailId]); + if (rows.length > 0) { + emitter.emit('newEmail', rows[0]); + } + } catch (error) { console.error('Failed to save email:', error); // We should not exit the process here, but maybe throw the error diff --git a/docker-compose.yml b/docker-compose.yml index 7e4b00c..94bc1cd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.3' - services: backend: build: ./backend diff --git a/frontend/public/index.html b/frontend/index.html similarity index 67% rename from frontend/public/index.html rename to frontend/index.html index a801360..0588132 100644 --- a/frontend/public/index.html +++ b/frontend/index.html @@ -4,11 +4,11 @@ - - + + - +