Compare commits
6 Commits
limit-mail
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
f785e7defc | |
|
|
742aa4d2fc | |
|
|
9ab3621658 | |
|
|
ae870fb601 | |
|
|
77030cc8fc | |
|
|
bb25928a8e |
|
|
@ -1,5 +1,10 @@
|
||||||
FROM node:20-alpine
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
# Set timezone to Asia/Shanghai FIRST
|
||||||
|
# This ensures that any native modules compiled during npm install are linked correctly.
|
||||||
|
RUN apk add --no-cache tzdata
|
||||||
|
ENV TZ=Asia/Shanghai
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ app.get('/api/messages', async (req, res) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [rows] = await db.execute(
|
const [rows] = await db.execute(
|
||||||
'SELECT id, sender, recipient, subject, body, received_at FROM emails WHERE recipient LIKE ? ORDER BY received_at DESC',
|
'SELECT id, sender, recipient, subject, body, CAST(received_at AS CHAR) as received_at FROM emails WHERE recipient LIKE ? ORDER BY received_at DESC',
|
||||||
[`%${recipient}%`]
|
[`%${recipient}%`]
|
||||||
);
|
);
|
||||||
res.json(rows);
|
res.json(rows);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS emails (
|
||||||
sender VARCHAR(255) NOT NULL,
|
sender VARCHAR(255) NOT NULL,
|
||||||
subject VARCHAR(512),
|
subject VARCHAR(512),
|
||||||
body TEXT,
|
body TEXT,
|
||||||
received_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
received_at DATETIME,
|
||||||
raw MEDIUMBLOB
|
raw MEDIUMBLOB
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ const logger = winston.createLogger({
|
||||||
level: 'info',
|
level: 'info',
|
||||||
format: winston.format.combine(
|
format: winston.format.combine(
|
||||||
winston.format.timestamp({
|
winston.format.timestamp({
|
||||||
format: 'YYYY-MM-DD HH:mm:ss'
|
format: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
tz: 'Asia/Shanghai'
|
||||||
}),
|
}),
|
||||||
winston.format.errors({ stack: true }),
|
winston.format.errors({ stack: true }),
|
||||||
winston.format.splat(),
|
winston.format.splat(),
|
||||||
|
|
@ -17,11 +18,23 @@ const logger = winston.createLogger({
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 在非生产环境下,添加一个带有着色的控制台输出
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
logger.add(new winston.transports.Console({
|
logger.add(new winston.transports.Console({
|
||||||
format: winston.format.combine(
|
format: winston.format.combine(
|
||||||
winston.format.colorize(),
|
winston.format.colorize(),
|
||||||
winston.format.simple()
|
// 确保控制台日志也有正确格式和时区的时间戳
|
||||||
|
winston.format.timestamp({
|
||||||
|
format: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
tz: 'Asia/Shanghai'
|
||||||
|
}),
|
||||||
|
winston.format.printf(({ level, message, timestamp, stack }) => {
|
||||||
|
if (stack) {
|
||||||
|
// 打印错误堆栈
|
||||||
|
return `${timestamp} ${level}: ${message}\n${stack}`;
|
||||||
|
}
|
||||||
|
return `${timestamp} ${level}: ${message}`;
|
||||||
|
})
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
const { simpleParser } = require('mailparser');
|
const { simpleParser } = require('mailparser');
|
||||||
const db = require('./db');
|
const db = require('./db');
|
||||||
const emitter = require('./eventEmitter');
|
const emitter = require('./eventEmitter');
|
||||||
|
const logger = require('./logger');
|
||||||
|
|
||||||
// Helper function to convert stream to buffer
|
// Helper function to convert stream to buffer
|
||||||
function streamToBuffer(stream) {
|
function streamToBuffer(stream) {
|
||||||
|
|
@ -19,20 +20,25 @@ async function saveEmail(stream) {
|
||||||
|
|
||||||
// Now, parse the buffered email content
|
// Now, parse the buffered email content
|
||||||
const parsed = await simpleParser(emailBuffer);
|
const parsed = await simpleParser(emailBuffer);
|
||||||
const rawEmail = emailBuffer.toString();
|
// const rawEmail = emailBuffer.toString(); // We are not saving the raw email content for now.
|
||||||
|
|
||||||
const recipient = parsed.to ? parsed.to.text : 'undisclosed-recipients';
|
const recipient = parsed.to ? parsed.to.text : 'undisclosed-recipients';
|
||||||
const sender = parsed.from ? parsed.from.text : 'unknown-sender';
|
const sender = parsed.from ? parsed.from.text : 'unknown-sender';
|
||||||
const subject = parsed.subject || 'No Subject';
|
const subject = parsed.subject || 'No Subject';
|
||||||
const body = parsed.text || (parsed.html || '');
|
const body = parsed.text || (parsed.html || '');
|
||||||
|
|
||||||
|
// Manually create a timestamp for 'Asia/Shanghai' timezone
|
||||||
|
const received_at = new Date().toLocaleString('sv-SE', {
|
||||||
|
timeZone: 'Asia/Shanghai'
|
||||||
|
});
|
||||||
|
|
||||||
const [result] = await db.execute(
|
const [result] = await db.execute(
|
||||||
'INSERT INTO emails (recipient, sender, subject, body, raw) VALUES (?, ?, ?, ?, ?)',
|
'INSERT INTO emails (recipient, sender, subject, body, received_at) VALUES (?, ?, ?, ?, ?)',
|
||||||
[recipient, sender, subject, body, rawEmail]
|
[recipient, sender, subject, body, received_at]
|
||||||
);
|
);
|
||||||
const newEmailId = result.insertId;
|
const newEmailId = result.insertId;
|
||||||
|
|
||||||
console.log(`Email from <${sender}> to <${recipient}> saved with ID: ${newEmailId}`);
|
logger.info(`Email from <${sender}> to <${recipient}> saved with ID: ${newEmailId}`);
|
||||||
|
|
||||||
if (parsed.attachments && parsed.attachments.length > 0) {
|
if (parsed.attachments && parsed.attachments.length > 0) {
|
||||||
for (const attachment of parsed.attachments) {
|
for (const attachment of parsed.attachments) {
|
||||||
|
|
@ -40,7 +46,7 @@ async function saveEmail(stream) {
|
||||||
'INSERT INTO email_attachments (email_id, filename, content_type, content) VALUES (?, ?, ?, ?)',
|
'INSERT INTO email_attachments (email_id, filename, content_type, content) VALUES (?, ?, ?, ?)',
|
||||||
[newEmailId, attachment.filename, attachment.contentType, attachment.content]
|
[newEmailId, attachment.filename, attachment.contentType, attachment.content]
|
||||||
);
|
);
|
||||||
console.log(`Attachment ${attachment.filename} saved.`);
|
logger.info(`Attachment ${attachment.filename} saved for email ID: ${newEmailId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,15 +54,17 @@ async function saveEmail(stream) {
|
||||||
const [rows] = await db.execute('SELECT id, sender, recipient, subject, body, received_at FROM emails WHERE id = ?', [newEmailId]);
|
const [rows] = await db.execute('SELECT id, sender, recipient, subject, body, received_at FROM emails WHERE id = ?', [newEmailId]);
|
||||||
if (rows.length > 0) {
|
if (rows.length > 0) {
|
||||||
emitter.emit('newEmail', rows[0]);
|
emitter.emit('newEmail', rows[0]);
|
||||||
|
logger.info(`Event 'newEmail' emitted for email ID: ${newEmailId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save email:', error);
|
logger.error('Failed to save email:', {
|
||||||
// We should not exit the process here, but maybe throw the error
|
errorMessage: error.message,
|
||||||
// so the caller (SMTPServer) can handle it.
|
errorStack: error.stack
|
||||||
|
});
|
||||||
|
// Re-throw the error so the caller (SMTPServer) can handle it.
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { saveEmail };
|
module.exports = { saveEmail };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ services:
|
||||||
image: mysql:8.0
|
image: mysql:8.0
|
||||||
container_name: email-mysql
|
container_name: email-mysql
|
||||||
restart: always
|
restart: always
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
env_file:
|
env_file:
|
||||||
- compose.env
|
- compose.env
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ services:
|
||||||
image: registry.cn-hangzhou.aliyuncs.com/pull-image/mysql:8.0
|
image: registry.cn-hangzhou.aliyuncs.com/pull-image/mysql:8.0
|
||||||
container_name: email-mysql
|
container_name: email-mysql
|
||||||
restart: always
|
restart: always
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
env_file:
|
env_file:
|
||||||
- compose.full.env
|
- compose.full.env
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ services:
|
||||||
image: registry.cn-hangzhou.aliyuncs.com/pull-image/email-unlimit-backend:latest
|
image: registry.cn-hangzhou.aliyuncs.com/pull-image/email-unlimit-backend:latest
|
||||||
container_name: email-backend
|
container_name: email-backend
|
||||||
restart: always
|
restart: always
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
ports:
|
ports:
|
||||||
- "5182:5182" # API port
|
- "5182:5182" # API port
|
||||||
- "25:25" # SMTP port
|
- "25:25" # SMTP port
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ const messages = {
|
||||||
selectMail: '请从选择一封邮件查看',
|
selectMail: '请从选择一封邮件查看',
|
||||||
from: '发件人',
|
from: '发件人',
|
||||||
to: '收件人',
|
to: '收件人',
|
||||||
|
received_at: '收件时间',
|
||||||
attachments: '附件',
|
attachments: '附件',
|
||||||
delete: '删除',
|
delete: '删除',
|
||||||
clearInbox: '清空收件箱',
|
clearInbox: '清空收件箱',
|
||||||
|
|
@ -68,6 +69,7 @@ const messages = {
|
||||||
selectMail: 'Please select an email to view',
|
selectMail: 'Please select an email to view',
|
||||||
from: 'From',
|
from: 'From',
|
||||||
to: 'To',
|
to: 'To',
|
||||||
|
received_at: 'Received At',
|
||||||
attachments: 'Attachments',
|
attachments: 'Attachments',
|
||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
clearInbox: 'Clear Inbox',
|
clearInbox: 'Clear Inbox',
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@
|
||||||
<h3>{{ selectedMessage.subject }}</h3>
|
<h3>{{ selectedMessage.subject }}</h3>
|
||||||
<p class="from-line"><strong>{{ $t('home.from') }}:</strong> {{ selectedMessage.sender }}</p>
|
<p class="from-line"><strong>{{ $t('home.from') }}:</strong> {{ selectedMessage.sender }}</p>
|
||||||
<p><strong>{{ $t('home.to') }}:</strong> {{ selectedMessage.recipient }}</p>
|
<p><strong>{{ $t('home.to') }}:</strong> {{ selectedMessage.recipient }}</p>
|
||||||
|
<p><strong>{{ $t('home.received_at') }}:</strong> {{ selectedMessage.received_at }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<button @click="deleteMessage(selectedMessage.id)" :title="$t('home.delete')">
|
<button @click="deleteMessage(selectedMessage.id)" :title="$t('home.delete')">
|
||||||
|
|
|
||||||
BIN
sample.png
BIN
sample.png
Binary file not shown.
|
Before Width: | Height: | Size: 204 KiB After Width: | Height: | Size: 103 KiB |
Loading…
Reference in New Issue