feat: support multiple domains

fix: https://github.com/zaunist/zmail/issues/5
This commit is contained in:
zaunist committed 2025-04-18 17:43:32 +08:00
1 parent 0e27a550f3
commit c1828cd62a
12 files changed
+154 -655

No files matched your search

+33 -31
View File
@@ -83,37 +83,7 @@
## 🚀 Quick Deployment
ZMAIL consists of two parts that need to be deployed separately:
### 🖥️ Frontend Deployment
<div align="center">
<h3>1️⃣ Deploy Frontend to Cloudflare Pages</h3>
<a href="https://dash.cloudflare.com/?to=/:account/pages/new/import-git" target="_blank">
<img src="https://img.shields.io/badge/Deploy_to_Cloudflare_Pages-F38020?style=for-the-badge&logo=cloudflare&logoColor=white" alt="Deploy Frontend to Cloudflare Pages" width="300" />
</a>
</div>
<div style="background-color: #2d2d2d; color: #ffffff; padding: 15px; border-radius: 5px; margin: 15px 0;">
<ol>
<li>Click the "Deploy to Cloudflare Pages" button</li>
<li>Connect your GitHub account and select this repository</li>
<li>Configure build settings:
<ul>
<li>Build command: <code>yarn build</code></li>
<li>Build output directory: <code>dist</code></li>
<li>Root directory (Advanced) -> Path: <code>frontend</code></li>
</ul>
</li>
<li>Configure environment variables:
<ul>
<li><code>VITE_API_BASE_URL</code>: Your Worker API base URL (e.g., <code>https://api.mdzz.uk</code>)</li>
<li><code>VITE_EMAIL_DOMAIN</code>: Your Domain (e.g., <code>mdzz.uk</code>)</li>
</ul>
</li>
<li>Click "Save and Deploy"</li>
</ol>
</div>
ZMAIL consists of two parts that need to be deployed separately, deploy the backend first, then the frontend:
### ⚙️ Backend Deployment
@@ -151,6 +121,38 @@ ZMAIL consists of two parts that need to be deployed separately:
---
### 🖥️ Frontend Deployment
<div align="center">
<h3>1️⃣ Deploy Frontend to Cloudflare Pages</h3>
<a href="https://dash.cloudflare.com/?to=/:account/pages/new/import-git" target="_blank">
<img src="https://img.shields.io/badge/Deploy_to_Cloudflare_Pages-F38020?style=for-the-badge&logo=cloudflare&logoColor=white" alt="Deploy Frontend to Cloudflare Pages" width="300" />
</a>
</div>
<div style="background-color: #2d2d2d; color: #ffffff; padding: 15px; border-radius: 5px; margin: 15px 0;">
<ol>
<li>Click the "Deploy to Cloudflare Pages" button</li>
<li>Connect your GitHub account and select this repository</li>
<li>Configure build settings:
<ul>
<li>Build command: <code>yarn build</code></li>
<li>Build output directory: <code>dist</code></li>
<li>Root directory (Advanced) -> Path: <code>frontend</code></li>
</ul>
</li>
<li>Configure environment variables:
<ul>
<li><code>VITE_API_BASE_URL</code>: Your Worker API base URL (e.g., <code>https://api.mdzz.uk</code>)</li>
<li><code>VITE_EMAIL_DOMAIN</code>: Your domain list, separated by ',' (e.g., <code>mdzz.uk,zaunist.com</code>)</li>
</ul>
</li>
<li>Click "Save and Deploy"</li>
</ol>
</div>
---
## 💻 Local Development
### 🎨 Frontend Development
+32 -31
View File
@@ -83,37 +83,7 @@
## 🚀 快速部署
ZMAIL 由前端和后端两部分组成,需要分别部署:
### 🖥️ 前端部署步骤
<div align="center">
<h3>1️⃣ 部署前端到 Cloudflare Pages</h3>
<a href="https://dash.cloudflare.com/?to=/:account/pages/new/import-git" target="_blank">
<img src="https://img.shields.io/badge/部署前端到_Cloudflare_Pages-F38020?style=for-the-badge&logo=cloudflare&logoColor=white" alt="Deploy Frontend to Cloudflare Pages" width="300" />
</a>
</div>
<div style="background-color: #2d2d2d; color: #ffffff; padding: 15px; border-radius: 5px; margin: 15px 0;">
<ol>
<li>点击"部署前端到 Cloudflare Pages"按钮</li>
<li>连接您的GitHub账户并选择导入此仓库</li>
<li>设置构建配置:
<ul>
<li>构建命令: <code>yarn build</code></li>
<li>构建输出目录: <code>dist</code></li>
<li>根目录(高级)-> 路径: <code>frontend</code></li>
</ul>
</li>
<li>配置环境变量:
<ul>
<li><code>VITE_API_BASE_URL</code>: 您的Worker API基础URL (例如: <code>https://api.mdzz.uk</code>)</li>
<li><code>VITE_EMAIL_DOMAIN</code>: 您的域名 (例如: <code>mdzz.uk</code>)</li>
</ul>
</li>
<li>点击"保存并部署"</li>
</ol>
</div>
ZMAIL 由前端和后端两部分组成,需要先部署后端,然后再部署前端:
### ⚙️ 后端部署步骤
@@ -151,6 +121,37 @@ ZMAIL 由前端和后端两部分组成,需要分别部署:
---
### 🖥️ 前端部署步骤
<div align="center">
<h3>1️⃣ 部署前端到 Cloudflare Pages</h3>
<a href="https://dash.cloudflare.com/?to=/:account/pages/new/import-git" target="_blank">
<img src="https://img.shields.io/badge/部署前端到_Cloudflare_Pages-F38020?style=for-the-badge&logo=cloudflare&logoColor=white" alt="Deploy Frontend to Cloudflare Pages" width="300" />
</a>
</div>
<div style="background-color: #2d2d2d; color: #ffffff; padding: 15px; border-radius: 5px; margin: 15px 0;">
<ol>
<li>点击"部署前端到 Cloudflare Pages"按钮</li>
<li>连接您的GitHub账户并选择导入此仓库</li>
<li>设置构建配置:
<ul>
<li>构建命令: <code>yarn build</code></li>
<li>构建输出目录: <code>dist</code></li>
<li>根目录(高级)-> 路径: <code>frontend</code></li>
</ul>
</li>
<li>配置环境变量:
<ul>
<li><code>VITE_API_BASE_URL</code>: 您的Worker API基础URL (例如: <code>https://api.mdzz.uk</code>)</li>
<li><code>VITE_EMAIL_DOMAIN</code>: 您的域名列表,使用','分割 (例如: <code>mdzz.uk,zaunist.com</code>)</li>
</ul>
</li>
<li>点击"保存并部署"</li>
</ol>
</div>
## 💻 本地开发
### 🎨 前端开发
+2 -2
View File
@@ -1,5 +1,5 @@
# 邮箱域名配置
VITE_EMAIL_DOMAIN=example.com
# 邮箱域名配置(多域名以逗号分隔)
VITE_EMAIL_DOMAIN=example.com,abc.com,def.com
# API地址配置
VITE_API_BASE_URL=https://api.example.com
@@ -1,225 +0,0 @@
import React, { useState, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
interface CreateMailboxFormProps {
onMailboxCreated: (address: string) => void;
}
const CreateMailboxForm: React.FC<CreateMailboxFormProps> = ({ onMailboxCreated }) => {
const { t } = useTranslation();
const [isCustom, setIsCustom] = useState(false);
const [customAddress, setCustomAddress] = useState('');
const expiresInHours = 24;
const [isLoading, setIsLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const errorTimeoutRef = useRef<number | null>(null);
const successTimeoutRef = useRef<number | null>(null);
// 清除提示的定时器
useEffect(() => {
return () => {
if (errorTimeoutRef.current) {
window.clearTimeout(errorTimeoutRef.current);
}
if (successTimeoutRef.current) {
window.clearTimeout(successTimeoutRef.current);
}
};
}, []);
const handleCreateRandom = async () => {
try {
// 清除之前的错误和成功信息
setErrorMessage(null);
setSuccessMessage(null);
setIsLoading(true);
const response = await fetch('/api/mailboxes', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
expiresInHours,
}),
});
if (!response.ok) {
throw new Error('Failed to create mailbox');
}
const data = await response.json();
if (data.success) {
setSuccessMessage(t('mailbox.createSuccess'));
// 2秒后调用回调函数
if (successTimeoutRef.current) {
window.clearTimeout(successTimeoutRef.current);
}
successTimeoutRef.current = window.setTimeout(() => {
onMailboxCreated(data.mailbox.address);
}, 2000);
} else {
throw new Error(data.error || 'Unknown error');
}
} catch (error) {
setErrorMessage(t('mailbox.createFailed'));
// 3秒后清除错误信息
if (errorTimeoutRef.current) {
window.clearTimeout(errorTimeoutRef.current);
}
errorTimeoutRef.current = window.setTimeout(() => {
setErrorMessage(null);
}, 3000);
} finally {
setIsLoading(false);
}
};
const handleCreateCustom = async (e: React.FormEvent) => {
e.preventDefault();
// 清除之前的错误和成功信息
setErrorMessage(null);
setSuccessMessage(null);
if (!customAddress.trim()) {
setErrorMessage(t('mailbox.invalidAddress'));
// 3秒后清除错误信息
if (errorTimeoutRef.current) {
window.clearTimeout(errorTimeoutRef.current);
}
errorTimeoutRef.current = window.setTimeout(() => {
setErrorMessage(null);
}, 3000);
return;
}
try {
setIsLoading(true);
const response = await fetch('/api/mailboxes', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
address: customAddress.trim(),
expiresInHours,
}),
});
if (!response.ok) {
if (response.status === 400) {
setErrorMessage(t('mailbox.addressExists'));
// 3秒后清除错误信息
if (errorTimeoutRef.current) {
window.clearTimeout(errorTimeoutRef.current);
}
errorTimeoutRef.current = window.setTimeout(() => {
setErrorMessage(null);
}, 3000);
return;
}
throw new Error('Failed to create mailbox');
}
const data = await response.json();
if (data.success) {
setSuccessMessage(t('mailbox.createSuccess'));
// 2秒后调用回调函数
if (successTimeoutRef.current) {
window.clearTimeout(successTimeoutRef.current);
}
successTimeoutRef.current = window.setTimeout(() => {
onMailboxCreated(data.mailbox.address);
}, 2000);
} else {
throw new Error(data.error || 'Unknown error');
}
} catch (error) {
setErrorMessage(t('mailbox.createFailed'));
// 3秒后清除错误信息
if (errorTimeoutRef.current) {
window.clearTimeout(errorTimeoutRef.current);
}
errorTimeoutRef.current = window.setTimeout(() => {
setErrorMessage(null);
}, 3000);
} finally {
setIsLoading(false);
}
};
return (
<div className="border rounded-lg p-6">
{/* 错误和成功提示 */}
{(errorMessage || successMessage) && (
<div className={`p-3 mb-4 rounded-md ${errorMessage ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800'}`}>
{errorMessage || successMessage}
</div>
)}
<div className="flex mb-6">
<button
type="button"
className={`flex-1 py-2 px-4 rounded-l-md ${
!isCustom ? 'bg-primary text-primary-foreground' : 'bg-muted'
}`}
onClick={() => setIsCustom(false)}
>
{t('mailbox.createRandom')}
</button>
<button
type="button"
className={`flex-1 py-2 px-4 rounded-r-md ${
isCustom ? 'bg-primary text-primary-foreground' : 'bg-muted'
}`}
onClick={() => setIsCustom(true)}
>
{t('mailbox.createCustom')}
</button>
</div>
{isCustom ? (
<form onSubmit={handleCreateCustom}>
<div className="flex flex-col space-y-4">
<div className="flex">
<input
type="text"
value={customAddress}
onChange={(e) => setCustomAddress(e.target.value)}
className="flex-1 px-3 py-2 border rounded-l-md focus:outline-none focus:ring-2 focus:ring-primary"
placeholder={t('mailbox.customAddressPlaceholder')}
disabled={isLoading}
/>
<button
type="submit"
className="px-4 py-2 bg-primary text-primary-foreground rounded-r-md hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary"
disabled={isLoading}
>
{isLoading ? t('common.loading') : t('common.create')}
</button>
</div>
</div>
</form>
) : (
<button
type="button"
onClick={handleCreateRandom}
className="w-full px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary"
disabled={isLoading}
>
{isLoading ? t('common.loading') : t('mailbox.createRandom')}
</button>
)}
</div>
);
};
export default CreateMailboxForm;
+3 -2
View File
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import LanguageSwitcher from './LanguageSwitcher';
import HeaderMailbox from './HeaderMailbox';
import Container from './Container';
import { EMAIL_DOMAIN } from '../config';
import { EMAIL_DOMAINS, DEFAULT_EMAIL_DOMAIN } from '../config';
interface HeaderProps {
mailbox: Mailbox | null;
@@ -32,7 +32,8 @@ const Header: React.FC<HeaderProps> = ({
<HeaderMailbox
mailbox={mailbox}
onMailboxChange={onMailboxChange}
domain={EMAIL_DOMAIN}
domain={DEFAULT_EMAIL_DOMAIN}
domains={EMAIL_DOMAINS}
isLoading={isLoading}
/>
<div className="ml-3 pl-3 border-l border-muted-foreground/20 flex items-center">
+79 -49
View File
@@ -6,6 +6,7 @@ interface HeaderMailboxProps {
mailbox: Mailbox | null;
onMailboxChange: (mailbox: Mailbox) => void;
domain: string;
domains: string[];
isLoading: boolean;
}
@@ -13,11 +14,13 @@ const HeaderMailbox: React.FC<HeaderMailboxProps> = ({
mailbox,
onMailboxChange,
domain,
domains,
isLoading
}) => {
const { t } = useTranslation();
const [isCustomMode, setIsCustomMode] = useState(false);
const [customAddress, setCustomAddress] = useState('');
const [selectedDomain, setSelectedDomain] = useState(domain);
const [isActionLoading, setIsActionLoading] = useState(false);
const [showCopyTooltip, setShowCopyTooltip] = useState(false);
const [copyError, setCopyError] = useState<string | null>(null);
@@ -30,6 +33,11 @@ const HeaderMailbox: React.FC<HeaderMailboxProps> = ({
const successMessageTimeoutRef = useRef<number | null>(null);
const errorMessageTimeoutRef = useRef<number | null>(null);
// 当props中的domain变化时更新selectedDomain
useEffect(() => {
setSelectedDomain(domain);
}, [domain]);
// 清除提示的定时器
useEffect(() => {
return () => {
@@ -55,7 +63,7 @@ const HeaderMailbox: React.FC<HeaderMailboxProps> = ({
// 清除之前的错误信息
setCopyError(null);
const fullAddress = mailbox.address.includes('@') ? mailbox.address : `${mailbox.address}@${domain}`;
const fullAddress = mailbox.address.includes('@') ? mailbox.address : `${mailbox.address}@${selectedDomain}`;
navigator.clipboard.writeText(fullAddress)
.then(() => {
// 显示复制成功提示
@@ -175,7 +183,7 @@ const HeaderMailbox: React.FC<HeaderMailboxProps> = ({
// 移动设备上的邮箱地址显示
const renderMobileAddress = () => {
const fullAddress = mailbox.address.includes('@') ? mailbox.address : `${mailbox.address}@${domain}`;
const fullAddress = mailbox.address.includes('@') ? mailbox.address : `${mailbox.address}@${selectedDomain}`;
const [username, domainPart] = fullAddress.split('@');
// 如果用户名太长,截断显示
@@ -188,6 +196,11 @@ const HeaderMailbox: React.FC<HeaderMailboxProps> = ({
);
};
// 切换域名
const handleDomainChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedDomain(e.target.value);
};
// 按钮基础样式
const buttonBaseClass = "flex items-center justify-center rounded-md transition-all duration-200";
const copyButtonClass = `${buttonBaseClass} hover:bg-primary/20 hover:text-primary hover:scale-110 mx-1`;
@@ -215,8 +228,17 @@ const HeaderMailbox: React.FC<HeaderMailboxProps> = ({
disabled={isActionLoading}
autoFocus
/>
<span className="px-2 py-1 text-sm border-y border-r rounded-r-md bg-muted">
@{domain}
<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>
</span>
</div>
<button
@@ -252,21 +274,29 @@ const HeaderMailbox: React.FC<HeaderMailboxProps> = ({
</form>
) : (
<>
{/* 桌面版显示 */}
<div className="hidden md:flex items-center flex-col">
{/* 邮箱地址和操作按钮 */}
<div className="flex items-center">
<code className="px-3 py-1.5 text-sm font-medium">
{mailbox.address.includes('@') ? mailbox.address : `${mailbox.address}@${domain}`}
<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">
{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>
</code>
<div className="relative">
<button
onClick={copyToClipboard}
<button
onClick={copyToClipboard}
className={`w-8 h-8 ${copyButtonClass}`}
aria-label={t('common.copy')}
title={t('common.copy')}
>
>
<i className="fas fa-copy text-sm"></i>
</button>
@@ -323,12 +353,12 @@ const HeaderMailbox: React.FC<HeaderMailboxProps> = ({
{renderMobileAddress()}
<div className="relative">
<button
onClick={copyToClipboard}
<button
onClick={copyToClipboard}
className={`w-6 h-6 ${copyButtonClass}`}
aria-label={t('common.copy')}
title={t('common.copy')}
>
>
<i className="fas fa-copy text-xs"></i>
</button>
@@ -340,43 +370,43 @@ const HeaderMailbox: React.FC<HeaderMailboxProps> = ({
</div>
)}
</div>
<div className="relative">
<button
onClick={handleRefreshMailbox}
className={`w-6 h-6 ${refreshButtonClass}`}
disabled={isActionLoading}
title={t('mailbox.refresh')}
>
<i className="fas fa-sync-alt text-xs"></i>
</button>
{/* 更新成功提示 */}
{showRefreshSuccess && (
<div className="absolute top-7 left-1/2 transform -translate-x-1/2 bg-primary text-primary-foreground text-xs py-1 px-2 rounded shadow-lg whitespace-nowrap z-10">
{t('mailbox.refreshSuccess')}
<div className="absolute -top-1 left-1/2 transform -translate-x-1/2 w-2 h-2 bg-primary rotate-45"></div>
</div>
)}
</div>
<button
onClick={() => setIsCustomMode(true)}
className={`w-6 h-6 ${customizeButtonClass}`}
disabled={isActionLoading}
title={t('mailbox.customize')}
>
<i className="fas fa-edit text-xs"></i>
</button>
</div>
{/* 错误信息显示 */}
{(copyError || refreshError) && (
<div className="relative">
<button
onClick={handleRefreshMailbox}
className={`w-6 h-6 ${refreshButtonClass}`}
disabled={isActionLoading}
title={t('mailbox.refresh')}
>
<i className="fas fa-sync-alt text-xs"></i>
</button>
{/* 更新成功提示 */}
{showRefreshSuccess && (
<div className="absolute top-7 left-1/2 transform -translate-x-1/2 bg-primary text-primary-foreground text-xs py-1 px-2 rounded shadow-lg whitespace-nowrap z-10">
{t('mailbox.refreshSuccess')}
<div className="absolute -top-1 left-1/2 transform -translate-x-1/2 w-2 h-2 bg-primary rotate-45"></div>
</div>
)}
</div>
<button
onClick={() => setIsCustomMode(true)}
className={`w-6 h-6 ${customizeButtonClass}`}
disabled={isActionLoading}
title={t('mailbox.customize')}
>
<i className="fas fa-edit text-xs"></i>
</button>
</div>
{/* 错误信息显示 */}
{(copyError || refreshError) && (
<div className="text-red-500 text-xs mt-1">
{copyError || refreshError}
</div>
{copyError || refreshError}
</div>
)}
</div>
</>
)}
</div>
+2 -1
View File
@@ -1,7 +1,8 @@
// 配置文件,用于管理域名和API地址设置
// 邮箱域名配置
export const EMAIL_DOMAIN = import.meta.env.VITE_EMAIL_DOMAIN;
export const EMAIL_DOMAINS = (import.meta.env.VITE_EMAIL_DOMAIN || '').split(',').map(domain => domain.trim());
export const DEFAULT_EMAIL_DOMAIN = EMAIL_DOMAINS[0] || '';
// API地址配置
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
+1 -2
View File
@@ -7,8 +7,7 @@ import {
EmailListItem,
Attachment,
AttachmentListItem,
SaveAttachmentParams,
AttachmentChunk
SaveAttachmentParams
} from './types';
import {
generateId,
-93
View File
@@ -1,93 +0,0 @@
import { generateId } from './utils';
/**
* 构建MIME格式的邮件
* @param fromAddress 发件人地址
* @param toAddress 收件人地址
* @param toName 收件人名称
* @param subject 邮件主题
* @param textContent 纯文本内容
* @param htmlContent HTML内容
* @returns 原始MIME格式邮件
*/
export function buildMimeEmail(
fromAddress: string,
toAddress: string,
toName: string = '',
subject: string,
textContent: string = '',
htmlContent: string = ''
): string {
// 生成邮件边界
const boundary = `----=_Part_${Date.now().toString(16)}`;
// 构建邮件头
const headers = [
`From: ${fromAddress}`,
`To: ${toName ? `${toName} <${toAddress}>` : toAddress}`,
`Subject: ${subject}`,
`Date: ${new Date().toUTCString()}`,
`Message-ID: <${generateId()}@${fromAddress.split('@')[1]}>`,
'MIME-Version: 1.0',
`Content-Type: multipart/alternative; boundary="${boundary}"`,
'',
''
].join('\r\n');
// 构建邮件内容
let body = '';
// 添加纯文本部分
if (textContent) {
body += [
`--${boundary}`,
'Content-Type: text/plain; charset=UTF-8',
'Content-Transfer-Encoding: 7bit',
'',
textContent,
''
].join('\r\n');
}
// 添加HTML部分
if (htmlContent) {
body += [
`--${boundary}`,
'Content-Type: text/html; charset=UTF-8',
'Content-Transfer-Encoding: 7bit',
'',
htmlContent,
''
].join('\r\n');
}
// 添加结束边界
body += `--${boundary}--\r\n`;
// 完整的原始邮件
return headers + body;
}
/**
* 解析原始邮件内容
* @param rawEmail 原始邮件内容
* @returns 解析后的邮件对象
*/
export async function parseRawEmail(rawEmail: ArrayBuffer): Promise<any> {
try {
// 动态导入PostalMime
const PostalMimeModule = await import('postal-mime');
// 获取构造函数
const PostalMimeConstructor = PostalMimeModule.default || PostalMimeModule;
// 创建解析器实例
const parser = new PostalMimeConstructor();
// 解析邮件
return await parser.parse(rawEmail);
} catch (error) {
console.error('PostalMime解析错误:', error);
throw error; // 重新抛出错误,让调用者处理
}
}
-2
View File
@@ -43,7 +43,6 @@ export default {
// 处理邮件
async email(message: any, env: Env, ctx: ExecutionContext): Promise<void> {
try {
await initializeDatabase(env.DB);
await handleEmail(message, env);
} catch (error) {
console.error('处理邮件失败:', error);
@@ -54,7 +53,6 @@ export default {
// 定时任务 - 每小时清理过期邮箱以及过期邮件和已被阅读的邮件
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
try {
await initializeDatabase(env.DB);
const deleted = await cleanupExpiredMailboxes(env.DB);
console.log(`已清理 ${deleted} 个过期邮箱`);
const deletedMail = await cleanupExpiredMails(env.DB);
+2 -129
View File
@@ -1,6 +1,6 @@
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { Env, SendEmailParams, ParsedEmail } from './types';
import { Env} from './types';
import {
createMailbox,
getMailbox,
@@ -11,8 +11,7 @@ import {
getAttachments,
getAttachment
} from './database';
import { generateRandomAddress, isValidEmailAddress, sendEmail } from './utils';
import { parseRawEmail } from './email-builder';
import { generateRandomAddress, } from './utils';
// 创建 Hono 应用
const app = new Hono<{ Bindings: Env }>();
@@ -30,123 +29,6 @@ app.get('/', (c) => {
return c.json({ status: 'ok', message: '临时邮箱系统API正常运行' });
});
// 发送邮件API,暂时不用
app.post('/api/send-email', async (c) => {
try {
const body = await c.req.json() as SendEmailParams;
// 验证必要参数
if (!body.fromAddress || typeof body.fromAddress !== 'string') {
return c.json({ success: false, error: '发件人地址不能为空' }, 400);
}
if (!body.toAddress || typeof body.toAddress !== 'string') {
return c.json({ success: false, error: '收件人地址不能为空' }, 400);
}
if (!isValidEmailAddress(body.toAddress)) {
return c.json({ success: false, error: '收件人邮箱地址格式无效' }, 400);
}
if (!body.subject || typeof body.subject !== 'string') {
return c.json({ success: false, error: '邮件主题不能为空' }, 400);
}
if (!body.textContent && !body.htmlContent) {
return c.json({ success: false, error: '邮件内容不能为空' }, 400);
}
// // 查找发件人邮箱是否存在
// const mailbox = await getMailbox(c.env.DB, body.fromAddress);
// if (!mailbox) {
// return c.json({ success: false, error: '发件人邮箱不存在' }, 404);
// }
// 发送邮件
const result = await sendEmail(
body.fromAddress,
body.toAddress,
body.toName || '',
body.subject,
body.textContent || '',
body.htmlContent || ''
);
if (result.success) {
return c.json({
success: true,
message: '邮件发送成功'
});
} else {
return c.json({
success: false,
error: result.error || '邮件发送失败'
}, 500);
}
} catch (error) {
console.error('发送邮件失败:', error);
return c.json({
success: false,
error: '发送邮件失败',
message: error instanceof Error ? error.message : String(error)
}, 500);
}
});
// 接收原始邮件并解析,用于测试解析邮件功能
app.post('/api/parse-email', async (c) => {
try {
console.log('接收到解析邮件请求');
// 获取请求体中的原始邮件内容
const body = await c.req.text();
if (!body) {
console.log('未提供邮件内容');
return c.json({ success: false, error: '未提供邮件内容' }, 400);
}
console.log(`接收到邮件内容,长度: ${body.length} 字节`);
// 将文本内容转换为ArrayBuffer
const encoder = new TextEncoder();
const rawEmail = encoder.encode(body).buffer;
// 使用PostalMime解析邮件
const parsedEmail = await parseRawEmail(rawEmail) as ParsedEmail;
console.log('邮件解析成功:', {
subject: parsedEmail.subject,
from: parsedEmail.from,
to: parsedEmail.to?.length || 0,
hasText: !!parsedEmail.text,
hasHtml: !!parsedEmail.html,
attachments: parsedEmail.attachments?.length || 0
});
// 返回解析结果
return c.json({
success: true,
email: {
subject: parsedEmail.subject,
from: parsedEmail.from,
to: parsedEmail.to,
text: parsedEmail.text,
html: parsedEmail.html,
hasAttachments: !!parsedEmail.attachments?.length,
attachmentsCount: parsedEmail.attachments?.length || 0
}
});
} catch (error) {
console.error('解析邮件失败:', error);
return c.json({
success: false,
error: '解析邮件失败',
message: error instanceof Error ? error.message : String(error)
}, 500);
}
});
// 创建邮箱
app.post('/api/mailboxes', async (c) => {
@@ -366,13 +248,4 @@ app.delete('/api/emails/:id', async (c) => {
}
});
// 调试端点
app.get('/api/debug/db', (c) => {
return c.json({
dbDefined: !!c.env.DB,
dbType: typeof c.env.DB,
dbMethods: c.env.DB ? Object.keys(c.env.DB) : []
});
});
export default app;
-88
View File
@@ -76,91 +76,3 @@ export function generateRandomString(length: number): string {
const date = new Date(timestamp * 1000);
return date.toISOString();
}
/**
* 发送邮件
* @param fromAddress 发件人地址
* @param toAddress 收件人地址
* @param toName 收件人名称
* @param subject 邮件主题
* @param textContent 纯文本内容
* @param htmlContent HTML内容
* @returns 发送结果
*/
export async function sendEmail(
fromAddress: string,
toAddress: string,
toName: string = '',
subject: string,
textContent: string = '',
htmlContent: string = ''
): Promise<{ success: boolean; message?: string; error?: string }> {
try {
// 导入buildMimeEmail函数
const { buildMimeEmail } = await import('./email-builder');
// 构建MIME格式邮件
const rawEmail = buildMimeEmail(
fromAddress,
toAddress,
toName,
subject,
textContent,
htmlContent
);
// 使用MailChannels API发送邮件
const response = await fetch('https://api.mailchannels.net/tx/v1/send', {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
personalizations: [
{
to: [{ email: toAddress, name: toName || undefined }],
},
],
from: {
email: fromAddress,
name: extractMailboxName(fromAddress),
},
subject: subject,
content: [
{
type: 'text/plain',
value: textContent,
},
...(htmlContent
? [
{
type: 'text/html',
value: htmlContent,
},
]
: []),
],
headers: {
'X-Generated-By': 'TempMail-System',
'X-Mime-Version': '1.0',
},
raw_mime: btoa(rawEmail),
}),
});
if (response.ok) {
return { success: true, message: '邮件发送成功' };
} else {
const errorData = await response.json() as { errors?: string[] };
return {
success: false,
error: `邮件发送失败: ${errorData.errors?.[0] || response.statusText}`,
};
}
} catch (error) {
return {
success: false,
error: `邮件发送失败: ${error instanceof Error ? error.message : String(error)}`,
};
}
}