Merge pull request #24 from gujiangjiang/main
feat: 添加Github Action实现自动化部署
This commit is contained in:
12 files changed
+428
-271
No files matched your search
@@ -0,0 +1,98 @@
|
||||
# 工作流的名称,会显示在 GitHub Actions 页面
|
||||
name: Deploy to Cloudflare
|
||||
|
||||
# 'on' 关键字定义了触发此工作流的事件
|
||||
on:
|
||||
# 当有代码推送到 'main' 分支时,自动触发此工作流
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
# 允许你在 GitHub Actions 页面手动点击按钮来触发此工作流
|
||||
workflow_dispatch:
|
||||
|
||||
# 'jobs' 关键字定义了工作流中要执行的一系列任务
|
||||
jobs:
|
||||
# 定义一个名为 'deploy' 的任务
|
||||
deploy:
|
||||
# 指定此任务运行在最新版的 Ubuntu 虚拟环境中
|
||||
runs-on: ubuntu-latest
|
||||
# 此任务在 GitHub UI 中显示的名称
|
||||
name: Deploy
|
||||
# 'steps' 关键字定义了任务中要按顺序执行的一系列步骤
|
||||
steps:
|
||||
# 第一步:检出代码
|
||||
# 使用官方的 'checkout' action 来获取你的仓库代码
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# 第二步:设置 pnpm 包管理器
|
||||
# 使用 pnpm 官方的 'action-setup' 来安装和配置 pnpm
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
# 注意:此处已移除 "version: 8" 行,以允许 action 自动从 package.json 的 "packageManager" 字段读取版本
|
||||
|
||||
# 第三步:设置 Node.js 环境
|
||||
# 使用官方的 'setup-node' action 来安装指定版本的 Node.js
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
# 指定 Node.js 的版本
|
||||
node-version: '20'
|
||||
# 为 pnpm 配置依赖缓存,以加快后续运行速度
|
||||
cache: 'pnpm'
|
||||
|
||||
# 第四步:安装项目依赖
|
||||
# 执行 'pnpm install' 命令来安装所有在 'package.json' 和 'pnpm-lock.yaml' 中定义的依赖
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
# 第五步:手动安装 Wrangler
|
||||
# 由于项目使用 pnpm workspace,我们需要手动安装 wrangler 并使用 '-w' 标志
|
||||
# 这可以避免 wrangler-action 自动安装时可能出现的冲突
|
||||
- name: Install Wrangler
|
||||
run: pnpm add wrangler@4 -w
|
||||
|
||||
# 第六步:构建项目
|
||||
# 执行在 'package.json' 中定义的 'build' 脚本,通常用于编译前端和后端代码
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
|
||||
# 第七步:配置 Wrangler 的 toml 文件
|
||||
# 这是关键的一步,在部署前动态地将占位符替换为真实的 secrets 值
|
||||
- name: Configure Wrangler
|
||||
run: |
|
||||
# 使用 sed 命令在 wrangler.toml 文件中查找并替换 D1 数据库 ID 的占位符
|
||||
sed -i "s/\${D1_DATABASE_ID}/${D1_DATABASE_ID}/g" wrangler.toml
|
||||
# 使用 sed 命令在 wrangler.toml 文件中查找并替换 D1 数据库名称的占位符
|
||||
sed -i "s/\${D1_DATABASE_NAME}/${D1_DATABASE_NAME}/g" wrangler.toml
|
||||
# 使用 sed 命令在 wrangler.toml 文件中查找并替换邮箱域名的占位符
|
||||
sed -i "s/\${VITE_EMAIL_DOMAIN}/${VITE_EMAIL_DOMAIN}/g" wrangler.toml
|
||||
env:
|
||||
# 从 GitHub Secrets 中读取 D1 数据库 ID 和名称,并将其作为环境变量注入到此步骤中
|
||||
D1_DATABASE_ID: ${{ secrets.D1_DATABASE_ID }}
|
||||
D1_DATABASE_NAME: ${{ secrets.D1_DATABASE_NAME }}
|
||||
VITE_EMAIL_DOMAIN: ${{ secrets.VITE_EMAIL_DOMAIN }}
|
||||
|
||||
# 新增步骤:检查 Cloudflare 密钥是否存在
|
||||
- name: Check for Cloudflare credentials
|
||||
id: cf_creds
|
||||
run: |
|
||||
if [ -z "${{ secrets.CF_API_TOKEN }}" ] || [ -z "${{ secrets.CF_ACCOUNT_ID }}" ]; then
|
||||
echo "Cloudflare credentials not found, skipping deployment."
|
||||
echo "has_creds=false" >> $GITHUB_ENV
|
||||
else
|
||||
echo "has_creds=true" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
# 第八步:部署到 Cloudflare
|
||||
# 仅在检测到密钥存在时执行
|
||||
- name: Deploy to Cloudflare
|
||||
if: env.has_creds == 'true'
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
# 传入 API 令牌用于身份验证
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
# 传入 Cloudflare 账户 ID
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
# 明确指定要执行的命令是 'deploy'
|
||||
command: deploy
|
||||
+14
-15
@@ -121,7 +121,7 @@ We provide two deployment methods, you can choose according to your needs:
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
#### Option 2: Fork and Custom Deployment (Recommended for Advanced Users)
|
||||
#### Option 2: Fork and Custom Deployment via Github Action (Recommended for Advanced Users)
|
||||
|
||||
<div style="background-color: #2d2d2d; color: #ffffff; padding: 15px; border-radius: 5px; margin: 15px 0;">
|
||||
<h4>✅ Advantages:</h4>
|
||||
@@ -129,33 +129,32 @@ We provide two deployment methods, you can choose according to your needs:
|
||||
<li>Can receive subsequent code updates</li>
|
||||
<li>Fully customizable configuration</li>
|
||||
<li>Better version control</li>
|
||||
<li>Automatic deployment via GitHub Actions, more secure and convenient</li>
|
||||
</ul>
|
||||
|
||||
<h4>❌ Disadvantages:</h4>
|
||||
<ul>
|
||||
<li>Need to manually modify configuration files</li>
|
||||
<li>Requires some technical knowledge</li>
|
||||
<li>Need to manually create database</li>
|
||||
<li>Need to manually create database and configure secrets</li>
|
||||
</ul>
|
||||
|
||||
<h4>📋 Deployment Steps:</h4>
|
||||
<ol>
|
||||
<li>Fork this project to your GitHub account</li>
|
||||
<li>Create a D1 database in Cloudflare Dashboard, remember the database name and id</li>
|
||||
<li>Modify the <code>wrangler.toml</code> file in the root directory:
|
||||
<li>Create a D1 database in your Cloudflare Dashboard and note down the <strong>database_name</strong> and <strong>database_id</strong></li>
|
||||
<li>In your GitHub repository, go to <strong>Settings</strong> > <strong>Secrets and variables</strong> > <strong>Actions</strong></li>
|
||||
<li>Click <strong>New repository secret</strong> and add the following five secrets:
|
||||
<ul>
|
||||
<li>Modify <code>name</code> to your application name (optional)</li>
|
||||
<li>Modify <code>routes</code> with your custom domain (required! Default worker.dev domain is not accessible in mainland China)</li>
|
||||
<li>Modify <code>database_name</code> and <code>database_id</code> to your D1 database information (required!)</li>
|
||||
<li>Modify <code>VITE_EMAIL_DOMAIN</code> to your domain list (optional, just for convenient copy-paste of email addresses on the webpage, doesn't affect email reception)</li>
|
||||
<li><code>CF_API_TOKEN</code>: Your Cloudflare API Token. You can create one <a href="https://dash.cloudflare.com/profile/api-tokens" target="_blank">here</a> using the "Edit Cloudflare Workers" template.</li>
|
||||
<li><code>CF_ACCOUNT_ID</code>: Your Cloudflare Account ID. You can find it on the right side of the Workers page.</li>
|
||||
<li><code>D1_DATABASE_ID</code>: The ID of the D1 database you created in step 2.</li>
|
||||
<li><code>D1_DATABASE_NAME</code>: The name of the D1 database you created in step 2.</li>
|
||||
<li><code>VITE_EMAIL_DOMAIN</code>: Your list of domains, separated by commas (e.g., example.com,test.com).</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>In Cloudflare Dashboard, select "Workers & Pages"</li>
|
||||
<li>Click "Create application" -> "Pages" -> "Connect to Git"</li>
|
||||
<li>Select your forked repository</li>
|
||||
<li>Configure build settings (usually auto-detected)</li>
|
||||
<li>Click "Save and Deploy"</li>
|
||||
<li>Configure Cloudflare Email routing to forward emails to your Worker</li>
|
||||
<li>After completing the steps above, the project will be automatically deployed on every push to the <code>main</code> branch. You can also trigger the deployment manually from the Actions page.</li>
|
||||
<li>After deployment, bind a custom domain to your Worker.</li>
|
||||
<li>Finally, configure Cloudflare Email Routing to forward emails to your Worker.</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ ZMAIL 现在采用全新的一体化部署方式,前端和后端整合为一
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
#### 方式二:Fork 后自定义部署(推荐进阶用户)
|
||||
#### 方式二:Fork 后通过 Github Action 自定义部署(推荐进阶用户)
|
||||
|
||||
<div style="background-color: #2d2d2d; color: #ffffff; padding: 15px; border-radius: 5px; margin: 15px 0;">
|
||||
<h4>✅ 优点:</h4>
|
||||
@@ -127,33 +127,32 @@ ZMAIL 现在采用全新的一体化部署方式,前端和后端整合为一
|
||||
<li>可以获得后续代码更新</li>
|
||||
<li>完全自定义配置</li>
|
||||
<li>更好的版本控制</li>
|
||||
<li>通过 GitHub Action 自动部署,更加安全便捷</li>
|
||||
</ul>
|
||||
|
||||
<h4>❌ 缺点:</h4>
|
||||
<ul>
|
||||
<li>需要手动修改配置文件</li>
|
||||
<li>需要一定的技术基础</li>
|
||||
<li>需要手动创建数据库</li>
|
||||
<li>需要手动创建数据库和配置密钥</li>
|
||||
</ul>
|
||||
|
||||
<h4>📋 部署步骤:</h4>
|
||||
<ol>
|
||||
<li>Fork 本项目到您的 GitHub 账户</li>
|
||||
<li>在 Cloudflare Dashboard 中创建一个 D1 数据库,记住数据库的 name 和 id</li>
|
||||
<li>修改根目录下的 <code>wrangler.toml</code> 文件:
|
||||
<li>在 Cloudflare Dashboard 中创建一个 D1 数据库,并记录下数据库的 <strong>database_name</strong> 和 <strong>database_id</strong></li>
|
||||
<li>在您的 GitHub 仓库中, 前往 <strong>Settings</strong> > <strong>Secrets and variables</strong> > <strong>Actions</strong></li>
|
||||
<li>点击 <strong>New repository secret</strong> 并添加以下五个密钥:
|
||||
<ul>
|
||||
<li>修改 <code>name</code> 为您的应用名称。(可选)</li>
|
||||
<li>修改 <code>routes</code> 中的自定义域名。(必须!默认的worker.dev域名在中国大陆无法访问)</li>
|
||||
<li>修改 <code>database_name</code> 和 <code>database_id</code> 为您的 D1 数据库信息。(必须!)</li>
|
||||
<li>修改 <code>VITE_EMAIL_DOMAIN</code> 为您的域名列表。(可选,只是方便网页上复制粘贴邮件地址使用,不设置也不影响接收邮件)</li>
|
||||
<li><code>CF_API_TOKEN</code>: 你的 Cloudflare API Token。你可以在 <a href="https://dash.cloudflare.com/profile/api-tokens" target="_blank">这里</a> 创建,使用 "Edit Cloudflare Workers" 模板即可。</li>
|
||||
<li><code>CF_ACCOUNT_ID</code>: 你的 Cloudflare 账户 ID。你可以在 Workers 页面的右侧找到。</li>
|
||||
<li><code>D1_DATABASE_ID</code>: 你在第二步中创建的 D1 数据库的 ID。</li>
|
||||
<li><code>D1_DATABASE_NAME</code>: 你在第二步中创建的 D1 数据库的名称。</li>
|
||||
<li><code>VITE_EMAIL_DOMAIN</code>: 你的域名列表,多个域名用逗号 ',' 分割 (例如: example.com,test.com)。</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>在 Cloudflare Dashboard 中选择 "Workers & Pages"</li>
|
||||
<li>点击 "Create application" -> "Pages" -> "Connect to Git"</li>
|
||||
<li>选择您 Fork 的仓库</li>
|
||||
<li>配置构建设置(通常会自动检测)</li>
|
||||
<li>点击 "Save and Deploy"</li>
|
||||
<li>配置 Cloudflare Email 路由,将邮件转发到您的 Worker</li>
|
||||
<li>完成以上步骤后,项目将在每次推送到 <code>main</code> 分支时自动部署。你也可以在 Actions 页面手动触发部署。</li>
|
||||
<li>部署完成后,为你的 Worker 绑定一个自定义域名。</li>
|
||||
<li>最后,配置 Cloudflare Email 路由,将邮件转发到你的 Worker。</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -57,7 +57,10 @@
|
||||
"notFound": "Mailbox not found or expired",
|
||||
"creatingNew": "Creating new mailbox...",
|
||||
"switch": "Switch Mailbox",
|
||||
"savedMailboxes": "Saved Mailboxes"
|
||||
"savedMailboxes": "Saved Mailboxes",
|
||||
"confirmDeleteMailbox": "Are you sure you want to delete this saved mailbox?",
|
||||
"confirmClearAllMailboxes": "Are you sure you want to clear all saved mailboxes? (The current mailbox will not be deleted)",
|
||||
"clearAll": "Clear All"
|
||||
},
|
||||
"email": {
|
||||
"inbox": "Inbox",
|
||||
@@ -90,7 +93,8 @@
|
||||
"messages": "messages"
|
||||
},
|
||||
"settings": {
|
||||
"language": "Language"
|
||||
"language": "Language",
|
||||
"toggleTheme": "Toggle Theme"
|
||||
},
|
||||
"errors": {
|
||||
"generic": "An error occurred, please try again later",
|
||||
|
||||
@@ -57,7 +57,10 @@
|
||||
"notFound": "邮箱不存在或已过期",
|
||||
"creatingNew": "正在创建新邮箱...",
|
||||
"switch": "切换邮箱",
|
||||
"savedMailboxes": "已保存的邮箱"
|
||||
"savedMailboxes": "已保存的邮箱",
|
||||
"confirmDeleteMailbox": "您确定要删除这个已保存的邮箱吗?",
|
||||
"confirmClearAllMailboxes": "您确定要清空所有已保存的邮箱吗?(当前使用的邮箱不会被删除)",
|
||||
"clearAll": "全部清除"
|
||||
},
|
||||
"email": {
|
||||
"inbox": "收件箱",
|
||||
@@ -90,7 +93,8 @@
|
||||
"messages": "封邮件"
|
||||
},
|
||||
"settings": {
|
||||
"language": "语言"
|
||||
"language": "语言",
|
||||
"toggleTheme": "切换主题"
|
||||
},
|
||||
"errors": {
|
||||
"generic": "发生错误,请稍后重试",
|
||||
|
||||
@@ -5,6 +5,7 @@ import LanguageSwitcher from './LanguageSwitcher';
|
||||
import HeaderMailbox from './HeaderMailbox';
|
||||
import Container from './Container';
|
||||
import { getEmailDomains, getDefaultEmailDomain, EMAIL_DOMAINS, DEFAULT_EMAIL_DOMAIN } from '../config';
|
||||
import ThemeSwitcher from './ThemeSwitcher'; // 导入新增的主题切换组件
|
||||
|
||||
interface HeaderProps {
|
||||
mailbox: Mailbox | null;
|
||||
@@ -56,6 +57,8 @@ const Header: React.FC<HeaderProps> = ({
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<div className="ml-3 pl-3 border-l border-muted-foreground/20 flex items-center">
|
||||
{/* 在这里添加主题切换组件 */}
|
||||
<ThemeSwitcher />
|
||||
<LanguageSwitcher />
|
||||
<a
|
||||
href="https://github.com/zaunist/zmail"
|
||||
|
||||
@@ -198,8 +198,10 @@ const HeaderMailbox: React.FC<HeaderMailboxProps> = ({
|
||||
};
|
||||
|
||||
// 切换域名
|
||||
const handleDomainChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const handleDomainChange = async (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setSelectedDomain(e.target.value);
|
||||
// [fix] 切换域名后自动更换邮箱
|
||||
await handleRefreshMailbox();
|
||||
};
|
||||
|
||||
// 按钮基础样式
|
||||
@@ -231,15 +233,21 @@ const HeaderMailbox: React.FC<HeaderMailboxProps> = ({
|
||||
/>
|
||||
<span className="flex items-center px-2 py-1 text-sm border-y border-r rounded-r-md bg-muted">
|
||||
@
|
||||
<select
|
||||
value={selectedDomain}
|
||||
onChange={handleDomainChange}
|
||||
className="bg-transparent border-none focus:outline-none"
|
||||
>
|
||||
{domains.map(d => (
|
||||
<option key={d} value={d}>{d}</option>
|
||||
))}
|
||||
</select>
|
||||
{/* [fix]: 为select包裹一个relative容器,用于绝对定位自定义箭头 */}
|
||||
<div className="relative">
|
||||
<select
|
||||
value={selectedDomain}
|
||||
onChange={handleDomainChange}
|
||||
// [fix]: 添加 appearance-none 移除原生样式,并增加padding-right为箭头留出空间
|
||||
className="appearance-none bg-transparent border-none focus:outline-none pl-1 pr-5"
|
||||
>
|
||||
{domains.map(d => (
|
||||
<option key={d} value={d}>{d}</option>
|
||||
))}
|
||||
</select>
|
||||
{/* [fix]: 添加自定义的下拉箭头图标 */}
|
||||
<i className="fas fa-chevron-down absolute right-0 top-1/2 -translate-y-1/2 text-xs text-muted-foreground pointer-events-none"></i>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
@@ -278,17 +286,23 @@ const HeaderMailbox: React.FC<HeaderMailboxProps> = ({
|
||||
<div className="flex items-center">
|
||||
{/* 电脑端邮箱地址显示 */}
|
||||
<div className="hidden sm:flex items-center">
|
||||
<code className="bg-muted px-2 py-1 rounded text-sm font-medium">
|
||||
<code className="bg-muted px-2 py-1 rounded text-sm font-medium flex items-center">
|
||||
{mailbox.address}@
|
||||
<select
|
||||
value={selectedDomain}
|
||||
onChange={handleDomainChange}
|
||||
className="bg-transparent border-none focus:outline-none px-0 py-0 font-medium"
|
||||
>
|
||||
{domains.map(d => (
|
||||
<option key={d} value={d}>{d}</option>
|
||||
))}
|
||||
</select>
|
||||
{/* [fix]: 为select包裹一个relative容器,用于绝对定位自定义箭头 */}
|
||||
<div className="relative">
|
||||
<select
|
||||
value={selectedDomain}
|
||||
onChange={handleDomainChange}
|
||||
// [fix]: 添加 appearance-none 移除原生样式,并增加padding-right为箭头留出空间
|
||||
className="appearance-none bg-transparent border-none focus:outline-none pl-1 pr-4 font-medium"
|
||||
>
|
||||
{domains.map(d => (
|
||||
<option key={d} value={d}>{d}</option>
|
||||
))}
|
||||
</select>
|
||||
{/* [fix]: 添加自定义的下拉箭头图标 */}
|
||||
<i className="fas fa-chevron-down absolute right-0 top-1/2 -translate-y-1/2 text-xs text-muted-foreground pointer-events-none"></i>
|
||||
</div>
|
||||
</code>
|
||||
|
||||
{/* 添加邮箱切换组件 */}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
// [feat]: 导入 deleteMailbox API 函数
|
||||
import { deleteMailbox as apiDeleteMailbox } from '../utils/api';
|
||||
|
||||
interface MailboxSwitcherProps {
|
||||
currentMailbox: Mailbox;
|
||||
@@ -103,6 +105,51 @@ const MailboxSwitcher: React.FC<MailboxSwitcherProps> = ({
|
||||
setShowDropdown(false);
|
||||
};
|
||||
|
||||
// 删除单个已保存的邮箱(包括后端)
|
||||
const handleDeleteMailbox = async (address: string) => {
|
||||
if (window.confirm(t('mailbox.confirmDeleteMailbox'))) {
|
||||
// 调用API删除后端的邮箱
|
||||
const result = await apiDeleteMailbox(address);
|
||||
if (result.success) {
|
||||
// 从前端列表和localStorage中移除
|
||||
const updatedMailboxes = savedMailboxes.filter(m => m.address !== address);
|
||||
setSavedMailboxes(updatedMailboxes);
|
||||
localStorage.setItem('savedMailboxes', JSON.stringify(updatedMailboxes));
|
||||
} else {
|
||||
// 如果删除失败,可以给用户一个提示
|
||||
alert(t('mailbox.deleteFailed'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// [feat]: 清空所有已保存的邮箱(包括后端数据)
|
||||
const handleClearAllMailboxes = async () => {
|
||||
if (window.confirm(t('mailbox.confirmClearAllMailboxes'))) {
|
||||
// 找出所有需要删除的邮箱(即列表中,非当前正在使用的邮箱)
|
||||
const mailboxesToDelete = savedMailboxes.filter(m => m.address !== currentMailbox.address);
|
||||
|
||||
// 如果没有需要删除的,直接返回
|
||||
if(mailboxesToDelete.length === 0) {
|
||||
setShowDropdown(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 并行执行所有后端删除请求
|
||||
const deletePromises = mailboxesToDelete.map(m => apiDeleteMailbox(m.address));
|
||||
await Promise.all(deletePromises);
|
||||
|
||||
// 从前端列表中只保留当前使用的邮箱
|
||||
const currentMailboxToKeep = savedMailboxes.find(m => m.address === currentMailbox.address);
|
||||
const mailboxesToKeep = currentMailboxToKeep ? [currentMailboxToKeep] : [];
|
||||
|
||||
// 更新UI和localStorage
|
||||
setSavedMailboxes(mailboxesToKeep);
|
||||
localStorage.setItem('savedMailboxes', JSON.stringify(mailboxesToKeep));
|
||||
setShowDropdown(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 如果没有保存的邮箱或者只有当前邮箱,不显示切换按钮
|
||||
if (savedMailboxes.length <= 1) {
|
||||
return null;
|
||||
@@ -120,21 +167,40 @@ const MailboxSwitcher: React.FC<MailboxSwitcherProps> = ({
|
||||
</button>
|
||||
|
||||
{showDropdown && (
|
||||
<div className="absolute top-9 left-0 bg-white border rounded-md shadow-lg p-1 z-20 min-w-[200px]">
|
||||
<div className="text-xs font-medium px-2 py-1 text-muted-foreground">
|
||||
// [fix]: 将 bg-white 替换为 bg-popover 和 text-popover-foreground 以支持黑暗模式
|
||||
<div className="absolute top-9 left-0 bg-popover text-popover-foreground border rounded-md shadow-lg p-1 z-20 min-w-[250px]">
|
||||
<div className="text-xs font-medium px-2 py-1 text-muted-foreground flex justify-between items-center">
|
||||
{t('mailbox.savedMailboxes') || "已保存的邮箱"}
|
||||
<button
|
||||
onClick={handleClearAllMailboxes}
|
||||
className="text-red-500 hover:text-red-700 text-xs"
|
||||
title={t('mailbox.clearAll') || "全部清除"}
|
||||
>
|
||||
<i className="fas fa-trash-alt mr-1"></i>
|
||||
{t('mailbox.clearAll') || "全部清除"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="max-h-[400px] overflow-y-auto">
|
||||
{savedMailboxes.map((m) => (
|
||||
<button
|
||||
key={m.address}
|
||||
onClick={() => handleSwitchMailbox(m)}
|
||||
className={`w-full text-left text-sm px-2 py-1.5 hover:bg-muted rounded-sm transition-colors truncate ${
|
||||
m.address === currentMailbox.address ? 'bg-primary/10 text-primary font-medium' : ''
|
||||
}`}
|
||||
>
|
||||
{m.address}@{domain}
|
||||
</button>
|
||||
<div key={m.address} className="flex items-center justify-between hover:bg-muted rounded-sm">
|
||||
<button
|
||||
onClick={() => handleSwitchMailbox(m)}
|
||||
className={`w-full text-left text-sm px-2 py-1.5 transition-colors truncate ${
|
||||
m.address === currentMailbox.address ? 'bg-primary/10 text-primary font-medium' : ''
|
||||
}`}
|
||||
>
|
||||
{m.address}@{domain}
|
||||
</button>
|
||||
{m.address !== currentMailbox.address && (
|
||||
<button
|
||||
onClick={() => handleDeleteMailbox(m.address)}
|
||||
className="p-2 text-red-500 hover:text-red-700"
|
||||
title={t('common.delete') || "删除"}
|
||||
>
|
||||
<i className="fas fa-trash-alt text-xs"></i>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
/**
|
||||
* 主题切换组件
|
||||
* 用于在明亮和暗黑模式之间切换
|
||||
*/
|
||||
const ThemeSwitcher: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 从 localStorage 获取初始主题,如果不存在则默认为 'light'
|
||||
const [theme, setTheme] = useState(() => {
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
return savedTheme || 'light';
|
||||
});
|
||||
|
||||
// 使用 useEffect 监听 theme 状态的变化,并应用到 <html> 元素上
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
// 先移除旧的 class,以防万一
|
||||
root.classList.remove(theme === 'light' ? 'dark' : 'light');
|
||||
// 添加当前主题的 class
|
||||
root.classList.add(theme);
|
||||
// 将当前主题保存到 localStorage
|
||||
localStorage.setItem('theme', theme);
|
||||
}, [theme]);
|
||||
|
||||
// 定义切换主题的函数
|
||||
const toggleTheme = () => {
|
||||
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="w-8 h-8 flex items-center justify-center rounded-md transition-all duration-200 hover:bg-primary/20 hover:text-primary hover:scale-110"
|
||||
aria-label={t('settings.toggleTheme', '切换主题')}
|
||||
title={t('settings.toggleTheme', '切换主题')}
|
||||
>
|
||||
{/* 根据当前主题显示不同的图标 */}
|
||||
{theme === 'light' ? (
|
||||
<i className="fas fa-moon text-base"></i> // 亮色模式下,显示月亮图标以切换到暗色
|
||||
) : (
|
||||
<i className="fas fa-sun text-base"></i> // 暗色模式下,显示太阳图标以切换到亮色
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeSwitcher;
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { createContext, useState, useEffect, ReactNode, useRef } from 'react';
|
||||
import {
|
||||
createRandomMailbox,
|
||||
getMailboxFromLocalStorage,
|
||||
import {
|
||||
createRandomMailbox,
|
||||
getMailboxFromLocalStorage,
|
||||
saveMailboxToLocalStorage,
|
||||
removeMailboxFromLocalStorage,
|
||||
getEmails,
|
||||
@@ -82,7 +82,7 @@ export const MailboxProvider: React.FC<MailboxProviderProps> = ({ children }) =>
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||
const errorTimeoutRef = useRef<number | null>(null);
|
||||
const successTimeoutRef = useRef<number | null>(null);
|
||||
|
||||
|
||||
// 清除提示的定时器
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -94,13 +94,13 @@ export const MailboxProvider: React.FC<MailboxProviderProps> = ({ children }) =>
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
// 初始化:检查本地存储或创建新邮箱
|
||||
useEffect(() => {
|
||||
const initMailbox = async () => {
|
||||
// 检查本地存储中是否有未过期的邮箱
|
||||
const savedMailbox = getMailboxFromLocalStorage();
|
||||
|
||||
|
||||
if (savedMailbox) {
|
||||
setMailbox(savedMailbox);
|
||||
setIsLoading(false);
|
||||
@@ -109,32 +109,36 @@ export const MailboxProvider: React.FC<MailboxProviderProps> = ({ children }) =>
|
||||
await createNewMailbox();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
initMailbox();
|
||||
}, []);
|
||||
|
||||
|
||||
// 创建新邮箱
|
||||
const createNewMailbox = async () => {
|
||||
try {
|
||||
// 清除之前的错误和成功信息
|
||||
setErrorMessage(null);
|
||||
setSuccessMessage(null);
|
||||
|
||||
|
||||
console.log('createNewMailbox: Started');
|
||||
setIsLoading(true);
|
||||
|
||||
|
||||
console.log('createNewMailbox: Calling createRandomMailbox...');
|
||||
const result = await createRandomMailbox();
|
||||
console.log('createNewMailbox: createRandomMailbox result:', result);
|
||||
|
||||
|
||||
if (result.success && result.mailbox) {
|
||||
console.log('createNewMailbox: Setting new mailbox:', result.mailbox);
|
||||
setMailbox(result.mailbox);
|
||||
saveMailboxToLocalStorage(result.mailbox);
|
||||
// [fix]: 创建新邮箱后,清空旧的邮件列表和缓存
|
||||
setEmails([]);
|
||||
setSelectedEmail(null);
|
||||
clearEmailCache();
|
||||
} else {
|
||||
console.error('createNewMailbox: Failed to create mailbox:', result.error);
|
||||
setErrorMessage(t('mailbox.createFailed'));
|
||||
|
||||
|
||||
// 3秒后清除错误信息
|
||||
if (errorTimeoutRef.current) {
|
||||
window.clearTimeout(errorTimeoutRef.current);
|
||||
@@ -151,30 +155,30 @@ export const MailboxProvider: React.FC<MailboxProviderProps> = ({ children }) =>
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 删除邮箱
|
||||
const deleteMailbox = async () => {
|
||||
if (!mailbox) return;
|
||||
|
||||
|
||||
try {
|
||||
// 清除之前的错误和成功信息
|
||||
setErrorMessage(null);
|
||||
setSuccessMessage(null);
|
||||
|
||||
|
||||
// 调用API删除邮箱
|
||||
const result = await apiDeleteMailbox(mailbox.address);
|
||||
|
||||
|
||||
if (result.success) {
|
||||
// 显示成功信息
|
||||
setSuccessMessage(t('mailbox.deleteSuccess'));
|
||||
|
||||
|
||||
// 清除本地数据
|
||||
setMailbox(null);
|
||||
setEmails([]);
|
||||
setSelectedEmail(null);
|
||||
removeMailboxFromLocalStorage();
|
||||
clearEmailCache();
|
||||
|
||||
|
||||
// 3秒后清除成功信息
|
||||
if (successTimeoutRef.current) {
|
||||
window.clearTimeout(successTimeoutRef.current);
|
||||
@@ -182,13 +186,13 @@ export const MailboxProvider: React.FC<MailboxProviderProps> = ({ children }) =>
|
||||
successTimeoutRef.current = window.setTimeout(() => {
|
||||
setSuccessMessage(null);
|
||||
}, 3000);
|
||||
|
||||
|
||||
// 创建新邮箱
|
||||
await createNewMailbox();
|
||||
} else {
|
||||
// 显示错误信息
|
||||
setErrorMessage(t('mailbox.deleteFailed'));
|
||||
|
||||
|
||||
// 3秒后清除错误信息
|
||||
if (errorTimeoutRef.current) {
|
||||
window.clearTimeout(errorTimeoutRef.current);
|
||||
@@ -199,10 +203,10 @@ export const MailboxProvider: React.FC<MailboxProviderProps> = ({ children }) =>
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting mailbox:', error);
|
||||
|
||||
|
||||
// 显示错误信息
|
||||
setErrorMessage(t('mailbox.deleteFailed'));
|
||||
|
||||
|
||||
// 3秒后清除错误信息
|
||||
if (errorTimeoutRef.current) {
|
||||
window.clearTimeout(errorTimeoutRef.current);
|
||||
@@ -212,37 +216,24 @@ export const MailboxProvider: React.FC<MailboxProviderProps> = ({ children }) =>
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 刷新邮件列表
|
||||
const refreshEmails = async () => {
|
||||
if (!mailbox) return;
|
||||
|
||||
|
||||
// 防止重复请求
|
||||
if (isEmailsLoading) return;
|
||||
|
||||
|
||||
setIsEmailsLoading(true);
|
||||
|
||||
|
||||
try {
|
||||
const result = await getEmails(mailbox.address);
|
||||
|
||||
|
||||
if (result.success) {
|
||||
setEmails(result.emails);
|
||||
} else if (result.notFound) {
|
||||
// 如果邮箱不存在,清除本地缓存并创建新邮箱
|
||||
try {
|
||||
// 直接调用handleMailboxNotFound函数
|
||||
await handleMailboxNotFound();
|
||||
} catch (error) {
|
||||
// 出错时也尝试清除缓存并创建新邮箱
|
||||
setMailbox(null);
|
||||
setEmails([]);
|
||||
setSelectedEmail(null);
|
||||
removeMailboxFromLocalStorage();
|
||||
clearEmailCache();
|
||||
|
||||
// 刷新页面
|
||||
window.location.href = '/';
|
||||
}
|
||||
// [fix]: 如果邮箱不存在,调用 handleMailboxNotFound 进行平滑处理,而不是强制刷新页面
|
||||
await handleMailboxNotFound();
|
||||
}
|
||||
} catch (error) {
|
||||
// 错误处理
|
||||
@@ -251,64 +242,48 @@ export const MailboxProvider: React.FC<MailboxProviderProps> = ({ children }) =>
|
||||
setIsEmailsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 自动刷新邮件
|
||||
useEffect(() => {
|
||||
if (!mailbox) return;
|
||||
|
||||
if (!mailbox || isLoading) return;
|
||||
|
||||
// 首次加载邮件(无论autoRefresh是否开启)
|
||||
refreshEmails();
|
||||
|
||||
|
||||
// 如果自动刷新开启,则设置定时器
|
||||
let intervalId: number | undefined;
|
||||
if (autoRefresh) {
|
||||
intervalId = window.setInterval(refreshEmails, AUTO_REFRESH_INTERVAL);
|
||||
}
|
||||
|
||||
|
||||
return () => {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
};
|
||||
}, [mailbox, autoRefresh]);
|
||||
|
||||
// 处理邮箱不存在的情况
|
||||
}, [mailbox, autoRefresh, isLoading]);
|
||||
|
||||
// [fix]: 重构处理邮箱不存在的逻辑,避免页面刷新
|
||||
const handleMailboxNotFound = async () => {
|
||||
try {
|
||||
// 清除之前的错误和成功信息
|
||||
setErrorMessage(null);
|
||||
setSuccessMessage(null);
|
||||
|
||||
setSuccessMessage(t('mailbox.creatingNew'));
|
||||
|
||||
// 清除当前邮箱信息
|
||||
setMailbox(null);
|
||||
setEmails([]);
|
||||
setSelectedEmail(null);
|
||||
removeMailboxFromLocalStorage();
|
||||
clearEmailCache();
|
||||
|
||||
// 创建新邮箱
|
||||
try {
|
||||
const result = await createRandomMailbox();
|
||||
|
||||
if (result.success && result.mailbox) {
|
||||
// 直接保存到localStorage,而不是通过setMailbox触发状态更新
|
||||
saveMailboxToLocalStorage(result.mailbox);
|
||||
|
||||
// 直接刷新页面,让页面重新加载时从localStorage获取新邮箱
|
||||
window.location.href = '/'; // 使用href而不是reload,确保导航到首页
|
||||
} else {
|
||||
throw new Error('Failed to create mailbox');
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
// 设置提示信息,告知用户正在创建新邮箱
|
||||
setSuccessMessage(t('mailbox.creatingNew'));
|
||||
|
||||
// 清除当前无效的邮箱信息
|
||||
removeMailboxFromLocalStorage();
|
||||
clearEmailCache();
|
||||
|
||||
// 异步创建新邮箱,并更新应用状态
|
||||
await createNewMailbox();
|
||||
|
||||
// 3秒后清除提示信息
|
||||
if (successTimeoutRef.current) {
|
||||
window.clearTimeout(successTimeoutRef.current);
|
||||
}
|
||||
successTimeoutRef.current = window.setTimeout(() => {
|
||||
setSuccessMessage(null);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
|
||||
// 添加邮件到缓存
|
||||
const addToEmailCache = (emailId: string, email: Email, attachments: any[]) => {
|
||||
setEmailCache(prev => ({
|
||||
@@ -319,7 +294,7 @@ export const MailboxProvider: React.FC<MailboxProviderProps> = ({ children }) =>
|
||||
timestamp: Date.now()
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
// 保存到localStorage
|
||||
try {
|
||||
const mailboxAddress = mailbox?.address;
|
||||
@@ -339,11 +314,11 @@ export const MailboxProvider: React.FC<MailboxProviderProps> = ({ children }) =>
|
||||
console.error('Error saving email cache to localStorage:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 清除邮件缓存
|
||||
const clearEmailCache = () => {
|
||||
setEmailCache({});
|
||||
|
||||
|
||||
// 清除localStorage中的缓存
|
||||
try {
|
||||
const mailboxAddress = mailbox?.address;
|
||||
@@ -355,15 +330,15 @@ export const MailboxProvider: React.FC<MailboxProviderProps> = ({ children }) =>
|
||||
console.error('Error clearing email cache from localStorage:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 从localStorage加载邮件缓存
|
||||
useEffect(() => {
|
||||
if (!mailbox) return;
|
||||
|
||||
|
||||
try {
|
||||
const cacheKey = `emailCache_${mailbox.address}`;
|
||||
const cachedData = localStorage.getItem(cacheKey);
|
||||
|
||||
|
||||
if (cachedData) {
|
||||
const parsedCache = JSON.parse(cachedData);
|
||||
setEmailCache(parsedCache);
|
||||
@@ -372,13 +347,13 @@ export const MailboxProvider: React.FC<MailboxProviderProps> = ({ children }) =>
|
||||
console.error('Error loading email cache from localStorage:', error);
|
||||
}
|
||||
}, [mailbox]);
|
||||
|
||||
|
||||
// 设置邮箱并保存到localStorage
|
||||
const handleSetMailbox = (newMailbox: Mailbox) => {
|
||||
setMailbox(newMailbox);
|
||||
saveMailboxToLocalStorage(newMailbox);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<MailboxContext.Provider
|
||||
value={{
|
||||
@@ -404,7 +379,7 @@ export const MailboxProvider: React.FC<MailboxProviderProps> = ({ children }) =>
|
||||
successMessage
|
||||
}}
|
||||
>
|
||||
{/* 错误和成功提示 */}
|
||||
{/* [feat] 将错误和成功提示移至此处,作为全局通知组件 */}
|
||||
{(errorMessage || successMessage) && (
|
||||
<div className="fixed top-4 right-4 z-50 p-3 rounded-md shadow-lg max-w-md" style={{ backgroundColor: errorMessage ? '#FEE2E2' : '#ECFDF5', color: errorMessage ? '#991B1B' : '#065F46' }}>
|
||||
{errorMessage || successMessage}
|
||||
|
||||
+54
-111
@@ -128,6 +128,7 @@ export async function getMailboxes(db: D1Database, ipAddress: string): Promise<M
|
||||
* @param address 邮箱地址
|
||||
*/
|
||||
export async function deleteMailbox(db: D1Database, address: string): Promise<void> {
|
||||
// [feat] 由于外键设置了 ON DELETE CASCADE,直接删除邮箱即可级联删除相关邮件和附件
|
||||
await db.prepare(`DELETE FROM mailboxes WHERE address = ?`).bind(address).run();
|
||||
}
|
||||
|
||||
@@ -137,39 +138,39 @@ export async function deleteMailbox(db: D1Database, address: string): Promise<vo
|
||||
* @returns 删除的附件数量
|
||||
*/
|
||||
async function cleanupOrphanedAttachments(db: D1Database): Promise<number> {
|
||||
try {
|
||||
// 获取所有孤立附件的ID列表
|
||||
const orphanedAttachments = await db.prepare(`SELECT id, is_large FROM attachments a WHERE NOT EXISTS (SELECT 1 FROM emails e WHERE e.id = a.email_id)`).all();
|
||||
|
||||
if (!orphanedAttachments.results || orphanedAttachments.results.length === 0) {
|
||||
return 0;
|
||||
// [refactor] 优化孤立附件的清理逻辑
|
||||
try {
|
||||
// 一次性查询所有孤立附件及其分块信息
|
||||
const orphanedAttachmentsResult = await db.prepare(`
|
||||
SELECT a.id
|
||||
FROM attachments a
|
||||
LEFT JOIN emails e ON a.email_id = e.id
|
||||
WHERE e.id IS NULL
|
||||
`).all<{ id: string }>();
|
||||
|
||||
if (!orphanedAttachmentsResult.results || orphanedAttachmentsResult.results.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const attachmentIds = orphanedAttachmentsResult.results.map(row => row.id);
|
||||
const placeholders = attachmentIds.map(() => '?').join(',');
|
||||
|
||||
console.log(`找到 ${attachmentIds.length} 个孤立附件,准备清理...`);
|
||||
|
||||
// 批量删除附件分块
|
||||
await db.prepare(`DELETE FROM attachment_chunks WHERE attachment_id IN (${placeholders})`).bind(...attachmentIds).run();
|
||||
console.log(`已清理孤立附件的所有分块`);
|
||||
|
||||
// 批量删除附件记录
|
||||
const deleteResult = await db.prepare(`DELETE FROM attachments WHERE id IN (${placeholders})`).bind(...attachmentIds).run();
|
||||
const deletedCount = deleteResult.meta?.changes || 0;
|
||||
console.log(`已清理 ${deletedCount} 个孤立附件记录`);
|
||||
|
||||
return deletedCount;
|
||||
} catch (error) {
|
||||
console.error('清理孤立附件时出错:', error);
|
||||
return 0;
|
||||
}
|
||||
|
||||
console.log(`找到 ${orphanedAttachments.results.length} 个孤立附件,准备清理...`);
|
||||
|
||||
// 清理每个孤立附件
|
||||
for (const attachment of orphanedAttachments.results) {
|
||||
const attachmentId = attachment.id as string;
|
||||
const isLarge = !!attachment.is_large;
|
||||
|
||||
// 如果是大型附件,先删除所有分块
|
||||
if (isLarge) {
|
||||
await db.prepare(`DELETE FROM attachment_chunks WHERE attachment_id = ?`).bind(attachmentId).run();
|
||||
console.log(`已清理孤立附件 ${attachmentId} 的所有分块`);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除所有孤立附件记录
|
||||
const result = await db.prepare(`DELETE FROM attachments WHERE NOT EXISTS (SELECT 1 FROM emails e WHERE e.id = attachments.email_id)`).run();
|
||||
|
||||
const deletedCount = result.meta?.changes || 0;
|
||||
console.log(`已清理 ${deletedCount} 个孤立附件记录`);
|
||||
|
||||
return deletedCount;
|
||||
} catch (error) {
|
||||
console.error('清理孤立附件时出错:', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,37 +180,12 @@ async function cleanupOrphanedAttachments(db: D1Database): Promise<number> {
|
||||
*/
|
||||
export async function cleanupExpiredMailboxes(db: D1Database): Promise<number> {
|
||||
const now = getCurrentTimestamp();
|
||||
|
||||
// 获取过期邮箱的ID列表
|
||||
const expiredMailboxes = await db.prepare(`SELECT id FROM mailboxes WHERE expires_at <= ?`).bind(now).all();
|
||||
|
||||
if (expiredMailboxes.results && expiredMailboxes.results.length > 0) {
|
||||
const mailboxIds = expiredMailboxes.results.map(row => row.id as string);
|
||||
|
||||
// 记录日志
|
||||
console.log(`找到 ${mailboxIds.length} 个过期邮箱,准备清理...`);
|
||||
|
||||
// 由于设置了外键级联删除,删除邮箱时会自动删除相关的邮件和附件
|
||||
// 但为了确保附件被正确清理,我们先获取这些邮箱中的所有邮件ID
|
||||
for (const mailboxId of mailboxIds) {
|
||||
const emails = await db.prepare(`SELECT id FROM emails WHERE mailbox_id = ?`).bind(mailboxId).all();
|
||||
|
||||
if (emails.results && emails.results.length > 0) {
|
||||
const emailIds = emails.results.map(row => row.id as string);
|
||||
console.log(`邮箱 ${mailboxId} 中有 ${emailIds.length} 封邮件需要清理`);
|
||||
|
||||
// 清理每封邮件的附件
|
||||
for (const emailId of emailIds) {
|
||||
await cleanupAttachments(db, emailId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除过期邮箱(会级联删除邮件和附件)
|
||||
// [refactor] 由于数据库 schema 中设置了 ON DELETE CASCADE,
|
||||
// 删除 mailboxes 表中的记录会自动删除 emails, attachments, 和 attachment_chunks 中所有相关的记录。
|
||||
// 这大大简化了清理逻辑,并提高了性能。
|
||||
const result = await db.prepare(`DELETE FROM mailboxes WHERE expires_at <= ?`).bind(now).run();
|
||||
|
||||
// 清理可能存在的孤立附件
|
||||
// 清理可能由于异常情况产生的孤立附件
|
||||
await cleanupOrphanedAttachments(db);
|
||||
|
||||
return result.meta?.changes || 0;
|
||||
@@ -224,23 +200,9 @@ export async function cleanupExpiredMails(db: D1Database): Promise<number> {
|
||||
const now = getCurrentTimestamp();
|
||||
const oneDayAgo = now - 24 * 60 * 60; // 24小时前的时间戳(秒)
|
||||
|
||||
// 获取过期邮件的ID列表
|
||||
const expiredEmails = await db.prepare(`SELECT id FROM emails WHERE received_at <= ?`).bind(oneDayAgo).all();
|
||||
|
||||
if (expiredEmails.results && expiredEmails.results.length > 0) {
|
||||
const emailIds = expiredEmails.results.map(row => row.id as string);
|
||||
console.log(`找到 ${emailIds.length} 封过期邮件,准备清理...`);
|
||||
|
||||
// 清理每封邮件的附件
|
||||
for (const emailId of emailIds) {
|
||||
await cleanupAttachments(db, emailId);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除过期邮件(会级联删除附件)
|
||||
// [refactor] 同样利用 ON DELETE CASCADE 特性简化逻辑
|
||||
const result = await db.prepare(`DELETE FROM emails WHERE received_at <= ?`).bind(oneDayAgo).run();
|
||||
|
||||
// 清理可能存在的孤立附件
|
||||
await cleanupOrphanedAttachments(db);
|
||||
|
||||
return result.meta?.changes || 0;
|
||||
@@ -252,23 +214,9 @@ export async function cleanupExpiredMails(db: D1Database): Promise<number> {
|
||||
* @returns 删除的邮件数量
|
||||
*/
|
||||
export async function cleanupReadMails(db: D1Database): Promise<number> {
|
||||
// 获取已读邮件的ID列表
|
||||
const readEmails = await db.prepare(`SELECT id FROM emails WHERE is_read = 1`).all();
|
||||
|
||||
if (readEmails.results && readEmails.results.length > 0) {
|
||||
const emailIds = readEmails.results.map(row => row.id as string);
|
||||
console.log(`找到 ${emailIds.length} 封已读邮件,准备清理...`);
|
||||
|
||||
// 清理每封邮件的附件
|
||||
for (const emailId of emailIds) {
|
||||
await cleanupAttachments(db, emailId);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除已读邮件(会级联删除附件)
|
||||
// [refactor] 同样利用 ON DELETE CASCADE 特性简化逻辑
|
||||
const result = await db.prepare(`DELETE FROM emails WHERE is_read = 1`).run();
|
||||
|
||||
// 清理可能存在的孤立附件
|
||||
await cleanupOrphanedAttachments(db);
|
||||
|
||||
return result.meta?.changes || 0;
|
||||
@@ -280,26 +228,24 @@ export async function cleanupReadMails(db: D1Database): Promise<number> {
|
||||
* @param emailId 邮件ID
|
||||
*/
|
||||
async function cleanupAttachments(db: D1Database, emailId: string): Promise<void> {
|
||||
// [refactor] 利用 ON DELETE CASCADE,此函数在删除邮件时不再需要手动调用。
|
||||
// 但保留此函数以备其他需要单独清理附件的场景。
|
||||
try {
|
||||
// 获取邮件的所有附件
|
||||
const attachments = await db.prepare(`SELECT id, is_large FROM attachments WHERE email_id = ?`).bind(emailId).all();
|
||||
// 获取邮件的所有附件ID
|
||||
const attachmentsResult = await db.prepare(`SELECT id FROM attachments WHERE email_id = ?`).bind(emailId).all<{ id: string }>();
|
||||
|
||||
if (attachments.results && attachments.results.length > 0) {
|
||||
console.log(`邮件 ${emailId} 有 ${attachments.results.length} 个附件需要清理`);
|
||||
if (attachmentsResult.results && attachmentsResult.results.length > 0) {
|
||||
const attachmentIds = attachmentsResult.results.map(row => row.id);
|
||||
const placeholders = attachmentIds.map(() => '?').join(',');
|
||||
|
||||
console.log(`邮件 ${emailId} 有 ${attachmentIds.length} 个附件需要清理`);
|
||||
|
||||
for (const attachment of attachments.results) {
|
||||
const attachmentId = attachment.id as string;
|
||||
const isLarge = !!attachment.is_large;
|
||||
|
||||
// 如果是大型附件,先删除所有分块
|
||||
if (isLarge) {
|
||||
await db.prepare(`DELETE FROM attachment_chunks WHERE attachment_id = ?`).bind(attachmentId).run();
|
||||
console.log(`已清理附件 ${attachmentId} 的所有分块`);
|
||||
}
|
||||
}
|
||||
// 批量删除所有分块
|
||||
await db.prepare(`DELETE FROM attachment_chunks WHERE attachment_id IN (${placeholders})`).bind(...attachmentIds).run();
|
||||
console.log(`已清理附件的所有分块`);
|
||||
|
||||
// 删除所有附件记录
|
||||
await db.prepare(`DELETE FROM attachments WHERE email_id = ?`).bind(emailId).run();
|
||||
// 批量删除所有附件记录
|
||||
await db.prepare(`DELETE FROM attachments WHERE id IN (${placeholders})`).bind(...attachmentIds).run();
|
||||
console.log(`已清理邮件 ${emailId} 的所有附件`);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -558,9 +504,6 @@ async function getAttachmentContent(db: D1Database, attachmentId: string, chunks
|
||||
* @param id 邮件ID
|
||||
*/
|
||||
export async function deleteEmail(db: D1Database, id: string): Promise<void> {
|
||||
// 先清理邮件的所有附件
|
||||
await cleanupAttachments(db, id);
|
||||
|
||||
// 然后删除邮件
|
||||
// [refactor] 由于外键设置了 ON DELETE CASCADE,直接删除邮件即可
|
||||
await db.prepare(`DELETE FROM emails WHERE id = ?`).bind(id).run();
|
||||
}
|
||||
+5
-3
@@ -8,14 +8,16 @@ compatibility_date = "2025-03-01"
|
||||
keep_vars = true
|
||||
|
||||
# 启用 D1 数据库
|
||||
# 注意:这里的 binding 是固定的 "DB",database_name 和 database_id 将由 GitHub Action 动态替换
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
database_name = "zmail" # 替换为您的实际的数据库 NAME
|
||||
database_id = "d77b6f63-43ad-46a9-8a49-d4a2706be53e" # 需要替换为您的实际数据库 ID
|
||||
database_name = "${D1_DATABASE_NAME}" # 这是 D1 数据库名称的占位符
|
||||
database_id = "${D1_DATABASE_ID}" # 这是 D1 数据库 ID 的占位符
|
||||
|
||||
# 设置域名
|
||||
# 注意:这里的 VITE_EMAIL_DOMAIN 将由 GitHub Action 动态替换
|
||||
[vars]
|
||||
VITE_EMAIL_DOMAIN = "mdzz.uk,zaunist.com" # 替换为您的域名列表,多个域名使用逗号 , 分割。这里的域名只是用来网页上展示,方便复制粘贴使用。
|
||||
VITE_EMAIL_DOMAIN = "${VITE_EMAIL_DOMAIN}" # 这是邮箱域名的占位符
|
||||
|
||||
# 配置邮件处理
|
||||
[triggers]
|
||||
|
||||
Reference in new issue
Block a user