feat: Implement core backend API server with configuration, S2A proxy, and mail service APIs, and introduce EmailConfig frontend page.

This commit is contained in:
2026-01-30 16:03:07 +08:00
parent 2e10b900fa
commit e61430b60d
2 changed files with 151 additions and 109 deletions

View File

@@ -344,16 +344,19 @@ func handleMailServices(w http.ResponseWriter, r *http.Request) {
switch r.Method { switch r.Method {
case "GET": case "GET":
services := mail.GetServices() services := mail.GetServices()
safeServices := make([]map[string]interface{}, len(services)) // 返回完整配置(包括 token供前端加载
result := make([]map[string]interface{}, len(services))
for i, s := range services { for i, s := range services {
safeServices[i] = map[string]interface{}{ result[i] = map[string]interface{}{
"name": s.Name, "name": s.Name,
"api_base": s.APIBase, "apiBase": s.APIBase,
"has_token": s.APIToken != "", "apiToken": s.APIToken,
"domain": s.Domain, "domain": s.Domain,
"emailPath": s.EmailPath,
"addUserApi": s.AddUserAPI,
} }
} }
api.Success(w, safeServices) api.Success(w, result)
case "POST": case "POST":
var req struct { var req struct {
Services []struct { Services []struct {
@@ -393,7 +396,7 @@ func handleMailServices(w http.ResponseWriter, r *http.Request) {
}) })
} }
// 更新邮箱服务配置 // 更新邮箱服务配置(内存)
mail.Init(services) mail.Init(services)
// 保存到全局配置 // 保存到全局配置
@@ -401,6 +404,14 @@ func handleMailServices(w http.ResponseWriter, r *http.Request) {
config.Global.MailServices = services config.Global.MailServices = services
} }
// 持久化到数据库
if database.Instance != nil {
jsonData, _ := json.Marshal(services)
if err := database.Instance.SetConfig("mail_services", string(jsonData)); err != nil {
logger.Error(fmt.Sprintf("保存邮箱配置到数据库失败: %v", err), "", "mail")
}
}
logger.Success(fmt.Sprintf("邮箱服务配置已保存: %d 个服务", len(services)), "", "mail") logger.Success(fmt.Sprintf("邮箱服务配置已保存: %d 个服务", len(services)), "", "mail")
for _, s := range services { for _, s := range services {
logger.Info(fmt.Sprintf(" - %s (%s) @ %s", s.Name, s.Domain, s.APIBase), "", "mail") logger.Info(fmt.Sprintf(" - %s (%s) @ %s", s.Name, s.Domain, s.APIBase), "", "mail")
@@ -483,8 +494,15 @@ func handleTestMailService(w http.ResponseWriter, r *http.Request) {
} }
// 判断结果 // 判断结果
if result.Code == 200 || strings.Contains(result.Message, "exist") { // code 200 = 创建成功
logger.Success(fmt.Sprintf("邮箱服务测试成功: %s", req.Name), "", "mail") // code 501 且消息包含"已存在" = 邮箱已存在,说明连接正常
isSuccess := result.Code == 200 ||
strings.Contains(result.Message, "exist") ||
strings.Contains(result.Message, "已存在") ||
(result.Code == 501 && strings.Contains(result.Message, "邮箱"))
if isSuccess {
logger.Success(fmt.Sprintf("邮箱服务测试成功: %s (邮箱已存在或创建成功)", req.Name), "", "mail")
api.Success(w, map[string]interface{}{ api.Success(w, map[string]interface{}{
"connected": true, "connected": true,
"message": "邮箱服务连接成功", "message": "邮箱服务连接成功",

View File

@@ -5,21 +5,36 @@ import { useConfig } from '../hooks/useConfig'
import type { MailServiceConfig } from '../types' import type { MailServiceConfig } from '../types'
export default function EmailConfig() { export default function EmailConfig() {
const { config, updateEmailConfig } = useConfig() const { updateEmailConfig } = useConfig()
const toast = useToast() const toast = useToast()
const [saved, setSaved] = useState(false) const [saved, setSaved] = useState(false)
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [services, setServices] = useState<MailServiceConfig[]>(config.email?.services || []) const [loading, setLoading] = useState(true)
const [services, setServices] = useState<MailServiceConfig[]>([])
const [testingIndex, setTestingIndex] = useState<number | null>(null) const [testingIndex, setTestingIndex] = useState<number | null>(null)
const [testResults, setTestResults] = useState<Record<number, { success: boolean; message: string }>>({}) const [testResults, setTestResults] = useState<Record<number, { success: boolean; message: string }>>({})
// 同步配置变化 // 从后端加载配置
useEffect(() => { useEffect(() => {
if (config.email?.services) { const loadConfig = async () => {
setServices(config.email.services) try {
const res = await fetch('/api/mail/services')
if (res.ok) {
const data = await res.json()
if (data.code === 0 && Array.isArray(data.data)) {
setServices(data.data)
updateEmailConfig({ services: data.data })
}
}
} catch (e) {
console.error('加载邮箱配置失败:', e)
}
setLoading(false)
} }
}, [config.email?.services]) loadConfig()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const handleSave = async () => { const handleSave = async () => {
if (services.length === 0) { if (services.length === 0) {
@@ -146,106 +161,115 @@ export default function EmailConfig() {
</div> </div>
{/* Service Cards */} {/* Service Cards */}
<div className="space-y-4"> {loading ? (
{services.map((service, index) => ( <Card>
<Card key={index}> <CardContent className="flex items-center justify-center py-8">
<CardHeader> <Loader2 className="h-6 w-6 animate-spin text-slate-400" />
<div className="flex items-center justify-between"> <span className="ml-2 text-slate-500">...</span>
<CardTitle className="flex items-center gap-2"> </CardContent>
<Server className="h-5 w-5 text-purple-500" /> </Card>
<span>{service.name || `服务 ${index + 1}`}</span> ) : (
<span className="text-sm font-normal text-slate-500"> <div className="space-y-4">
(@{service.domain || '未设置域名'}) {services.map((service, index) => (
</span> <Card key={index}>
</CardTitle> <CardHeader>
<div className="flex items-center gap-2"> <div className="flex items-center justify-between">
{testResults[index] && ( <CardTitle className="flex items-center gap-2">
<span className={`text-sm ${testResults[index].success ? 'text-green-500' : 'text-red-500'}`}> <Server className="h-5 w-5 text-purple-500" />
{testResults[index].message} <span>{service.name || `服务 ${index + 1}`}</span>
<span className="text-sm font-normal text-slate-500">
(@{service.domain || '未设置域名'})
</span> </span>
)} </CardTitle>
<Button <div className="flex items-center gap-2">
variant="ghost" {testResults[index] && (
size="sm" <span className={`text-sm ${testResults[index].success ? 'text-green-500' : 'text-red-500'}`}>
onClick={() => handleTestService(index)} {testResults[index].message}
disabled={testingIndex === index || !service.apiBase} </span>
icon={testingIndex === index ? <Loader2 className="h-4 w-4 animate-spin" /> : <TestTube className="h-4 w-4" />} )}
> <Button
variant="ghost"
</Button> size="sm"
<Button onClick={() => handleTestService(index)}
variant="ghost" disabled={testingIndex === index || !service.apiBase}
size="sm" icon={testingIndex === index ? <Loader2 className="h-4 w-4 animate-spin" /> : <TestTube className="h-4 w-4" />}
onClick={() => handleRemoveService(index)} >
disabled={services.length <= 1}
icon={<Trash2 className="h-4 w-4" />} </Button>
className="text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20" <Button
> variant="ghost"
size="sm"
</Button> onClick={() => handleRemoveService(index)}
disabled={services.length <= 1}
icon={<Trash2 className="h-4 w-4" />}
className="text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20"
>
</Button>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
label="服务名称"
placeholder="如:主邮箱服务"
value={service.name}
onChange={(e) => handleUpdateService(index, { name: e.target.value })}
hint="用于识别不同的邮箱服务"
/>
<Input
label="邮箱域名"
placeholder="如example.com"
value={service.domain}
onChange={(e) => handleUpdateService(index, { domain: e.target.value })}
hint="生成邮箱地址的域名后缀"
/>
</div> </div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input <Input
label="服务名称" label="API 地址"
placeholder="如:主邮箱服务" placeholder="https://mail.example.com"
value={service.name} value={service.apiBase}
onChange={(e) => handleUpdateService(index, { name: e.target.value })} onChange={(e) => handleUpdateService(index, { apiBase: e.target.value })}
hint="用于识别不同的邮箱服务" hint="邮箱服务 API 地址"
/> />
<Input <Input
label="邮箱域名" label="API Token"
placeholder="如example.com" type="password"
value={service.domain} placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
onChange={(e) => handleUpdateService(index, { domain: e.target.value })} value={service.apiToken}
hint="生成邮箱地址的域名后缀" onChange={(e) => handleUpdateService(index, { apiToken: e.target.value })}
hint="邮箱服务的 API 认证令牌"
/> />
</div>
<Input
label="API 地址"
placeholder="https://mail.example.com"
value={service.apiBase}
onChange={(e) => handleUpdateService(index, { apiBase: e.target.value })}
hint="邮箱服务 API 地址"
/>
<Input
label="API Token"
type="password"
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
value={service.apiToken}
onChange={(e) => handleUpdateService(index, { apiToken: e.target.value })}
hint="邮箱服务的 API 认证令牌"
/>
{/* Advanced Settings (Collapsed by default) */} {/* Advanced Settings (Collapsed by default) */}
<details className="group"> <details className="group">
<summary className="cursor-pointer text-sm text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 flex items-center gap-1"> <summary className="cursor-pointer text-sm text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 flex items-center gap-1">
<Settings className="h-4 w-4" /> <Settings className="h-4 w-4" />
</summary> </summary>
<div className="mt-4 space-y-4 pl-5 border-l-2 border-slate-200 dark:border-slate-700"> <div className="mt-4 space-y-4 pl-5 border-l-2 border-slate-200 dark:border-slate-700">
<Input <Input
label="邮件列表 API 路径" label="邮件列表 API 路径"
placeholder="/api/public/emailList (默认)" placeholder="/api/public/emailList (默认)"
value={service.emailPath || ''} value={service.emailPath || ''}
onChange={(e) => handleUpdateService(index, { emailPath: e.target.value })} onChange={(e) => handleUpdateService(index, { emailPath: e.target.value })}
hint="获取邮件列表的 API 路径" hint="获取邮件列表的 API 路径"
/> />
<Input <Input
label="创建用户 API 路径" label="创建用户 API 路径"
placeholder="/api/public/addUser (默认)" placeholder="/api/public/addUser (默认)"
value={service.addUserApi || ''} value={service.addUserApi || ''}
onChange={(e) => handleUpdateService(index, { addUserApi: e.target.value })} onChange={(e) => handleUpdateService(index, { addUserApi: e.target.value })}
hint="创建邮箱用户的 API 路径" hint="创建邮箱用户的 API 路径"
/> />
</div> </div>
</details> </details>
</CardContent> </CardContent>
</Card> </Card>
))} ))}
</div> </div>
)}
{/* Help Info */} {/* Help Info */}
<Card> <Card>