From 06a8a7c635fc2de1c69aba6cf1a5f5f28fb0314a Mon Sep 17 00:00:00 2001 From: gujiangjiang Date: Fri, 10 Oct 2025 03:30:04 +0800 Subject: [PATCH 1/9] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0Github=20Action?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E8=87=AA=E5=8A=A8=E5=8C=96=E9=83=A8=E7=BD=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 98 ++++++++++++++++++++++++++++++++++++ README.en.md | 29 ++++++----- README.md | 29 ++++++----- wrangler.toml | 8 +-- 4 files changed, 131 insertions(+), 33 deletions(-) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..013bf64 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -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 \ No newline at end of file diff --git a/README.en.md b/README.en.md index cc20fe1..2ef3182 100644 --- a/README.en.md +++ b/README.en.md @@ -121,7 +121,7 @@ We provide two deployment methods, you can choose according to your needs: -#### Option 2: Fork and Custom Deployment (Recommended for Advanced Users) +#### Option 2: Fork and Custom Deployment via Github Action (Recommended for Advanced Users)

✅ Advantages:

@@ -129,33 +129,32 @@ We provide two deployment methods, you can choose according to your needs:
  • Can receive subsequent code updates
  • Fully customizable configuration
  • Better version control
  • +
  • Automatic deployment via GitHub Actions, more secure and convenient
  • ❌ Disadvantages:

    📋 Deployment Steps:

    1. Fork this project to your GitHub account
    2. -
    3. Create a D1 database in Cloudflare Dashboard, remember the database name and id
    4. -
    5. Modify the wrangler.toml file in the root directory: +
    6. Create a D1 database in your Cloudflare Dashboard and note down the database_name and database_id
    7. +
    8. In your GitHub repository, go to Settings > Secrets and variables > Actions
    9. +
    10. Click New repository secret and add the following five secrets:
        -
      • Modify name to your application name (optional)
      • -
      • Modify routes with your custom domain (required! Default worker.dev domain is not accessible in mainland China)
      • -
      • Modify database_name and database_id to your D1 database information (required!)
      • -
      • Modify VITE_EMAIL_DOMAIN to your domain list (optional, just for convenient copy-paste of email addresses on the webpage, doesn't affect email reception)
      • +
      • CF_API_TOKEN: Your Cloudflare API Token. You can create one here using the "Edit Cloudflare Workers" template.
      • +
      • CF_ACCOUNT_ID: Your Cloudflare Account ID. You can find it on the right side of the Workers page.
      • +
      • D1_DATABASE_ID: The ID of the D1 database you created in step 2.
      • +
      • D1_DATABASE_NAME: The name of the D1 database you created in step 2.
      • +
      • VITE_EMAIL_DOMAIN: Your list of domains, separated by commas (e.g., example.com,test.com).
    11. -
    12. In Cloudflare Dashboard, select "Workers & Pages"
    13. -
    14. Click "Create application" -> "Pages" -> "Connect to Git"
    15. -
    16. Select your forked repository
    17. -
    18. Configure build settings (usually auto-detected)
    19. -
    20. Click "Save and Deploy"
    21. -
    22. Configure Cloudflare Email routing to forward emails to your Worker
    23. +
    24. After completing the steps above, the project will be automatically deployed on every push to the main branch. You can also trigger the deployment manually from the Actions page.
    25. +
    26. After deployment, bind a custom domain to your Worker.
    27. +
    28. Finally, configure Cloudflare Email Routing to forward emails to your Worker.
    diff --git a/README.md b/README.md index cebbd69..deb61b4 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ ZMAIL 现在采用全新的一体化部署方式,前端和后端整合为一 -#### 方式二:Fork 后自定义部署(推荐进阶用户) +#### 方式二:Fork 后通过 Github Action 自定义部署(推荐进阶用户)

    ✅ 优点:

    @@ -127,33 +127,32 @@ ZMAIL 现在采用全新的一体化部署方式,前端和后端整合为一
  • 可以获得后续代码更新
  • 完全自定义配置
  • 更好的版本控制
  • +
  • 通过 GitHub Action 自动部署,更加安全便捷
  • ❌ 缺点:

    📋 部署步骤:

    1. Fork 本项目到您的 GitHub 账户
    2. -
    3. 在 Cloudflare Dashboard 中创建一个 D1 数据库,记住数据库的 name 和 id
    4. -
    5. 修改根目录下的 wrangler.toml 文件: +
    6. 在 Cloudflare Dashboard 中创建一个 D1 数据库,并记录下数据库的 database_name 和 database_id
    7. +
    8. 在您的 GitHub 仓库中, 前往 Settings > Secrets and variables > Actions
    9. +
    10. 点击 New repository secret 并添加以下五个密钥:
        -
      • 修改 name 为您的应用名称。(可选)
      • -
      • 修改 routes 中的自定义域名。(必须!默认的worker.dev域名在中国大陆无法访问)
      • -
      • 修改 database_name 和 database_id 为您的 D1 数据库信息。(必须!)
      • -
      • 修改 VITE_EMAIL_DOMAIN 为您的域名列表。(可选,只是方便网页上复制粘贴邮件地址使用,不设置也不影响接收邮件)
      • +
      • CF_API_TOKEN: 你的 Cloudflare API Token。你可以在 这里 创建,使用 "Edit Cloudflare Workers" 模板即可。
      • +
      • CF_ACCOUNT_ID: 你的 Cloudflare 账户 ID。你可以在 Workers 页面的右侧找到。
      • +
      • D1_DATABASE_ID: 你在第二步中创建的 D1 数据库的 ID。
      • +
      • D1_DATABASE_NAME: 你在第二步中创建的 D1 数据库的名称。
      • +
      • VITE_EMAIL_DOMAIN: 你的域名列表,多个域名用逗号 ',' 分割 (例如: example.com,test.com)。
    11. -
    12. 在 Cloudflare Dashboard 中选择 "Workers & Pages"
    13. -
    14. 点击 "Create application" -> "Pages" -> "Connect to Git"
    15. -
    16. 选择您 Fork 的仓库
    17. -
    18. 配置构建设置(通常会自动检测)
    19. -
    20. 点击 "Save and Deploy"
    21. -
    22. 配置 Cloudflare Email 路由,将邮件转发到您的 Worker
    23. +
    24. 完成以上步骤后,项目将在每次推送到 main 分支时自动部署。你也可以在 Actions 页面手动触发部署。
    25. +
    26. 部署完成后,为你的 Worker 绑定一个自定义域名。
    27. +
    28. 最后,配置 Cloudflare Email 路由,将邮件转发到你的 Worker。
    diff --git a/wrangler.toml b/wrangler.toml index 2d29e6f..a6f8e9b 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -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] From d4462671aedf9b1d378f5eaca866946f8763af69 Mon Sep 17 00:00:00 2001 From: gujiangjiang Date: Fri, 10 Oct 2025 03:48:43 +0800 Subject: [PATCH 2/9] =?UTF-8?q?fix:=20=E7=BB=9F=E4=B8=80=20select=20?= =?UTF-8?q?=E4=B8=8B=E6=8B=89=E6=A1=86=E5=9C=A8=E4=B8=8D=E5=90=8C=E6=B5=8F?= =?UTF-8?q?=E8=A7=88=E5=99=A8=E4=B8=AD=E7=9A=84=E6=98=BE=E7=A4=BA=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/HeaderMailbox.tsx | 50 ++++++++++++++--------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/frontend/src/components/HeaderMailbox.tsx b/frontend/src/components/HeaderMailbox.tsx index ba65f22..c41bfba 100644 --- a/frontend/src/components/HeaderMailbox.tsx +++ b/frontend/src/components/HeaderMailbox.tsx @@ -231,15 +231,21 @@ const HeaderMailbox: React.FC = ({ /> @ - + {/* [fix]: 为select包裹一个relative容器,用于绝对定位自定义箭头 */} +
    + + {/* [fix]: 添加自定义的下拉箭头图标 */} + +
    + ); +}; + +export default ThemeSwitcher; \ No newline at end of file From 3f62bbe9d4ea06f72b9650bf3d9a37f29d4cc810 Mon Sep 17 00:00:00 2001 From: gujiangjiang Date: Fri, 10 Oct 2025 04:09:20 +0800 Subject: [PATCH 4/9] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E6=B8=85=E7=90=86=E9=80=BB=E8=BE=91=E4=BB=A5?= =?UTF-8?q?=E6=8F=90=E5=8D=87=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- worker/src/database.ts | 165 ++++++++++++++--------------------------- 1 file changed, 54 insertions(+), 111 deletions(-) diff --git a/worker/src/database.ts b/worker/src/database.ts index 3899c4d..403a7ae 100644 --- a/worker/src/database.ts +++ b/worker/src/database.ts @@ -128,6 +128,7 @@ export async function getMailboxes(db: D1Database, ipAddress: string): Promise { + // [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 { - 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 { */ export async function cleanupExpiredMailboxes(db: D1Database): Promise { 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 { 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 { * @returns 删除的邮件数量 */ export async function cleanupReadMails(db: D1Database): Promise { - // 获取已读邮件的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 { * @param emailId 邮件ID */ async function cleanupAttachments(db: D1Database, emailId: string): Promise { + // [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 { - // 先清理邮件的所有附件 - await cleanupAttachments(db, id); - - // 然后删除邮件 + // [refactor] 由于外键设置了 ON DELETE CASCADE,直接删除邮件即可 await db.prepare(`DELETE FROM emails WHERE id = ?`).bind(id).run(); } \ No newline at end of file From 11c1604de14c54259b252605712bbd5bb92b5a16 Mon Sep 17 00:00:00 2001 From: gujiangjiang Date: Fri, 10 Oct 2025 04:11:24 +0800 Subject: [PATCH 5/9] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=82=AE=E7=AE=B1?= =?UTF-8?q?=E4=B8=8D=E5=AD=98=E5=9C=A8=E6=88=96=E8=BF=87=E6=9C=9F=E6=97=B6?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E5=BC=BA=E5=88=B6=E5=88=B7=E6=96=B0=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/contexts/MailboxContext.tsx | 159 ++++++++++------------- 1 file changed, 67 insertions(+), 92 deletions(-) diff --git a/frontend/src/contexts/MailboxContext.tsx b/frontend/src/contexts/MailboxContext.tsx index 47173b0..8f39da1 100644 --- a/frontend/src/contexts/MailboxContext.tsx +++ b/frontend/src/contexts/MailboxContext.tsx @@ -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 = ({ children }) => const [successMessage, setSuccessMessage] = useState(null); const errorTimeoutRef = useRef(null); const successTimeoutRef = useRef(null); - + // 清除提示的定时器 useEffect(() => { return () => { @@ -94,13 +94,13 @@ export const MailboxProvider: React.FC = ({ children }) => } }; }, []); - + // 初始化:检查本地存储或创建新邮箱 useEffect(() => { const initMailbox = async () => { // 检查本地存储中是否有未过期的邮箱 const savedMailbox = getMailboxFromLocalStorage(); - + if (savedMailbox) { setMailbox(savedMailbox); setIsLoading(false); @@ -109,32 +109,36 @@ export const MailboxProvider: React.FC = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ children }) => timestamp: Date.now() } })); - + // 保存到localStorage try { const mailboxAddress = mailbox?.address; @@ -339,11 +314,11 @@ export const MailboxProvider: React.FC = ({ 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 = ({ 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 = ({ children }) => console.error('Error loading email cache from localStorage:', error); } }, [mailbox]); - + // 设置邮箱并保存到localStorage const handleSetMailbox = (newMailbox: Mailbox) => { setMailbox(newMailbox); saveMailboxToLocalStorage(newMailbox); }; - + return ( = ({ children }) => successMessage }} > - {/* 错误和成功提示 */} + {/* [feat] 将错误和成功提示移至此处,作为全局通知组件 */} {(errorMessage || successMessage) && (
    {errorMessage || successMessage} From 32a9e25faf26460ab7476d8ff18fafc3ef144022 Mon Sep 17 00:00:00 2001 From: gujiangjiang Date: Fri, 10 Oct 2025 04:23:19 +0800 Subject: [PATCH 6/9] =?UTF-8?q?fix:=20=E5=88=87=E6=8D=A2=E5=9F=9F=E5=90=8D?= =?UTF-8?q?=E5=90=8E=E8=87=AA=E5=8A=A8=E6=9B=B4=E6=8D=A2=E9=82=AE=E7=AE=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/HeaderMailbox.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/HeaderMailbox.tsx b/frontend/src/components/HeaderMailbox.tsx index c41bfba..212b791 100644 --- a/frontend/src/components/HeaderMailbox.tsx +++ b/frontend/src/components/HeaderMailbox.tsx @@ -198,8 +198,10 @@ const HeaderMailbox: React.FC = ({ }; // 切换域名 - const handleDomainChange = (e: React.ChangeEvent) => { + const handleDomainChange = async (e: React.ChangeEvent) => { setSelectedDomain(e.target.value); + // [fix] 切换域名后自动更换邮箱 + await handleRefreshMailbox(); }; // 按钮基础样式 From 2741de6756e27cd04fef52a122a22ba32d629bab Mon Sep 17 00:00:00 2001 From: gujiangjiang Date: Fri, 10 Oct 2025 04:28:03 +0800 Subject: [PATCH 7/9] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E5=B7=B2=E4=BF=9D=E5=AD=98=E9=82=AE=E7=AE=B1=E5=92=8C?= =?UTF-8?q?=E4=B8=80=E9=94=AE=E6=B8=85=E7=A9=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/i18n/locales/en.json | 5 +- frontend/i18n/locales/zh-CN.json | 5 +- frontend/src/components/MailboxSwitcher.tsx | 61 +++++++++++++++++---- 3 files changed, 58 insertions(+), 13 deletions(-) diff --git a/frontend/i18n/locales/en.json b/frontend/i18n/locales/en.json index 0a3daf1..cfa4a65 100644 --- a/frontend/i18n/locales/en.json +++ b/frontend/i18n/locales/en.json @@ -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", diff --git a/frontend/i18n/locales/zh-CN.json b/frontend/i18n/locales/zh-CN.json index f1894e3..108b173 100644 --- a/frontend/i18n/locales/zh-CN.json +++ b/frontend/i18n/locales/zh-CN.json @@ -57,7 +57,10 @@ "notFound": "邮箱不存在或已过期", "creatingNew": "正在创建新邮箱...", "switch": "切换邮箱", - "savedMailboxes": "已保存的邮箱" + "savedMailboxes": "已保存的邮箱", + "confirmDeleteMailbox": "您确定要删除这个已保存的邮箱吗?", + "confirmClearAllMailboxes": "您确定要清空所有已保存的邮箱吗?(当前使用的邮箱不会被删除)", + "clearAll": "全部清除" }, "email": { "inbox": "收件箱", diff --git a/frontend/src/components/MailboxSwitcher.tsx b/frontend/src/components/MailboxSwitcher.tsx index ffec4c2..7fc3524 100644 --- a/frontend/src/components/MailboxSwitcher.tsx +++ b/frontend/src/components/MailboxSwitcher.tsx @@ -103,6 +103,27 @@ const MailboxSwitcher: React.FC = ({ setShowDropdown(false); }; + // 删除单个已保存的邮箱 + const handleDeleteMailbox = (address: string) => { + if (window.confirm(t('mailbox.confirmDeleteMailbox'))) { + const updatedMailboxes = savedMailboxes.filter(m => m.address !== address); + setSavedMailboxes(updatedMailboxes); + localStorage.setItem('savedMailboxes', JSON.stringify(updatedMailboxes)); + } + }; + + // 清空所有已保存的邮箱 + const handleClearAllMailboxes = () => { + if (window.confirm(t('mailbox.confirmClearAllMailboxes'))) { + const currentMailboxToKeep = savedMailboxes.find(m => m.address === currentMailbox.address); + const mailboxesToKeep = currentMailboxToKeep ? [currentMailboxToKeep] : []; + setSavedMailboxes(mailboxesToKeep); + localStorage.setItem('savedMailboxes', JSON.stringify(mailboxesToKeep)); + setShowDropdown(false); + } + }; + + // 如果没有保存的邮箱或者只有当前邮箱,不显示切换按钮 if (savedMailboxes.length <= 1) { return null; @@ -120,21 +141,39 @@ const MailboxSwitcher: React.FC = ({ {showDropdown && ( -
    -
    +
    +
    {t('mailbox.savedMailboxes') || "已保存的邮箱"} +
    {savedMailboxes.map((m) => ( - +
    + + {m.address !== currentMailbox.address && ( + + )} +
    ))}
    From 49173333cecf30145e13fa866a4505e96e30eff6 Mon Sep 17 00:00:00 2001 From: gujiangjiang Date: Fri, 10 Oct 2025 04:32:00 +0800 Subject: [PATCH 8/9] =?UTF-8?q?fix:=20=E5=B7=B2=E4=BF=9D=E5=AD=98=E9=82=AE?= =?UTF-8?q?=E7=AE=B1=E4=B8=8B=E6=8B=89=E6=A1=86=E7=9A=84=E9=BB=91=E6=9A=97?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/MailboxSwitcher.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/MailboxSwitcher.tsx b/frontend/src/components/MailboxSwitcher.tsx index 7fc3524..27a665c 100644 --- a/frontend/src/components/MailboxSwitcher.tsx +++ b/frontend/src/components/MailboxSwitcher.tsx @@ -141,7 +141,8 @@ const MailboxSwitcher: React.FC = ({ {showDropdown && ( -
    + // [fix]: 将 bg-white 替换为 bg-popover 和 text-popover-foreground 以支持黑暗模式 +
    {t('mailbox.savedMailboxes') || "已保存的邮箱"}