feat: Enhance backend with request/SQL logging via morgan/winston and fix frontend by correctly rendering email Markdown content using the 'marked' library.
This commit is contained in:
140
backend/app.js
140
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}`);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
4
backend/eventEmitter.js
Normal file
4
backend/eventEmitter.js
Normal file
@@ -0,0 +1,4 @@
|
||||
const EventEmitter = require('events');
|
||||
class MyEmitter extends EventEmitter {}
|
||||
const emitter = new MyEmitter();
|
||||
module.exports = emitter;
|
||||
29
backend/logger.js
Normal file
29
backend/logger.js
Normal file
@@ -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;
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user