feat: support multiple domains
fix: https://github.com/zaunist/zmail/issues/5
This commit is contained in:
1 parent
0e27a550f3
commit
c1828cd62a
12 files changed
+154
-655
No files matched your search
+33
-31
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
## 💻 本地开发
|
||||
|
||||
### 🎨 前端开发
|
||||
|
||||
@@ -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;
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -7,8 +7,7 @@ import {
|
||||
EmailListItem,
|
||||
Attachment,
|
||||
AttachmentListItem,
|
||||
SaveAttachmentParams,
|
||||
AttachmentChunk
|
||||
SaveAttachmentParams
|
||||
} from './types';
|
||||
import {
|
||||
generateId,
|
||||
|
||||
@@ -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; // 重新抛出错误,让调用者处理
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
@@ -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)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user