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:
parent
c38a4f0f62
commit
8c649adf93
|
|
@ -2,4 +2,5 @@
|
|||
/frontend/node_modules
|
||||
/backend/package-lock.json
|
||||
/frontend/package-lock.json
|
||||
/frontend/dist
|
||||
/frontend/dist
|
||||
plan.md
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
const EventEmitter = require('events');
|
||||
class MyEmitter extends EventEmitter {}
|
||||
const emitter = new MyEmitter();
|
||||
module.exports = emitter;
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
version: '3.3'
|
||||
|
||||
services:
|
||||
backend:
|
||||
build: ./backend
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@
|
|||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>logo.svg" type="image/svg+xml">
|
||||
<link rel="manifest" href="<%= BASE_URL %>manifest.json">
|
||||
<link rel="icon" href="/logo.svg" type="image/svg+xml">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<link rel="apple-touch-icon" href="<%= BASE_URL %>img/icons/apple-touch-icon.png">
|
||||
<link rel="apple-touch-icon" href="/img/icons/apple-touch-icon.png">
|
||||
<title>临时邮件</title>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -16,6 +16,6 @@
|
|||
<strong>We're sorry but this app doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -3,19 +3,24 @@
|
|||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build"
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"axios": "^1.11.0",
|
||||
"element-plus": "^2.10.4",
|
||||
"pinia": "^3.0.3",
|
||||
"register-service-worker": "^1.7.2",
|
||||
"vue": "^3.5.18",
|
||||
"vue-i18n": "^11.1.11",
|
||||
"vue-router": "^4.5.1"
|
||||
"vue-router": "^4.5.1",
|
||||
"ws": "^8.18.3",
|
||||
"marked": "^13.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-pwa": "~5.0.0",
|
||||
"@vue/cli-service": "~5.0.8"
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"vite": "^5.3.1",
|
||||
"vite-plugin-pwa": "^0.20.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap');
|
||||
/* Global Styles & Resets */
|
||||
*,
|
||||
*::before,
|
||||
|
|
@ -5,7 +6,7 @@
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap');
|
||||
|
||||
|
||||
:root {
|
||||
--primary-purple: #6d28d9;
|
||||
|
|
@ -181,6 +182,15 @@ body {
|
|||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #ef4444; /* a nice red */
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #dc2626; /* a darker red */
|
||||
}
|
||||
|
||||
/* Inbox Section */
|
||||
.inbox-section {
|
||||
width: 100%;
|
||||
|
|
@ -219,12 +229,36 @@ body {
|
|||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.header-actions button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.5rem;
|
||||
color: var(--dark-grey);
|
||||
padding: 0.25rem;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.header-actions button:hover:not(:disabled) {
|
||||
background-color: var(--medium-grey);
|
||||
color: var(--primary-purple);
|
||||
}
|
||||
|
||||
.header-actions button:disabled {
|
||||
color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.header-actions .el-icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,20 @@ const messages = {
|
|||
selectMail: '请从选择一封邮件查看',
|
||||
from: '发件人',
|
||||
to: '收件人',
|
||||
attachments: '附件',
|
||||
delete: '删除',
|
||||
clearInbox: '清空收件箱',
|
||||
alerts: {
|
||||
enterEmail: '请输入一个邮箱地址',
|
||||
fetchFailed: '无法获取邮件,请检查后端服务和 Nginx 配置是否正常。',
|
||||
confirmDelete: '确定要删除这封邮件吗?',
|
||||
deleteFailed: '删除邮件失败。',
|
||||
confirmClearAll: '确定要清空这个收件箱的所有邮件吗?此操作不可恢复。',
|
||||
clearFailed: '清空收件箱失败。',
|
||||
copyFailed: '复制失败!',
|
||||
actionCancelled: '操作已取消',
|
||||
},
|
||||
newMailNotification: '收到新邮件!',
|
||||
}
|
||||
},
|
||||
en: {
|
||||
|
|
@ -49,6 +63,20 @@ const messages = {
|
|||
selectMail: 'Please select an email to view',
|
||||
from: 'From',
|
||||
to: 'To',
|
||||
attachments: 'Attachments',
|
||||
delete: 'Delete',
|
||||
clearInbox: 'Clear Inbox',
|
||||
alerts: {
|
||||
enterEmail: 'Please enter an email address',
|
||||
fetchFailed: 'Failed to fetch emails. Please check if the backend service and Nginx configuration are normal.',
|
||||
confirmDelete: 'Are you sure you want to delete this email?',
|
||||
deleteFailed: 'Failed to delete email.',
|
||||
confirmClearAll: 'Are you sure you want to clear all emails in this inbox? This action cannot be undone.',
|
||||
clearFailed: 'Failed to clear inbox.',
|
||||
copyFailed: 'Copy failed!',
|
||||
actionCancelled: 'Action cancelled',
|
||||
},
|
||||
newMailNotification: 'New mail received!',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { createApp, watch } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import './registerServiceWorker'
|
||||
import router from './router'
|
||||
import i18n from './i18n'
|
||||
import { useLanguageStore } from './stores/language'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import './assets/main.css'
|
||||
|
||||
const app = createApp(App)
|
||||
|
|
@ -13,6 +14,7 @@ const pinia = createPinia()
|
|||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(i18n)
|
||||
app.use(ElementPlus)
|
||||
|
||||
const languageStore = useLanguageStore(pinia)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
/* eslint-disable no-console */
|
||||
|
||||
import { register } from 'register-service-worker'
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
register(`${process.env.BASE_URL}service-worker.js`, {
|
||||
ready () {
|
||||
console.log(
|
||||
'App is being served from cache by a service worker.\n' +
|
||||
'For more details, visit https://goo.gl/AFskqB'
|
||||
)
|
||||
},
|
||||
registered () {
|
||||
console.log('Service worker has been registered.')
|
||||
},
|
||||
cached () {
|
||||
console.log('Content has been cached for offline use.')
|
||||
},
|
||||
updatefound () {
|
||||
console.log('New content is downloading.')
|
||||
},
|
||||
updated () {
|
||||
console.log('New content is available; please refresh.')
|
||||
},
|
||||
offline () {
|
||||
console.log('No internet connection found. App is running in offline mode.')
|
||||
},
|
||||
error (error) {
|
||||
console.error('Error during service worker registration:', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { md5 } from './md5.js';
|
||||
|
||||
function get_gravatar(email, size = 80, defaultImage = 'retro') {
|
||||
if (!email) {
|
||||
return `https://www.gravatar.com/avatar/?s=${size}&d=${defaultImage}`;
|
||||
}
|
||||
const emailHash = md5(email.trim().toLowerCase());
|
||||
return `https://www.gravatar.com/avatar/${emailHash}?s=${size}&d=${defaultImage}`;
|
||||
}
|
||||
|
||||
export { get_gravatar };
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
/*
|
||||
* A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
|
||||
* Digest Algorithm, as defined in RFC 1321.
|
||||
* Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
|
||||
* Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
|
||||
* Distributed under the BSD License
|
||||
* See http://pajhome.org.uk/crypt/md5 for more info.
|
||||
*/
|
||||
|
||||
function md5(string) {
|
||||
|
||||
function RotateLeft(lValue, iShiftBits) {
|
||||
return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits));
|
||||
}
|
||||
|
||||
function AddUnsigned(lX, lY) {
|
||||
var lX4, lY4, lX8, lY8, lResult;
|
||||
lX8 = (lX & 0x80000000);
|
||||
lY8 = (lY & 0x80000000);
|
||||
lX4 = (lX & 0x40000000);
|
||||
lY4 = (lY & 0x40000000);
|
||||
lResult = (lX & 0x3FFFFFFF) + (lY & 0x3FFFFFFF);
|
||||
if (lX4 & lY4) {
|
||||
return (lResult ^ 0x80000000 ^ lX8 ^ lY8);
|
||||
}
|
||||
if (lX4 | lY4) {
|
||||
if (lResult & 0x40000000) {
|
||||
return (lResult ^ 0xC0000000 ^ lX8 ^ lY8);
|
||||
} else {
|
||||
return (lResult ^ 0x40000000 ^ lX8 ^ lY8);
|
||||
}
|
||||
} else {
|
||||
return (lResult ^ lX8 ^ lY8);
|
||||
}
|
||||
}
|
||||
|
||||
function F(x, y, z) {
|
||||
return (x & y) | ((~x) & z);
|
||||
}
|
||||
function G(x, y, z) {
|
||||
return (x & z) | (y & (~z));
|
||||
}
|
||||
function H(x, y, z) {
|
||||
return (x ^ y ^ z);
|
||||
}
|
||||
function I(x, y, z) {
|
||||
return (y ^ (x | (~z)));
|
||||
}
|
||||
|
||||
function FF(a, b, c, d, x, s, ac) {
|
||||
a = AddUnsigned(a, AddUnsigned(AddUnsigned(F(b, c, d), x), ac));
|
||||
return AddUnsigned(RotateLeft(a, s), b);
|
||||
}
|
||||
|
||||
function GG(a, b, c, d, x, s, ac) {
|
||||
a = AddUnsigned(a, AddUnsigned(AddUnsigned(G(b, c, d), x), ac));
|
||||
return AddUnsigned(RotateLeft(a, s), b);
|
||||
}
|
||||
|
||||
function HH(a, b, c, d, x, s, ac) {
|
||||
a = AddUnsigned(a, AddUnsigned(AddUnsigned(H(b, c, d), x), ac));
|
||||
return AddUnsigned(RotateLeft(a, s), b);
|
||||
}
|
||||
|
||||
function II(a, b, c, d, x, s, ac) {
|
||||
a = AddUnsigned(a, AddUnsigned(AddUnsigned(I(b, c, d), x), ac));
|
||||
return AddUnsigned(RotateLeft(a, s), b);
|
||||
}
|
||||
|
||||
function ConvertToWordArray(string) {
|
||||
var lWordCount;
|
||||
var lMessageLength = string.length;
|
||||
var lNumberOfWords_temp1 = lMessageLength + 8;
|
||||
var lNumberOfWords_temp2 = (lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64;
|
||||
var lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16;
|
||||
var lWordArray = Array(lNumberOfWords - 1);
|
||||
var lBytePosition = 0;
|
||||
var lByteCount = 0;
|
||||
while (lByteCount < lMessageLength) {
|
||||
lWordCount = (lByteCount - (lByteCount % 4)) / 4;
|
||||
lBytePosition = (lByteCount % 4) * 8;
|
||||
lWordArray[lWordCount] = (lWordArray[lWordCount] | (string.charCodeAt(lByteCount) << lBytePosition));
|
||||
lByteCount++;
|
||||
}
|
||||
lWordCount = (lByteCount - (lByteCount % 4)) / 4;
|
||||
lBytePosition = (lByteCount % 4) * 8;
|
||||
lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition);
|
||||
lWordArray[lNumberOfWords - 2] = lMessageLength << 3;
|
||||
lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29;
|
||||
return lWordArray;
|
||||
}
|
||||
|
||||
function WordToHex(lValue) {
|
||||
var WordToHexValue = "", WordToHexValue_temp = "", lByte, lCount;
|
||||
for (lCount = 0; lCount <= 3; lCount++) {
|
||||
lByte = (lValue >>> (lCount * 8)) & 255;
|
||||
WordToHexValue_temp = "0" + lByte.toString(16);
|
||||
WordToHexValue = WordToHexValue + WordToHexValue_temp.substr(WordToHexValue_temp.length - 2, 2);
|
||||
}
|
||||
return WordToHexValue;
|
||||
}
|
||||
|
||||
function Utf8Encode(string) {
|
||||
string = string.replace(/\r\n/g, "\n");
|
||||
var utftext = "";
|
||||
|
||||
for (var n = 0; n < string.length; n++) {
|
||||
|
||||
var c = string.charCodeAt(n);
|
||||
|
||||
if (c < 128) {
|
||||
utftext += String.fromCharCode(c);
|
||||
}
|
||||
else if ((c > 127) && (c < 2048)) {
|
||||
utftext += String.fromCharCode((c >> 6) | 192);
|
||||
utftext += String.fromCharCode((c & 63) | 128);
|
||||
}
|
||||
else {
|
||||
utftext += String.fromCharCode((c >> 12) | 224);
|
||||
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
|
||||
utftext += String.fromCharCode((c & 63) | 128);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return utftext;
|
||||
}
|
||||
|
||||
var x = Array();
|
||||
var k, AA, BB, CC, DD, a, b, c, d;
|
||||
var S11 = 7, S12 = 12, S13 = 17, S14 = 22;
|
||||
var S21 = 5, S22 = 9, S23 = 14, S24 = 20;
|
||||
var S31 = 4, S32 = 11, S33 = 16, S34 = 23;
|
||||
var S41 = 6, S42 = 10, S43 = 15, S44 = 21;
|
||||
|
||||
string = Utf8Encode(string);
|
||||
|
||||
x = ConvertToWordArray(string);
|
||||
|
||||
a = 0x67452301; b = 0xEFCDAB89; c = 0x98BADCFE; d = 0x10325476;
|
||||
|
||||
for (k = 0; k < x.length; k += 16) {
|
||||
AA = a; BB = b; CC = c; DD = d;
|
||||
a = FF(a, b, c, d, x[k + 0], S11, 0xD76AA478);
|
||||
d = FF(d, a, b, c, x[k + 1], S12, 0xE8C7B756);
|
||||
c = FF(c, d, a, b, x[k + 2], S13, 0x242070DB);
|
||||
b = FF(b, c, d, a, x[k + 3], S14, 0xC1BDCEEE);
|
||||
a = FF(a, b, c, d, x[k + 4], S11, 0xF57C0FAF);
|
||||
d = FF(d, a, b, c, x[k + 5], S12, 0x4787C62A);
|
||||
c = FF(c, d, a, b, x[k + 6], S13, 0xA8304613);
|
||||
b = FF(b, c, d, a, x[k + 7], S14, 0xFD469501);
|
||||
a = FF(a, b, c, d, x[k + 8], S11, 0x698098D8);
|
||||
d = FF(d, a, b, c, x[k + 9], S12, 0x8B44F7AF);
|
||||
c = FF(c, d, a, b, x[k + 10], S13, 0xFFFF5BB1);
|
||||
b = FF(b, c, d, a, x[k + 11], S14, 0x895CD7BE);
|
||||
a = FF(a, b, c, d, x[k + 12], S11, 0x6B901122);
|
||||
d = FF(d, a, b, c, x[k + 13], S12, 0xFD987193);
|
||||
c = FF(c, d, a, b, x[k + 14], S13, 0xA679438E);
|
||||
b = FF(b, c, d, a, x[k + 15], S14, 0x49B40821);
|
||||
a = GG(a, b, c, d, x[k + 1], S21, 0xF61E2562);
|
||||
d = GG(d, a, b, c, x[k + 6], S22, 0xC040B340);
|
||||
c = GG(c, d, a, b, x[k + 11], S23, 0x265E5A51);
|
||||
b = GG(b, c, d, a, x[k + 0], S24, 0xE9B6C7AA);
|
||||
a = GG(a, b, c, d, x[k + 5], S21, 0xD62F105D);
|
||||
d = GG(d, a, b, c, x[k + 10], S22, 0x2441453);
|
||||
c = GG(c, d, a, b, x[k + 15], S23, 0xD8A1E681);
|
||||
b = GG(b, c, d, a, x[k + 4], S24, 0xE7D3FBC8);
|
||||
a = GG(a, b, c, d, x[k + 9], S21, 0x21E1CDE6);
|
||||
d = GG(d, a, b, c, x[k + 14], S22, 0xC33707D6);
|
||||
c = GG(c, d, a, b, x[k + 3], S23, 0xF4D50D87);
|
||||
b = GG(b, c, d, a, x[k + 8], S24, 0x455A14ED);
|
||||
a = GG(a, b, c, d, x[k + 13], S21, 0xA9E3E905);
|
||||
d = GG(d, a, b, c, x[k + 2], S22, 0xFCEFA3F8);
|
||||
c = GG(c, d, a, b, x[k + 7], S23, 0x676F02D9);
|
||||
b = GG(b, c, d, a, x[k + 12], S24, 0x8D2A4C8A);
|
||||
a = HH(a, b, c, d, x[k + 5], S31, 0xFFFA3942);
|
||||
d = HH(d, a, b, c, x[k + 8], S32, 0x8771F681);
|
||||
c = HH(c, d, a, b, x[k + 11], S33, 0x6D9D6122);
|
||||
b = HH(b, c, d, a, x[k + 14], S34, 0xFDE5380C);
|
||||
a = HH(a, b, c, d, x[k + 1], S31, 0xA4BEEA44);
|
||||
d = HH(d, a, b, c, x[k + 4], S32, 0x4BDECFA9);
|
||||
c = HH(c, d, a, b, x[k + 7], S33, 0xF6BB4B60);
|
||||
b = HH(b, c, d, a, x[k + 10], S34, 0xBEBFBC70);
|
||||
a = HH(a, b, c, d, x[k + 13], S31, 0x289B7EC6);
|
||||
d = HH(d, a, b, c, x[k + 0], S32, 0xEAA127FA);
|
||||
c = HH(c, d, a, b, x[k + 3], S33, 0xD4EF3085);
|
||||
b = HH(b, c, d, a, x[k + 6], S34, 0x4881D05);
|
||||
a = HH(a, b, c, d, x[k + 9], S31, 0xD9D4D039);
|
||||
d = HH(d, a, b, c, x[k + 12], S32, 0xE6DB99E5);
|
||||
c = HH(c, d, a, b, x[k + 15], S33, 0x1FA27CF8);
|
||||
b = HH(b, c, d, a, x[k + 2], S34, 0xC4AC5665);
|
||||
a = II(a, b, c, d, x[k + 0], S41, 0xF4292244);
|
||||
d = II(d, a, b, c, x[k + 7], S42, 0x432AFF97);
|
||||
c = II(c, d, a, b, x[k + 14], S43, 0xAB9423A7);
|
||||
b = II(b, c, d, a, x[k + 5], S44, 0xFC93A039);
|
||||
a = II(a, b, c, d, x[k + 12], S41, 0x655B59C3);
|
||||
d = II(d, a, b, c, x[k + 3], S42, 0x8F0CCC92);
|
||||
c = II(c, d, a, b, x[k + 10], S43, 0xFFEFF47D);
|
||||
b = II(b, c, d, a, x[k + 1], S44, 0x85845DD1);
|
||||
a = II(a, b, c, d, x[k + 8], S41, 0x6FA87E4F);
|
||||
d = II(d, a, b, c, x[k + 15], S42, 0xFE2CE6E0);
|
||||
c = II(c, d, a, b, x[k + 6], S43, 0xA3014314);
|
||||
b = II(b, c, d, a, x[k + 13], S44, 0x4E0811A1);
|
||||
a = II(a, b, c, d, x[k + 4], S41, 0xF7537E82);
|
||||
d = II(d, a, b, c, x[k + 11], S42, 0xBD3AF235);
|
||||
c = II(c, d, a, b, x[k + 2], S43, 0x2AD7D2BB);
|
||||
b = II(b, c, d, a, x[k + 9], S44, 0xEB86D391);
|
||||
a = AddUnsigned(a, AA);
|
||||
b = AddUnsigned(b, BB);
|
||||
c = AddUnsigned(c, CC);
|
||||
d = AddUnsigned(d, DD);
|
||||
}
|
||||
|
||||
var temp = WordToHex(a) + WordToHex(b) + WordToHex(c) + WordToHex(d);
|
||||
|
||||
return temp.toLowerCase();
|
||||
}
|
||||
|
||||
export { md5 };
|
||||
|
|
@ -13,11 +13,8 @@
|
|||
required
|
||||
/>
|
||||
<button @click="copyEmail" type="button" class="btn-copy" :title="$t('home.copyTitle')">
|
||||
<span v-if="copyStatus === 'copied'">✓</span>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M4 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1z"/>
|
||||
<path d="M2 5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-1h-1v1a.5.5 0 0 1-.5.5H2.5a.5.5 0 0 1-.5-.5V6.5a.5.5 0 0 1 .5-.5H3v-1z"/>
|
||||
</svg>
|
||||
<el-icon v-if="copyStatus === 'copied'"><Check /></el-icon>
|
||||
<el-icon v-else><CopyDocument /></el-icon>
|
||||
</button>
|
||||
</div>
|
||||
<button @click="generateRandomEmail" type="button" class="btn btn-secondary">{{ $t('home.random') }}</button>
|
||||
|
|
@ -30,9 +27,17 @@
|
|||
<div class="message-list">
|
||||
<div class="message-list-header">
|
||||
<h2>{{ $t('home.inbox') }}</h2>
|
||||
<button @click="fetchMessages" class="refresh-btn" :title="$t('home.refresh')">
|
||||
↻
|
||||
</button>
|
||||
<div class="header-actions">
|
||||
<button @click="clearInbox" :title="$t('home.clearInbox')" :disabled="messages.length === 0">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</button>
|
||||
<button @click="fetchMessages" :title="$t('home.refresh')">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="newMailNotification" class="new-mail-notification">
|
||||
{{ $t('home.newMailNotification') }}
|
||||
</div>
|
||||
<div v-if="loading" class="loading-state">{{ $t('home.loading') }}</div>
|
||||
<div v-else-if="messages.length === 0" class="empty-state">{{ $t('home.noMail') }}</div>
|
||||
|
|
@ -55,12 +60,27 @@
|
|||
</div>
|
||||
<div v-else>
|
||||
<div class="message-content-header">
|
||||
<h3>{{ selectedMessage.subject }}</h3>
|
||||
<p class="from-line"><strong>{{ $t('home.from') }}:</strong> {{ selectedMessage.sender }}</p>
|
||||
<p><strong>{{ $t('home.to') }}:</strong> {{ selectedMessage.recipient }}</p>
|
||||
<div class="header-text">
|
||||
<h3>{{ selectedMessage.subject }}</h3>
|
||||
<p class="from-line"><strong>{{ $t('home.from') }}:</strong> {{ selectedMessage.sender }}</p>
|
||||
<p><strong>{{ $t('home.to') }}:</strong> {{ selectedMessage.recipient }}</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button @click="deleteMessage(selectedMessage.id)" :title="$t('home.delete')">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="message-body">
|
||||
{{ selectedMessage.body }}
|
||||
<div class="message-body" v-html="renderedBody">
|
||||
</div>
|
||||
<div v-if="attachmentsLoading" class="loading-state">{{ $t('home.loading') }}</div>
|
||||
<div v-if="attachments.length > 0" class="attachments-section">
|
||||
<h4>{{ $t('home.attachments') }}</h4>
|
||||
<ul>
|
||||
<li v-for="attachment in attachments" :key="attachment.id">
|
||||
<a :href="`/api/attachments/${attachment.id}`" :download="attachment.filename">{{ attachment.filename }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -69,153 +89,285 @@
|
|||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref } from 'vue';
|
||||
<script setup>
|
||||
import { ref, onUnmounted, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Delete, Refresh, CopyDocument, Check } from '@element-plus/icons-vue';
|
||||
import { marked } from 'marked';
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
setup() {
|
||||
const recipient = ref('');
|
||||
const messages = ref([]);
|
||||
const selectedMessage = ref(null);
|
||||
const loading = ref(false);
|
||||
const copyStatus = ref('idle'); // 'idle' | 'copied'
|
||||
const domain = 'shenjianl.cn';
|
||||
const { t } = useI18n();
|
||||
const recipient = ref('');
|
||||
const messages = ref([]);
|
||||
const selectedMessage = ref(null);
|
||||
const loading = ref(false);
|
||||
const copyStatus = ref('idle'); // 'idle' | 'copied'
|
||||
const domain = 'shenjianl.cn';
|
||||
const attachments = ref([]);
|
||||
const attachmentsLoading = ref(false);
|
||||
const newMailNotification = ref(false);
|
||||
|
||||
const fetchMessages = async () => {
|
||||
if (!recipient.value) {
|
||||
alert('请输入一个邮箱地址');
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
selectedMessage.value = null; // Clear selected message on new fetch
|
||||
try {
|
||||
const response = await fetch(`/api/messages?recipient=${recipient.value}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
const data = await response.json();
|
||||
messages.value = data;
|
||||
if (messages.value.length > 0) {
|
||||
selectedMessage.value = messages.value[0];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch messages:', error);
|
||||
alert('无法获取邮件,请检查后端服务和 Nginx 配置是否正常。');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
const renderedBody = computed(() => {
|
||||
if (selectedMessage.value && selectedMessage.value.body) {
|
||||
return marked(selectedMessage.value.body);
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
const selectMessage = (message) => {
|
||||
selectedMessage.value = message;
|
||||
};
|
||||
let ws = null;
|
||||
|
||||
const generateRandomEmail = () => {
|
||||
const names = [
|
||||
'alex', 'casey', 'morgan', 'jordan', 'taylor', 'jamie', 'ryan', 'drew', 'jesse', 'pat',
|
||||
'chris', 'dylan', 'aaron', 'blake', 'cameron', 'devon', 'elliot', 'finn', 'gray', 'harper',
|
||||
'kai', 'logan', 'max', 'noah', 'owen', 'quinn', 'riley', 'rowan', 'sage', 'skyler'
|
||||
];
|
||||
const places = [
|
||||
'tokyo', 'paris', 'london', 'cairo', 'sydney', 'rio', 'moscow', 'rome', 'nile', 'everest',
|
||||
'sahara', 'amazon', 'gobi', 'andes', 'pacific', 'kyoto', 'berlin', 'dubai', 'seoul', 'milan',
|
||||
'vienna', 'prague', 'athens', 'lisbon', 'oslo', 'helsinki', 'zürich', 'geneva', 'brussels', 'amsterdam'
|
||||
];
|
||||
const concepts = [
|
||||
'apollo', 'artemis', 'athena', 'zeus', 'thor', 'loki', 'odin', 'freya', 'phoenix', 'dragon',
|
||||
'griffin', 'sphinx', 'pyramid', 'colossus', 'acropolis', 'obelisk', 'pagoda', 'castle', 'cyberspace', 'matrix',
|
||||
'protocol', 'algorithm', 'pixel', 'vector', 'photon', 'quark', 'nova', 'pulsar', 'saga', 'voyage',
|
||||
'enigma', 'oracle', 'cipher', 'vortex', 'helix', 'axiom', 'zenith', 'epoch', 'nexus', 'trinity'
|
||||
];
|
||||
const setupWebSocket = () => {
|
||||
if (ws) {
|
||||
ws.close();
|
||||
}
|
||||
|
||||
const allLists = [names, places, concepts];
|
||||
const getRandomItem = (arr) => arr[Math.floor(Math.random() * arr.length)];
|
||||
if (!recipient.value) return;
|
||||
|
||||
const listA = getRandomItem(allLists);
|
||||
const listB = getRandomItem(allLists);
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${wsProtocol}//${window.location.host}/ws?recipient=${encodeURIComponent(recipient.value)}`;
|
||||
|
||||
ws = new WebSocket(wsUrl);
|
||||
|
||||
const word1 = getRandomItem(listA);
|
||||
const word2 = getRandomItem(listB);
|
||||
|
||||
const number = Math.floor(Math.random() * 9000) + 1000;
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket connection established');
|
||||
};
|
||||
|
||||
const separators = ['.', '-', '_', ''];
|
||||
const separator = getRandomItem(separators);
|
||||
ws.onmessage = (event) => {
|
||||
const newEmail = JSON.parse(event.data);
|
||||
messages.value.unshift(newEmail);
|
||||
ElMessage.success(t('home.newMailNotification'));
|
||||
};
|
||||
|
||||
let prefix;
|
||||
if (word1 === word2) {
|
||||
prefix = `${word1}${number}`;
|
||||
} else {
|
||||
prefix = `${word1}${separator}${word2}${number}`;
|
||||
}
|
||||
|
||||
recipient.value = `${prefix}@${domain}`;
|
||||
};
|
||||
|
||||
const copyEmail = () => {
|
||||
if (!recipient.value) return;
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
|
||||
const textToCopy = recipient.value;
|
||||
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||
showCopySuccess();
|
||||
}).catch(err => {
|
||||
console.error('Modern copy failed: ', err);
|
||||
fallbackCopy(textToCopy);
|
||||
});
|
||||
} else {
|
||||
fallbackCopy(textToCopy);
|
||||
}
|
||||
};
|
||||
|
||||
const fallbackCopy = (text) => {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.top = '-9999px';
|
||||
textArea.style.left = '-9999px';
|
||||
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
try {
|
||||
const successful = document.execCommand('copy');
|
||||
if (successful) {
|
||||
showCopySuccess();
|
||||
} else {
|
||||
throw new Error('Fallback copy was unsuccessful');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fallback copy failed: ', err);
|
||||
alert('复制失败!');
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
};
|
||||
|
||||
const showCopySuccess = () => {
|
||||
copyStatus.value = 'copied';
|
||||
setTimeout(() => {
|
||||
copyStatus.value = 'idle';
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
return {
|
||||
recipient,
|
||||
messages,
|
||||
selectedMessage,
|
||||
loading,
|
||||
copyStatus,
|
||||
domain,
|
||||
fetchMessages,
|
||||
selectMessage,
|
||||
generateRandomEmail,
|
||||
copyEmail,
|
||||
};
|
||||
},
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket connection closed');
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
onUnmounted(() => {
|
||||
if (ws) {
|
||||
ws.close();
|
||||
}
|
||||
});
|
||||
|
||||
const fetchMessages = async () => {
|
||||
if (!recipient.value) {
|
||||
ElMessage.warning(t('home.alerts.enterEmail'));
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
selectedMessage.value = null;
|
||||
attachments.value = [];
|
||||
try {
|
||||
const response = await fetch(`/api/messages?recipient=${recipient.value}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
const data = await response.json();
|
||||
messages.value = data;
|
||||
if (messages.value.length > 0) {
|
||||
selectMessage(messages.value[0]);
|
||||
}
|
||||
setupWebSocket(); // Setup WebSocket after fetching messages
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch messages:', error);
|
||||
ElMessage.error(t('home.alerts.fetchFailed'));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAttachments = async (messageId) => {
|
||||
attachmentsLoading.value = true;
|
||||
attachments.value = [];
|
||||
try {
|
||||
const response = await fetch(`/api/messages/${messageId}/attachments`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
const data = await response.json();
|
||||
attachments.value = data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch attachments:', error);
|
||||
} finally {
|
||||
attachmentsLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const selectMessage = (message) => {
|
||||
selectedMessage.value = message;
|
||||
if (message) {
|
||||
fetchAttachments(message.id);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteMessage = async (messageId) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
t('home.alerts.confirmDelete'),
|
||||
t('home.delete'),
|
||||
{
|
||||
confirmButtonText: 'OK',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'warning',
|
||||
}
|
||||
);
|
||||
|
||||
const response = await fetch(`/api/messages/${messageId}`, { method: 'DELETE' });
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
|
||||
const index = messages.value.findIndex(m => m.id === messageId);
|
||||
if (index !== -1) {
|
||||
messages.value.splice(index, 1);
|
||||
}
|
||||
|
||||
if (selectedMessage.value && selectedMessage.value.id === messageId) {
|
||||
const newIndex = Math.max(0, index - 1);
|
||||
selectedMessage.value = messages.value.length > 0 ? messages.value[newIndex] : null;
|
||||
if (selectedMessage.value) {
|
||||
selectMessage(selectedMessage.value);
|
||||
} else {
|
||||
attachments.value = [];
|
||||
}
|
||||
}
|
||||
ElMessage.success(t('home.alerts.deleteSuccess'));
|
||||
} catch (action) {
|
||||
if (action === 'cancel') {
|
||||
ElMessage.info(t('home.alerts.actionCancelled'));
|
||||
} else {
|
||||
console.error('Failed to delete message:', action);
|
||||
ElMessage.error(t('home.alerts.deleteFailed'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const clearInbox = async () => {
|
||||
if (!recipient.value || messages.value.length === 0) return;
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
t('home.alerts.confirmClearAll'),
|
||||
t('home.clearInbox'),
|
||||
{
|
||||
confirmButtonText: 'OK',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'warning',
|
||||
}
|
||||
);
|
||||
|
||||
const response = await fetch(`/api/messages?recipient=${recipient.value}`, { method: 'DELETE' });
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
messages.value = [];
|
||||
selectedMessage.value = null;
|
||||
attachments.value = [];
|
||||
ElMessage.success(t('home.alerts.clearSuccess'));
|
||||
} catch (action) {
|
||||
if (action === 'cancel') {
|
||||
ElMessage.info(t('home.alerts.actionCancelled'));
|
||||
} else {
|
||||
console.error('Failed to clear inbox:', action);
|
||||
ElMessage.error(t('home.alerts.clearFailed'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const generateRandomEmail = () => {
|
||||
const names = [
|
||||
'alex', 'casey', 'morgan', 'jordan', 'taylor', 'jamie', 'ryan', 'drew', 'jesse', 'pat',
|
||||
'chris', 'dylan', 'aaron', 'blake', 'cameron', 'devon', 'elliot', 'finn', 'gray', 'harper',
|
||||
'kai', 'logan', 'max', 'noah', 'owen', 'quinn', 'riley', 'rowan', 'sage', 'skyler'
|
||||
];
|
||||
const places = [
|
||||
'tokyo', 'paris', 'london', 'cairo', 'sydney', 'rio', 'moscow', 'rome', 'nile', 'everest',
|
||||
'sahara', 'amazon', 'gobi', 'andes', 'pacific', 'kyoto', 'berlin', 'dubai', 'seoul', 'milan',
|
||||
'vienna', 'prague', 'athens', 'lisbon', 'oslo', 'helsinki', 'zürich', 'geneva', 'brussels', 'amsterdam'
|
||||
];
|
||||
const concepts = [
|
||||
'apollo', 'artemis', 'athena', 'zeus', 'thor', 'loki', 'odin', 'freya', 'phoenix', 'dragon',
|
||||
'griffin', 'sphinx', 'pyramid', 'colossus', 'acropolis', 'obelisk', 'pagoda', 'castle', 'cyberspace', 'matrix',
|
||||
'protocol', 'algorithm', 'pixel', 'vector', 'photon', 'quark', 'nova', 'pulsar', 'saga', 'voyage',
|
||||
'enigma', 'oracle', 'cipher', 'vortex', 'helix', 'axiom', 'zenith', 'epoch', 'nexus', 'trinity'
|
||||
];
|
||||
|
||||
const allLists = [names, places, concepts];
|
||||
const getRandomItem = (arr) => arr[Math.floor(Math.random() * arr.length)];
|
||||
|
||||
const listA = getRandomItem(allLists);
|
||||
const listB = getRandomItem(allLists);
|
||||
|
||||
const word1 = getRandomItem(listA);
|
||||
const word2 = getRandomItem(listB);
|
||||
|
||||
const number = Math.floor(Math.random() * 9000) + 1000;
|
||||
|
||||
const separators = ['.', '-', '_', ''];
|
||||
const separator = getRandomItem(separators);
|
||||
|
||||
let prefix;
|
||||
if (word1 === word2) {
|
||||
prefix = `${word1}${number}`;
|
||||
} else {
|
||||
prefix = `${word1}${separator}${word2}${number}`;
|
||||
}
|
||||
|
||||
recipient.value = `${prefix}@${domain}`;
|
||||
};
|
||||
|
||||
const copyEmail = () => {
|
||||
if (!recipient.value) return;
|
||||
|
||||
const textToCopy = recipient.value;
|
||||
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||
showCopySuccess();
|
||||
}).catch(err => {
|
||||
console.error('Modern copy failed: ', err);
|
||||
fallbackCopy(textToCopy);
|
||||
});
|
||||
} else {
|
||||
fallbackCopy(textToCopy);
|
||||
}
|
||||
};
|
||||
|
||||
const fallbackCopy = (text) => {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.top = '-9999px';
|
||||
textArea.style.left = '-9999px';
|
||||
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
try {
|
||||
const successful = document.execCommand('copy');
|
||||
if (successful) {
|
||||
showCopySuccess();
|
||||
} else {
|
||||
throw new Error('Fallback copy was unsuccessful');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fallback copy failed: ', err);
|
||||
ElMessage.error(t('home.alerts.copyFailed'));
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
};
|
||||
|
||||
const showCopySuccess = () => {
|
||||
copyStatus.value = 'copied';
|
||||
setTimeout(() => {
|
||||
copyStatus.value = 'idle';
|
||||
}, 2000);
|
||||
};
|
||||
</script>
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
manifest: {
|
||||
name: 'Email Unlimit',
|
||||
short_name: 'EmailUnlimit',
|
||||
description: 'Your Exclusive, Unlimited, and Private Temporary Email',
|
||||
theme_color: '#000000',
|
||||
icons: [
|
||||
{
|
||||
src: 'icons/icon.svg',
|
||||
sizes: 'any',
|
||||
type: 'image/svg+xml',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
server: {
|
||||
proxy: process.env.NODE_ENV === 'production' ? {} : {
|
||||
'/api': {
|
||||
target: 'http://localhost:5182',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/ws': {
|
||||
target: 'ws://localhost:5182',
|
||||
ws: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
define: {
|
||||
'process.env': process.env
|
||||
}
|
||||
})
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
const { defineConfig } = require('@vue/cli-service')
|
||||
|
||||
module.exports = defineConfig({
|
||||
transpileDependencies: true,
|
||||
// devServer 代理配置已移除,所有代理均由外部 Nginx 处理。
|
||||
pwa: {
|
||||
name: 'EmailUnlimit',
|
||||
themeColor: '#000000',
|
||||
msTileColor: '#000000',
|
||||
appleMobileWebAppCapable: 'yes',
|
||||
appleMobileWebAppStatusBarStyle: 'black',
|
||||
|
||||
// 配置 workbox-webpack-plugin
|
||||
workboxPluginMode: 'GenerateSW',
|
||||
workboxOptions: {
|
||||
// swSrc is required in InjectManifest mode.
|
||||
// swSrc: 'dev/sw.js',
|
||||
// ...other Workbox options...
|
||||
}
|
||||
},
|
||||
chainWebpack: config => {
|
||||
config.plugin('define').tap(definitions => {
|
||||
Object.assign(definitions[0], {
|
||||
__VUE_OPTIONS_API__: true,
|
||||
__VUE_PROD_DEVTOOLS__: false,
|
||||
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false
|
||||
})
|
||||
return definitions
|
||||
})
|
||||
}
|
||||
})
|
||||
Loading…
Reference in New Issue