+ // [fix]: 将 bg-white 替换为 bg-popover 和 text-popover-foreground 以支持黑暗模式
+
+
{t('mailbox.savedMailboxes') || "已保存的邮箱"}
+
+
+ {t('mailbox.clearAll') || "全部清除"}
+
{savedMailboxes.map((m) => (
-
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}
-
+
+ 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}
+
+ {m.address !== currentMailbox.address && (
+ handleDeleteMailbox(m.address)}
+ className="p-2 text-red-500 hover:text-red-700"
+ title={t('common.delete') || "删除"}
+ >
+
+
+ )}
+
))}
diff --git a/frontend/src/components/ThemeSwitcher.tsx b/frontend/src/components/ThemeSwitcher.tsx
new file mode 100644
index 0000000..a6ca5b3
--- /dev/null
+++ b/frontend/src/components/ThemeSwitcher.tsx
@@ -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 状态的变化,并应用到 元素上
+ 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 (
+
+ {/* 根据当前主题显示不同的图标 */}
+ {theme === 'light' ? (
+ // 亮色模式下,显示月亮图标以切换到暗色
+ ) : (
+ // 暗色模式下,显示太阳图标以切换到亮色
+ )}
+
+ );
+};
+
+export default ThemeSwitcher;
\ No newline at end of file
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}
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
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]