Compare commits
2 Commits
mailu
...
f1b39d22a5
| Author | SHA1 | Date | |
|---|---|---|---|
| f1b39d22a5 | |||
| eeb3b4b0df |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@
|
||||
/frontend/node_modules
|
||||
/backend/package-lock.json
|
||||
/frontend/package-lock.json
|
||||
/frontend/dist
|
||||
158
README.md
158
README.md
@@ -1,60 +1,100 @@
|
||||
# Email Unlimit Project (临时邮件项目)
|
||||
# 轻量级临时邮件项目 (Email Unlimit)
|
||||
|
||||
## 最终架构 (单一 Docker Compose + 宿主机 Nginx)
|
||||
本项目是一个轻量级的、可自托管的临时邮件解决方案。它允许您使用自己的域名接收邮件,并通过一个简洁的网页界面来查看这些邮件。
|
||||
|
||||
本项目采用生产环境推荐的架构:所有服务(包括您的应用和完整的 Mailu 邮件套件)都由一个 `docker-compose.full.yml` 文件统一管理,而入口的反向代理则由 **宿主机上的 Nginx** 负责。
|
||||
与 `Mailu` 等复杂的邮件套件不同,本项目采用了一个极简的 Node.js 服务来直接接收和处理邮件,部署和维护都非常简单。
|
||||
|
||||
- **宿主机 Nginx:** 作为项目的唯一公共入口 (`main.shenjianl.cn`)。它负责:
|
||||
1. 处理所有外部流量和 SSL 加密(推荐)。
|
||||
2. 将对域名根路径 (`/`) 的访问反向代理到 **前端 Docker 容器** (`localhost:5181`)。
|
||||
3. 将对 `/api` 路径的访问反向代理到 **后端 Docker 容器** (`localhost:5182`)。
|
||||
4. 将对 `/mailu` 路径的访问反向代理到 **Mailu Admin UI** (`localhost:80`)。
|
||||
## 技术架构
|
||||
|
||||
- **Docker 容器 (由 `docker-compose.full.yml` 管理):**
|
||||
- **您的应用:** `frontend`, `backend`, `mysql`。
|
||||
- **Mailu 套件:** `front` (Mailu Nginx), `admin`, `smtp`, `imap`, `redis` 等全套服务。
|
||||
* **前端 (Frontend)**: 使用 Vue.js 构建的单页面应用,负责展示收到的邮件列表。
|
||||
* **后端 (Backend)**:
|
||||
* 使用 Node.js 和 Express 搭建的 API 服务器。
|
||||
* 内置一个轻量级的 SMTP 服务器 (`smtp-server`),用于直接接收邮件,无需外部邮件服务。
|
||||
* 负责将收到的邮件解析并存入数据库。
|
||||
* **数据库 (Database)**: 需要一个外部的 MySQL 数据库来存储邮件信息。
|
||||
* **部署 (Deployment)**: 后端服务通过 Docker Compose 进行容器化部署,前端静态文件由宿主机的 Nginx 提供服务。
|
||||
|
||||
---
|
||||
## 部署要求
|
||||
|
||||
## 如何运行
|
||||
在开始之前,<EFBFBD><EFBFBD>确保您已准备好以下环境:
|
||||
|
||||
### 步骤 1: 准备工作
|
||||
1. 一台拥有公网 IP 的 Linux 服务器。
|
||||
2. 一个您自己的域名。
|
||||
3. 服务器上已安装 `Docker` 和 `Docker Compose`。
|
||||
4. 服务器上已安装 `Nginx`。
|
||||
5. 一个可用的外部 MySQL 数据库,并已创建好数据库。
|
||||
6. 本地开发环境已安装 `Node.js` 和 `npm` (用于构建前端)。
|
||||
|
||||
1. **安装 Docker**: 确保您的系统中已安装 Docker 和 Docker Compose。
|
||||
2. **配置 Mailu**: 打开 `mailu.env` 文件,**务必修改** `SECRET_KEY` 和 `ADMIN_PASSWORD` 为安全的值。域名和数据库配置已预填。
|
||||
3. **配置 DNS**: 根据 `info.md` 中的 DNS 记录示例,在您的域名提供商处完成 SPF、DKIM 和 DMARC 的配置。
|
||||
## 部署步骤
|
||||
|
||||
### 步骤 2: 配置宿主机 Nginx
|
||||
### 步骤 1: 配置域名 DNS
|
||||
|
||||
将下面的配置块添加到您宿主机的 Nginx 中。这份配置统一处理了对您的应用和 Mailu 管理后台的访问。
|
||||
要让邮件能正确发送到您的服务器,您必须配置域名的 `MX` 记录。
|
||||
|
||||
1. 登录您的域名注册商(如 GoDaddy, Cloudflare 等)。
|
||||
2. 找到 DNS 解析设置。
|
||||
3. 添加一条 `MX` 记录:
|
||||
* **类型 (Type)**: `MX`
|
||||
* **名称 (Name/Host)**: `@` (代表您的根域名)
|
||||
* **值 (Value/Points to)**: `您的服务器公网 IP 地址`
|
||||
* **优先级 (Priority)**: `10`
|
||||
|
||||
> **注意**: DNS 记录生效可能需要几分钟到几小时。
|
||||
|
||||
### 步骤 2: 部署后端服务
|
||||
|
||||
1. 将本项目克隆或上传到您的服务器。
|
||||
2. 进入项目根目录,编辑 `docker-compose.yml` 文件。
|
||||
3. **填写您的外部数据库连接信息**:
|
||||
```yaml
|
||||
services:
|
||||
backend:
|
||||
# ...
|
||||
environment:
|
||||
- DB_HOST=your_external_db_host # 替换为您的外部数据库主机名或IP
|
||||
- DB_USER=your_external_db_user # 替换为您的数据库用户名
|
||||
- DB_PASSWORD=your_external_db_password # 替换为您的数据库密码
|
||||
- DB_NAME=your_external_db_name # 替换为您的数据库名称
|
||||
```
|
||||
4. 在 `backend` 目录下有一个 `init.sql` 文件,请手动将其中的 SQL 命令在您的外部数据库中执行,以创建所需的表。
|
||||
5. 在项目根目录,使用 Docker Compose 启动后端服务:
|
||||
```bash
|
||||
docker-compose up -d --build
|
||||
```
|
||||
此命令会构建并以后台模式启动后端容器。服务将监听服务器的 `5182` (API) 和 `25` (SMTP) 端口。
|
||||
|
||||
### 步骤 3: 构建和部署前端
|
||||
|
||||
1. **在您的本地开发机上**,进入 `frontend` 目录。
|
||||
2. 安装依赖并构建静态文件:
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
这将在 `frontend/dist` 目录下生成所有用于部署的静态文件。
|
||||
3. 将 `frontend/dist` 目录下的 **所有文件** 上传到您服务器的指定位置,例如 `/var/www/email-unlimit`。
|
||||
|
||||
### 步骤 4: 配置宿主机 Nginx
|
||||
|
||||
1. 在服务器上,为您的应用创建一个 Nginx 配置文件,例如 `/etc/nginx/sites-available/email.conf`。
|
||||
2. 将以下配置写入该文件。请务必将 `your_domain.com` 和 `root` 路径修改为您自己的配置。
|
||||
|
||||
```nginx
|
||||
# /etc/nginx/conf.d/main.shenjianl.cn.conf
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name main.shenjianl.cn;
|
||||
listen 443 ssl;
|
||||
server_name mail.shenjianl.cn; # 替换为您的域名
|
||||
ssl_certificate /usr/local/nginx/conf/ssl_certificate/mail/mail.shenjianl.cn_bundle.pem;
|
||||
ssl_certificate_key /usr/local/nginx/conf/ssl_certificate/mail/mail.shenjianl.cn.key;
|
||||
# 前端静态文件路径
|
||||
root /data/email-unlimit/frontend/dist;
|
||||
index index.html;
|
||||
|
||||
# --- 推荐配置 SSL ---
|
||||
# listen 443 ssl http2;
|
||||
# server_name main.shenjianl.cn;
|
||||
# ssl_certificate /path/to/your/fullchain.pem;
|
||||
# ssl_certificate_key /path/to/your/privkey.pem;
|
||||
# --------------------
|
||||
|
||||
# 反向代理到前端 Vue.js 容器
|
||||
# 处理 Vue Router 的 history 模式
|
||||
location / {
|
||||
proxy_pass http://localhost:5181;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# 反向代理到后端 API 容器
|
||||
# 将 /api 请求反向代理到后端 Docker 容器
|
||||
location /api {
|
||||
proxy_pass http://localhost:5182;
|
||||
proxy_set_header Host $host;
|
||||
@@ -62,34 +102,22 @@ server {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# 反向代理到 Mailu Admin UI 和 Webmail
|
||||
# 注意:Mailu 自己的 Nginx (front) 在 80 端口上运行
|
||||
location /mailu {
|
||||
proxy_pass http://localhost:80;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
```
|
||||
**配置完成后,请重载或重启您的 Nginx 服务。**
|
||||
|
||||
### 步骤 3: 启动 Docker 服务
|
||||
|
||||
使用我们最终生成的 `docker-compose.full.yml` 文件来启动所有服务。
|
||||
|
||||
3. 启用该配置并重启 Nginx:
|
||||
```bash
|
||||
# 在项目根目录执行
|
||||
docker-compose -f docker-compose.full.yml up -d --build
|
||||
# 创建软链接
|
||||
sudo ln -s /etc/nginx/sites-available/email.conf /etc/nginx/sites-enabled/
|
||||
|
||||
# 测试配置语法
|
||||
sudo nginx -t
|
||||
|
||||
# 重启 Nginx
|
||||
sudo systemctl restart nginx
|
||||
```
|
||||
|
||||
---
|
||||
## 如何使用
|
||||
|
||||
## 访问<E8AEBF><E997AE><EFBFBD>址
|
||||
|
||||
- **主应用入口**: `http://main.shenjianl.cn`
|
||||
- **Mailu 管理后台**: `http://main.shenjianl.cn/mailu`
|
||||
- **Mailu Webmail**: `http://main.shenjianl.cn/mailu/webmail`
|
||||
|
||||
docker-compose up -d --build
|
||||
1. **访问您的网站**: 在浏览器中打开 `http://your_domain.com`。
|
||||
2. **发送测试邮件**: 使用任何邮箱客户端,向 `anything@your_domain.com` (例如 `test@your_domain.com`) 发送一封邮件。
|
||||
3. **查看邮件**: 在网站上输入您刚刚使用的收件人地址 (`anything@your_domain.com`),点击查询,即可看到收到的邮件。
|
||||
|
||||
@@ -7,6 +7,14 @@ RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
# Expose API port and SMTP port
|
||||
EXPOSE 5182
|
||||
EXPOSE 25
|
||||
|
||||
# Environment variables for database connection will be passed at runtime
|
||||
# ENV DB_HOST=...
|
||||
# ENV DB_USER=...
|
||||
# ENV DB_PASSWORD=...
|
||||
# ENV DB_NAME=...
|
||||
|
||||
CMD [ "node", "app.js" ]
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const db = require('./db');
|
||||
const { SMTPServer } = require('smtp-server');
|
||||
const { saveEmail } = require('./saveEmail');
|
||||
|
||||
const app = express();
|
||||
const port = 5182;
|
||||
const apiPort = 5182;
|
||||
const smtpPort = 25;
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
@@ -17,7 +20,7 @@ app.get('/api/messages', async (req, res) => {
|
||||
|
||||
try {
|
||||
const [rows] = await db.execute(
|
||||
'SELECT id, sender, subject, body, received_at FROM emails WHERE recipient = ? ORDER BY received_at DESC',
|
||||
'SELECT id, sender, recipient, subject, body, received_at FROM emails WHERE recipient = ? ORDER BY received_at DESC',
|
||||
[recipient]
|
||||
);
|
||||
res.json(rows);
|
||||
@@ -27,6 +30,33 @@ app.get('/api/messages', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Backend server listening at http://localhost:${port}`);
|
||||
// Start API server
|
||||
app.listen(apiPort, () => {
|
||||
console.log(`Backend API server listening at http://localhost:${apiPort}`);
|
||||
});
|
||||
|
||||
// Configure and start SMTP server
|
||||
const smtpServer = new SMTPServer({
|
||||
authOptional: true,
|
||||
disabledCommands: ['AUTH'],
|
||||
onData(stream, session, callback) {
|
||||
console.log('Receiving email...');
|
||||
saveEmail(stream)
|
||||
.then(() => {
|
||||
console.log('Email processed and saved successfully.');
|
||||
callback(); // Accept the message
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Error processing email:', err);
|
||||
callback(new Error('Failed to process email.'));
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
smtpServer.on('error', err => {
|
||||
console.error('SMTP Server Error:', err.message);
|
||||
});
|
||||
|
||||
smtpServer.listen(smtpPort, () => {
|
||||
console.log(`SMTP server listening on port ${smtpPort}`);
|
||||
});
|
||||
|
||||
@@ -11,6 +11,6 @@
|
||||
"mysql2": "^3.14.2",
|
||||
"mailparser": "^3.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"get-stream": "^9.0.1"
|
||||
"smtp-server": "^3.13.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { simpleParser } = require('mailparser');
|
||||
const db = require('./db');
|
||||
|
||||
// Using require for get-stream v5 as it's CommonJS
|
||||
const getStream = require('get-stream');
|
||||
// Helper function to convert stream to buffer
|
||||
function streamToBuffer(stream) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
stream.on('data', (chunk) => chunks.push(chunk));
|
||||
stream.on('error', reject);
|
||||
stream.on('end', () => resolve(Buffer.concat(chunks)));
|
||||
});
|
||||
}
|
||||
|
||||
async function saveEmail(rawEmail) {
|
||||
async function saveEmail(stream) {
|
||||
try {
|
||||
const parsed = await simpleParser(rawEmail);
|
||||
// First, buffer the entire stream
|
||||
const emailBuffer = await streamToBuffer(stream);
|
||||
|
||||
// Now, parse the buffered email content
|
||||
const parsed = await simpleParser(emailBuffer);
|
||||
const rawEmail = emailBuffer.toString();
|
||||
|
||||
// Ensure 'to' and 'from' objects exist before accessing 'text'
|
||||
const recipient = parsed.to ? parsed.to.text : 'undisclosed-recipients';
|
||||
const sender = parsed.from ? parsed.from.text : 'unknown-sender';
|
||||
const subject = parsed.subject;
|
||||
@@ -21,7 +30,7 @@ async function saveEmail(rawEmail) {
|
||||
[recipient, sender, subject, body, rawEmail]
|
||||
);
|
||||
|
||||
console.log(`Email saved with ID: ${result.insertId}`);
|
||||
console.log(`Email from <${sender}> to <${recipient}> saved with ID: ${result.insertId}`);
|
||||
|
||||
if (parsed.attachments && parsed.attachments.length > 0) {
|
||||
for (const attachment of parsed.attachments) {
|
||||
@@ -34,21 +43,11 @@ async function saveEmail(rawEmail) {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save email:', error);
|
||||
// Exit with an error code to signal failure to the calling process (e.g., Mailu)
|
||||
process.exit(1);
|
||||
// We should not exit the process here, but maybe throw the error
|
||||
// so the caller (SMTPServer) can handle it.
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const rawEmail = await getStream(process.stdin);
|
||||
if (rawEmail && rawEmail.length > 0) {
|
||||
await saveEmail(rawEmail);
|
||||
} else {
|
||||
console.log('Received empty input, no email to save.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error reading from stdin:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
module.exports = { saveEmail };
|
||||
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
# docker-compose.full.yml
|
||||
# 最终合并版本:包含您的应用 (frontend, backend) 和完整的 Mailu 服务套件。
|
||||
|
||||
version: '3.3'
|
||||
|
||||
services:
|
||||
|
||||
# -----------------------------------------
|
||||
# 您的应用服务
|
||||
# -----------------------------------------
|
||||
|
||||
backend:
|
||||
build: ./backend
|
||||
container_name: mail_backend
|
||||
restart: always
|
||||
environment:
|
||||
DB_HOST: "43.143.145.172"
|
||||
DB_USER: "root"
|
||||
DB_PASSWORD: "kyff145972"
|
||||
DB_NAME: "maildb"
|
||||
ports:
|
||||
- "5182:5182"
|
||||
|
||||
frontend:
|
||||
build: ./frontend
|
||||
container_name: mail_frontend
|
||||
restart: always
|
||||
ports:
|
||||
- "5181:8080"
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
# -----------------------------------------
|
||||
# Mailu 官方服务套件
|
||||
# -----------------------------------------
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
restart: always
|
||||
volumes:
|
||||
- "mailu_redis:/data"
|
||||
|
||||
front:
|
||||
image: ghcr.io/mailu/nginx:2.0
|
||||
restart: always
|
||||
env_file: mailu.env
|
||||
hostname: mail.shenjianl.cn
|
||||
volumes:
|
||||
- "mailu_certs:/certs"
|
||||
- "mailu_overrides_nginx:/overrides"
|
||||
|
||||
resolver:
|
||||
image: ghcr.io/mailu/unbound:2.0
|
||||
restart: always
|
||||
env_file: mailu.env
|
||||
hostname: mail.shenjianl.cn
|
||||
dns:
|
||||
- 8.8.8.8
|
||||
- 1.1.1.1
|
||||
# --- 用于调试的临时修改 ---
|
||||
entrypoint: /bin/sh
|
||||
command: -c "sleep 3600"
|
||||
|
||||
admin:
|
||||
image: ghcr.io/mailu/admin:2.0
|
||||
restart: always
|
||||
env_file: mailu.env
|
||||
hostname: mail.shenjianl.cn
|
||||
volumes:
|
||||
- "mailu_data:/data"
|
||||
- "mailu_dkim:/dkim"
|
||||
depends_on:
|
||||
- redis
|
||||
|
||||
imap:
|
||||
image: ghcr.io/mailu/dovecot:2.0
|
||||
restart: always
|
||||
env_file: mailu.env
|
||||
hostname: mail.shenjianl.cn
|
||||
volumes:
|
||||
- "mailu_mail:/mail"
|
||||
- "mailu_overrides_dovecot:/overrides"
|
||||
depends_on:
|
||||
- front
|
||||
|
||||
smtp:
|
||||
image: ghcr.io/mailu/postfix:2.0
|
||||
restart: always
|
||||
env_file: mailu.env
|
||||
hostname: mail.shenjianl.cn
|
||||
volumes:
|
||||
- "mailu_overrides_postfix:/overrides"
|
||||
depends_on:
|
||||
- front
|
||||
- resolver
|
||||
|
||||
antispam:
|
||||
image: ghcr.io/mailu/rspamd:2.0
|
||||
restart: always
|
||||
env_file: mailu.env
|
||||
hostname: mail.shenjianl.cn
|
||||
volumes:
|
||||
- "mailu_filter:/var/lib/rspamd"
|
||||
- "mailu_overrides_rspamd:/etc/rspamd/override.d"
|
||||
depends_on:
|
||||
- front
|
||||
|
||||
volumes:
|
||||
mailu_data:
|
||||
mailu_certs:
|
||||
mailu_dkim:
|
||||
mailu_filter:
|
||||
mailu_mail:
|
||||
mailu_redis:
|
||||
mailu_overrides_nginx:
|
||||
mailu_overrides_dovecot:
|
||||
mailu_overrides_postfix:
|
||||
mailu_overrides_rspamd:
|
||||
@@ -1,115 +1,21 @@
|
||||
# docker-compose.full.yml
|
||||
# 最终合并版本:包含您的应用 (frontend, backend) 和完整的 Mailu 服务套件。
|
||||
|
||||
version: '3.3'
|
||||
|
||||
services:
|
||||
|
||||
# -----------------------------------------
|
||||
# 您的应用服务
|
||||
# -----------------------------------------
|
||||
|
||||
backend:
|
||||
build: ./backend
|
||||
container_name: mail_backend
|
||||
container_name: email-backend-container
|
||||
restart: always
|
||||
ports:
|
||||
- "5182:5182" # API port
|
||||
- "25:25" # SMTP port
|
||||
environment:
|
||||
DB_HOST: "43.143.145.172"
|
||||
DB_USER: "root"
|
||||
DB_PASSWORD: "kyff145972"
|
||||
DB_NAME: "maildb"
|
||||
ports:
|
||||
- "5182:5182"
|
||||
- DB_HOST=43.143.145.172 # 替换为您的外部数据库主机名或IP
|
||||
- DB_USER=root # 替换为您的数据库用户名
|
||||
- DB_PASSWORD=kyff145972 # 替换为您的数据库密码
|
||||
- DB_NAME=maildb # 替换为您的数据库名称
|
||||
networks:
|
||||
- email-network
|
||||
|
||||
frontend:
|
||||
build: ./frontend
|
||||
container_name: mail_frontend
|
||||
restart: always
|
||||
ports:
|
||||
- "5181:8080"
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
# -----------------------------------------
|
||||
# Mailu 官方服务套件
|
||||
# -----------------------------------------
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
restart: always
|
||||
volumes:
|
||||
- "mailu_redis:/data"
|
||||
|
||||
front:
|
||||
image: ghcr.io/mailu/nginx:2.0
|
||||
restart: always
|
||||
env_file: mailu.env
|
||||
hostname: mail.shenjianl.cn
|
||||
volumes:
|
||||
- "mailu_certs:/certs"
|
||||
- "mailu_overrides_nginx:/overrides"
|
||||
|
||||
resolver:
|
||||
image: ghcr.io/mailu/unbound:2.0
|
||||
restart: always
|
||||
env_file: mailu.env
|
||||
hostname: mail.shenjianl.cn
|
||||
dns:
|
||||
- 8.8.8.8
|
||||
- 1.1.1.1
|
||||
|
||||
admin:
|
||||
image: ghcr.io/mailu/admin:2.0
|
||||
restart: always
|
||||
env_file: mailu.env
|
||||
hostname: mail.shenjianl.cn
|
||||
volumes:
|
||||
- "mailu_data:/data"
|
||||
- "mailu_dkim:/dkim"
|
||||
depends_on:
|
||||
- redis
|
||||
|
||||
imap:
|
||||
image: ghcr.io/mailu/dovecot:2.0
|
||||
restart: always
|
||||
env_file: mailu.env
|
||||
hostname: mail.shenjianl.cn
|
||||
volumes:
|
||||
- "mailu_mail:/mail"
|
||||
- "mailu_overrides_dovecot:/overrides"
|
||||
depends_on:
|
||||
- front
|
||||
|
||||
smtp:
|
||||
image: ghcr.io/mailu/postfix:2.0
|
||||
restart: always
|
||||
env_file: mailu.env
|
||||
hostname: mail.shenjianl.cn
|
||||
volumes:
|
||||
- "mailu_overrides_postfix:/overrides"
|
||||
depends_on:
|
||||
- front
|
||||
- resolver
|
||||
|
||||
antispam:
|
||||
image: ghcr.io/mailu/rspamd:2.0
|
||||
restart: always
|
||||
env_file: mailu.env
|
||||
hostname: mail.shenjianl.cn
|
||||
volumes:
|
||||
- "mailu_filter:/var/lib/rspamd"
|
||||
- "mailu_overrides_rspamd:/etc/rspamd/override.d"
|
||||
depends_on:
|
||||
- front
|
||||
|
||||
volumes:
|
||||
mailu_data:
|
||||
mailu_certs:
|
||||
mailu_dkim:
|
||||
mailu_filter:
|
||||
mailu_mail:
|
||||
mailu_redis:
|
||||
mailu_overrides_nginx:
|
||||
mailu_overrides_dovecot:
|
||||
mailu_overrides_postfix:
|
||||
mailu_overrides_rspamd:
|
||||
networks:
|
||||
email-network:
|
||||
driver: bridge
|
||||
@@ -1,257 +1,158 @@
|
||||
<template>
|
||||
<div id="app-container">
|
||||
<h1>临时邮箱</h1>
|
||||
<div class="email-generator">
|
||||
<input v-model="randomEmail" type="text" readonly @click="copyToClipboard" title="点击复制"/>
|
||||
<button @click="generateRandomEmail">生成新地址</button>
|
||||
</div>
|
||||
<header class="app-header">
|
||||
<div class="logo">Email Unlimit</div>
|
||||
<nav class="nav-links">
|
||||
<a href="https://gitea.shenjianl.cn/shenjianZ/email-unlimit" target="_blank">Gitee</a>
|
||||
<a href="#" @click.prevent="showHowItWorks">How it Works</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="inbox">
|
||||
<h2>收件箱 ({{ currentEmail }})</h2>
|
||||
<button @click="fetchMessages" :disabled="loading">
|
||||
{{ loading ? '刷新中...' : '刷新邮件' }}
|
||||
<main id="app-main">
|
||||
<section class="hero-section">
|
||||
<h1>您的专属临时邮箱,无限且私密</h1>
|
||||
<p>输入任何<code>@shenjianl.cn</code>地址,立即在此查看收件箱。</p>
|
||||
<form @submit.prevent="fetchMessages" class="input-group">
|
||||
<input
|
||||
type="email"
|
||||
v-model="recipient"
|
||||
class="email-input"
|
||||
placeholder="输入您的临时邮箱地址..."
|
||||
required
|
||||
/>
|
||||
<button type="submit" class="btn btn-primary">查看收件箱</button>
|
||||
<button @click="generateRandomEmail" type="button" class="btn btn-secondary">随机生成</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="inbox-section">
|
||||
<div class="inbox-container">
|
||||
<div class="message-list">
|
||||
<div class="message-list-header">
|
||||
<h2>收件箱</h2>
|
||||
<button @click="fetchMessages" class="refresh-btn" title="刷新">
|
||||
↻
|
||||
</button>
|
||||
<div v-if="error" class="error">{{ error }}</div>
|
||||
<ul v-if="messages.length > 0" class="message-list">
|
||||
<li v-for="msg in messages" :key="msg.id" @click="selectMessage(msg)" :class="{ selected: selectedMessage && selectedMessage.id === msg.id }">
|
||||
<div class="sender"><strong>发件人:</strong> {{ msg.sender }}</div>
|
||||
<div class="subject"><strong>主题:</strong> {{ msg.subject }}</div>
|
||||
<div class="time">{{ new Date(msg.received_at).toLocaleString() }}</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else class="no-messages">
|
||||
<p>{{ loading ? '正在加载...' : '收件箱是空的。' }}</p>
|
||||
</div>
|
||||
<div v-if="loading" class="loading-state">正在加载...</div>
|
||||
<div v-else-if="messages.length === 0" class="empty-state">暂无邮件</div>
|
||||
<div v-else>
|
||||
<div
|
||||
v-for="message in messages"
|
||||
:key="message.id"
|
||||
class="message-item"
|
||||
:class="{ selected: selectedMessage && selectedMessage.id === message.id }"
|
||||
@click="selectMessage(message)"
|
||||
>
|
||||
<div class="from">{{ message.sender }}</div>
|
||||
<div class="subject">{{ message.subject }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedMessage" class="message-view">
|
||||
</div>
|
||||
<div class="message-detail">
|
||||
<div v-if="!selectedMessage" class="empty-state">
|
||||
<p>请从左侧选择一封邮件查看</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="message-content-header">
|
||||
<h3>{{ selectedMessage.subject }}</h3>
|
||||
<p><strong>发件人:</strong> {{ selectedMessage.sender }}</p>
|
||||
<p><strong>收件人:</strong> {{ currentEmail }}</p>
|
||||
<p><strong>时间:</strong> {{ new Date(selectedMessage.received_at).toLocaleString() }}</p>
|
||||
<hr>
|
||||
<div v-html="selectedMessage.body" class="email-body"></div>
|
||||
<p><strong>收件人:</strong> {{ selectedMessage.recipient }}</p>
|
||||
</div>
|
||||
<div class="message-body">
|
||||
{{ selectedMessage.body }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- How it Works Modal -->
|
||||
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
|
||||
<div class="modal-content">
|
||||
<button @click="closeModal" class="close-btn">×</button>
|
||||
<h3>工作原理</h3>
|
||||
<ol>
|
||||
<li>在上面的输入框中,随意编造一个以<code>@{{ domain }}</code>结尾的邮箱地址。</li>
|
||||
<li>使用这个地址去注册任何网站或接收邮件。</li>
|
||||
<li>在这里输入您刚刚使用的地址,点击“查看收件箱”,即可看到邮件。</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
|
||||
const API_URL = '/api';
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
data() {
|
||||
return {
|
||||
randomEmail: '',
|
||||
currentEmail: '',
|
||||
messages: [],
|
||||
selectedMessage: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
domain: 'shenjianl.cn', // 从 info.md 获取
|
||||
refreshInterval: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
generateRandomString(length) {
|
||||
const characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
}
|
||||
return result;
|
||||
},
|
||||
generateRandomEmail() {
|
||||
const name = this.generateRandomString(8);
|
||||
this.randomEmail = `${name}@${this.domain}`;
|
||||
this.currentEmail = this.randomEmail;
|
||||
this.messages = [];
|
||||
this.selectedMessage = null;
|
||||
this.fetchMessages();
|
||||
},
|
||||
async fetchMessages() {
|
||||
if (!this.currentEmail) {
|
||||
setup() {
|
||||
const recipient = ref('');
|
||||
const messages = ref([]);
|
||||
const selectedMessage = ref(null);
|
||||
const loading = ref(false);
|
||||
const showModal = ref(false);
|
||||
// !!! 生产环境<E78EAF><E5A283>要提示 !!!
|
||||
// 请务必将下面的 'yourdomain.com' 替换为您的真实域名
|
||||
const domain = 'shenjianl.cn';
|
||||
|
||||
const fetchMessages = async () => {
|
||||
if (!recipient.value) {
|
||||
alert('请输入一个邮箱地址');
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
loading.value = true;
|
||||
selectedMessage.value = null; // Clear selected message on new fetch
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/messages`, {
|
||||
params: { recipient: this.currentEmail }
|
||||
});
|
||||
this.messages = response.data;
|
||||
// 如果有新邮件,且当前选中了邮件,则更新选中的邮件内容
|
||||
if (this.selectedMessage) {
|
||||
const updatedSelected = this.messages.find(m => m.id === this.selectedMessage.id);
|
||||
if (updatedSelected) {
|
||||
this.selectedMessage = updatedSelected;
|
||||
} else {
|
||||
this.selectedMessage = null; // 如果邮件被删除,则取消选中
|
||||
// API URL 已修改为相对路径,以适配 Nginx 反向代理
|
||||
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;
|
||||
// Automatically select the first message if available
|
||||
if (messages.value.length > 0) {
|
||||
selectedMessage.value = messages.value[0];
|
||||
}
|
||||
} catch (err) {
|
||||
this.error = '无法加载邮件。请检查后端服务是否运行。';
|
||||
console.error(err);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch messages:', error);
|
||||
alert('无法获取邮件,请检查后端服务和 Nginx 配置是否正常。');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
selectMessage(message) {
|
||||
this.selectedMessage = message;
|
||||
},
|
||||
copyToClipboard() {
|
||||
navigator.clipboard.writeText(this.randomEmail).then(() => {
|
||||
alert('邮箱地址已复制到剪贴板');
|
||||
}, (err) => {
|
||||
console.error('Could not copy text: ', err);
|
||||
});
|
||||
},
|
||||
startAutoRefresh() {
|
||||
this.refreshInterval = setInterval(this.fetchMessages, 15000); // 每15秒刷新一次
|
||||
},
|
||||
stopAutoRefresh() {
|
||||
if (this.refreshInterval) {
|
||||
clearInterval(this.refreshInterval);
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.generateRandomEmail();
|
||||
this.startAutoRefresh();
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.stopAutoRefresh();
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const selectMessage = (message) => {
|
||||
selectedMessage.value = message;
|
||||
};
|
||||
|
||||
const generateRandomEmail = () => {
|
||||
const randomPart = Math.random().toString(36).substring(2, 10);
|
||||
recipient.value = `${randomPart}@${domain}`;
|
||||
};
|
||||
|
||||
const showHowItWorks = () => {
|
||||
showModal.value = true;
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
showModal.value = false;
|
||||
};
|
||||
|
||||
return {
|
||||
recipient,
|
||||
messages,
|
||||
selectedMessage,
|
||||
loading,
|
||||
showModal,
|
||||
domain,
|
||||
fetchMessages,
|
||||
selectMessage,
|
||||
generateRandomEmail,
|
||||
showHowItWorks,
|
||||
closeModal,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
background-color: #f4f7f9;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#app-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.email-generator {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.email-generator input {
|
||||
flex-grow: 1;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
background: #f9f9f9;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 15px;
|
||||
border: none;
|
||||
background-color: #42b983;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #36a374;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.inbox {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.message-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.message-list li {
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #eee;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.message-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.message-list li:hover {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.message-list li.selected {
|
||||
background-color: #e8f5e9;
|
||||
}
|
||||
|
||||
.sender, .subject, .time {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 0.8em;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.no-messages {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.message-view {
|
||||
border: 1px solid #eee;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
background: #fdfdfd;
|
||||
}
|
||||
|
||||
.email-body {
|
||||
margin-top: 15px;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
background-color: #fff;
|
||||
padding: 1em;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #e74c3c;
|
||||
margin: 10px 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
313
frontend/src/assets/main.css
Normal file
313
frontend/src/assets/main.css
Normal file
@@ -0,0 +1,313 @@
|
||||
/* Global Styles & Resets */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap');
|
||||
|
||||
:root {
|
||||
--primary-purple: #6d28d9;
|
||||
--light-purple: #8b5cf6;
|
||||
--dark-purple: #5b21b6;
|
||||
--light-grey: #f3f4f6;
|
||||
--medium-grey: #e5e7eb;
|
||||
--dark-grey: #4b5563;
|
||||
--text-light: #ffffff;
|
||||
--text-dark: #1f2937;
|
||||
--border-radius: 12px;
|
||||
--card-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: #ffffff;
|
||||
color: var(--text-dark);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* App Container */
|
||||
#app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0 2rem;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.app-header {
|
||||
width: 100%;
|
||||
max-width: 1400px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
margin-left: 2rem;
|
||||
text-decoration: none;
|
||||
color: var(--dark-grey);
|
||||
font-weight: 500;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-links a:hover {
|
||||
color: var(--primary-purple);
|
||||
}
|
||||
|
||||
/* Hero Section */
|
||||
.hero-section {
|
||||
width: 100%;
|
||||
max-width: 1400px;
|
||||
background-color: var(--primary-purple);
|
||||
color: var(--text-light);
|
||||
padding: 4rem 2rem;
|
||||
border-radius: var(--border-radius);
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.hero-section h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.hero-section p {
|
||||
font-size: 1.125rem;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.email-input {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.email-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--light-purple);
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--light-purple);
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--dark-purple);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* Inbox Section */
|
||||
.inbox-section {
|
||||
width: 100%;
|
||||
max-width: 1400px;
|
||||
min-width: 1200px;
|
||||
background-color: var(--light-grey);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--card-shadow);
|
||||
}
|
||||
|
||||
.inbox-container {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.message-list {
|
||||
width: 35%;
|
||||
border-right: 1px solid var(--medium-grey);
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
|
||||
.message-list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.message-list-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.5rem;
|
||||
color: var(--dark-grey);
|
||||
}
|
||||
|
||||
.message-item {
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
border-left: 4px solid transparent;
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.message-item:hover {
|
||||
background-color: var(--medium-grey);
|
||||
}
|
||||
|
||||
.message-item.selected {
|
||||
background-color: #ffffff;
|
||||
border-left-color: var(--primary-purple);
|
||||
}
|
||||
|
||||
.message-item .from {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.message-item .subject {
|
||||
color: var(--dark-grey);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.message-detail {
|
||||
width: 65%;
|
||||
}
|
||||
|
||||
.empty-state, .loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
color: var(--dark-grey);
|
||||
}
|
||||
|
||||
.message-content-header {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.message-content-header h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.message-content-header p {
|
||||
margin: 0;
|
||||
color: var(--dark-grey);
|
||||
}
|
||||
|
||||
.message-body {
|
||||
background-color: #ffffff;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #ffffff;
|
||||
padding: 2rem;
|
||||
border-radius: var(--border-radius);
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
box-shadow: 0 10px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modal-content h3 {
|
||||
margin-top: 0;
|
||||
color: var(--primary-purple);
|
||||
}
|
||||
|
||||
.modal-content ol {
|
||||
padding-left: 1.5rem;
|
||||
line-height: 1.8;
|
||||
font-size: 1.05rem;
|
||||
color: var(--dark-grey);
|
||||
}
|
||||
|
||||
.modal-content code {
|
||||
background-color: var(--light-purple);
|
||||
color: var(--text-light);
|
||||
padding: 0.3em 0.6em;
|
||||
border-radius: 6px;
|
||||
font-family: monospace;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.75rem;
|
||||
cursor: pointer;
|
||||
color: var(--dark-grey);
|
||||
}
|
||||
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.inbox-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
.message-list, .message-detail {
|
||||
width: 100%;
|
||||
border-right: none;
|
||||
padding-right: 0;
|
||||
}
|
||||
.input-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import './assets/main.css' // 引入新的全局样式文件
|
||||
|
||||
createApp(App).mount('#app')
|
||||
26
mailu.env
26
mailu.env
@@ -1,26 +0,0 @@
|
||||
SECRET_KEY=d4f7a1b3c2e987f05a6d3b4c8e1f2097
|
||||
DOMAIN=shenjianl.cn
|
||||
HOSTNAMES=mail.shenjianl.cn
|
||||
POSTMASTER=admin
|
||||
|
||||
DB_FLAVOR=mysql
|
||||
DB_HOST=43.143.145.172
|
||||
DB_USER=root
|
||||
DB_PASSWORD=kyff145972
|
||||
DB_NAME=maildb
|
||||
|
||||
WEB_ADMIN=/mailu
|
||||
WEB_WEBMAIL=/mailu/webmail
|
||||
|
||||
ENABLE_UNPRIVILEGED_USER=false
|
||||
PASSWORD_SCHEME=BLF-CRYPT
|
||||
ENABLE_ADMIN=true
|
||||
ENABLE_WEBMAIL=true
|
||||
ENABLE_WEBDAV=false
|
||||
ENABLE_ANTIVIRUS=false
|
||||
ENABLE_ANTISPAM=true
|
||||
DEFAULT_QUOTA=2G
|
||||
ADMIN_PASSWORD=admin
|
||||
|
||||
TLS_FLAVOR=notls
|
||||
MESSAGE_SIZE_LIMIT=20000000
|
||||
Reference in New Issue
Block a user