fix(smtp): 适配内置 Postfix 发信链路

- 后端按 SMTP 端口区分普通连接、STARTTLS 和隐式 TLS,避免发信流程在不同中继场景下异常。
- 前端调整管理员页的发信说明与字段文案,强调默认使用内置 Postfix。
This commit is contained in:
LanQin
2026-06-16 22:10:15 +08:00
parent 57fa0d0b57
commit b9b76c6b8c
2 changed files with 68 additions and 13 deletions
+50 -4
View File
@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"crypto/tls" "crypto/tls"
"encoding/base64" "encoding/base64"
"errors"
"fmt" "fmt"
"io" "io"
"mime" "mime"
@@ -132,18 +133,63 @@ func sendSMTPWithConfig(cfg Config, from string, recipients []string, mimeBytes
auth = smtp.PlainAuth("", cfg.SMTPUsername, cfg.SMTPPassword, cfg.SMTPHost) auth = smtp.PlainAuth("", cfg.SMTPUsername, cfg.SMTPPassword, cfg.SMTPHost)
} }
if !cfg.SMTPRequireTLS { if !cfg.SMTPRequireTLS {
return smtp.SendMail(addr, auth, from, recipients, mimeBytes) return sendSMTPPlain(addr, cfg.SMTPHost, auth, from, recipients, mimeBytes)
} }
conn, err := tls.Dial("tcp", addr, &tls.Config{ServerName: cfg.SMTPHost, MinVersion: tls.VersionTLS12}) if cfg.SMTPPort == "465" {
return sendSMTPImplicitTLS(addr, cfg.SMTPHost, auth, from, recipients, mimeBytes)
}
return sendSMTPStartTLS(addr, cfg.SMTPHost, auth, from, recipients, mimeBytes)
}
func sendSMTPPlain(addr, host string, auth smtp.Auth, from string, recipients []string, mimeBytes []byte) error {
conn, err := net.Dial("tcp", addr)
if err != nil { if err != nil {
return err return err
} }
defer conn.Close() client, err := smtp.NewClient(conn, host)
client, err := smtp.NewClient(conn, cfg.SMTPHost)
if err != nil { if err != nil {
_ = conn.Close()
return err return err
} }
defer client.Close() defer client.Close()
return sendSMTPMessage(client, auth, from, recipients, mimeBytes)
}
func sendSMTPImplicitTLS(addr, host string, auth smtp.Auth, from string, recipients []string, mimeBytes []byte) error {
conn, err := tls.Dial("tcp", addr, &tls.Config{ServerName: host, MinVersion: tls.VersionTLS12})
if err != nil {
return err
}
client, err := smtp.NewClient(conn, host)
if err != nil {
_ = conn.Close()
return err
}
defer client.Close()
return sendSMTPMessage(client, auth, from, recipients, mimeBytes)
}
func sendSMTPStartTLS(addr, host string, auth smtp.Auth, from string, recipients []string, mimeBytes []byte) error {
conn, err := net.Dial("tcp", addr)
if err != nil {
return err
}
client, err := smtp.NewClient(conn, host)
if err != nil {
_ = conn.Close()
return err
}
defer client.Close()
if ok, _ := client.Extension("STARTTLS"); !ok {
return errors.New("smtp server does not support STARTTLS")
}
if err := client.StartTLS(&tls.Config{ServerName: host, MinVersion: tls.VersionTLS12}); err != nil {
return err
}
return sendSMTPMessage(client, auth, from, recipients, mimeBytes)
}
func sendSMTPMessage(client *smtp.Client, auth smtp.Auth, from string, recipients []string, mimeBytes []byte) error {
if auth != nil { if auth != nil {
if err := client.Auth(auth); err != nil { if err := client.Auth(auth); err != nil {
return err return err
+18 -9
View File
@@ -9,7 +9,7 @@ import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox"
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog" import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
@@ -148,7 +148,7 @@ function setupChecklist(overview: { activeUsers: number; activeMailboxes: number
{ key: "domain", title: "添加邮件域名", detail: hasDomain ? `${domains.length} 个域名已添加` : "先添加 example.com 这样的邮件域名", done: hasDomain, section: "domains" as Section }, { key: "domain", title: "添加邮件域名", detail: hasDomain ? `${domains.length} 个域名已添加` : "先添加 example.com 这样的邮件域名", done: hasDomain, section: "domains" as Section },
{ key: "dns", title: "完成 DNS 检测", detail: dnsReady ? "至少一个域名 DNS 正常" : "配置 MX、SPF、DKIM、DMARC 后执行检测", done: dnsReady, section: "domains" as Section }, { key: "dns", title: "完成 DNS 检测", detail: dnsReady ? "至少一个域名 DNS 正常" : "配置 MX、SPF、DKIM、DMARC 后执行检测", done: dnsReady, section: "domains" as Section },
{ key: "mailbox", title: "创建邮箱账号", detail: hasMailbox ? `${overview?.activeMailboxes || 0} 个活跃邮箱` : "给管理员或用户创建第一个邮箱", done: hasMailbox, section: "mailboxes" as Section }, { key: "mailbox", title: "创建邮箱账号", detail: hasMailbox ? `${overview?.activeMailboxes || 0} 个活跃邮箱` : "给管理员或用户创建第一个邮箱", done: hasMailbox, section: "mailboxes" as Section },
{ key: "smtp", title: "确认发信配置", detail: settings?.smtpHost ? `${settings.smtpHost}:${settings.smtpPort}` : "配置本机 Postfix 或外部 SMTP", done: !!settings?.smtpHost, section: "settings" as Section }, { key: "smtp", title: "确认发信链路", detail: settings?.smtpHost ? `内置 Postfix${settings.smtpHost}:${settings.smtpPort}` : "默认使用内置 Postfix", done: true, section: "settings" as Section },
{ key: "mail", title: "完成收发测试", detail: hasMail ? `${overview?.messages || 0} 封邮件已入库` : "发送或接收一封测试邮件", done: hasMail, section: "messages" as Section }, { key: "mail", title: "完成收发测试", detail: hasMail ? `${overview?.messages || 0} 封邮件已入库` : "发送或接收一封测试邮件", done: hasMail, section: "messages" as Section },
] ]
} }
@@ -569,16 +569,25 @@ function SystemSettingsSection({ settings, domains }: { settings?: SystemSetting
{settingsTab === "smtp" && <Card> {settingsTab === "smtp" && <Card>
<CardHeader> <CardHeader>
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<CardTitle>SMTP </CardTitle> <div>
<CardTitle></CardTitle>
<CardDescription>使 Postfix127.0.0.1:25 SMTP</CardDescription>
</div>
<TestSMTPDialog disabled={!settings} /> <TestSMTPDialog disabled={!settings} />
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="grid gap-4 md:grid-cols-2"> <CardContent className="space-y-4">
<Field name="smtpHost" label="SMTP 主机" defaultValue={settings?.smtpHost || ""} required={false} /> <div className="rounded-lg border bg-muted/30 p-4 text-sm text-muted-foreground">
<Field name="smtpPort" label="SMTP 端口" defaultValue={settings?.smtpPort || "25"} /> <div className="font-medium text-foreground"> Postfix</div>
<Field name="smtpUsername" label="SMTP 用户名" defaultValue={settings?.smtpUsername || ""} required={false} /> <div>Host 127.0.0.1 25/ TLS TLS SMTP 587 STARTTLS 465 TLS</div>
<Field name="smtpPassword" label={settings?.smtpPasswordSet ? "SMTP 密码(留空不变)" : "SMTP 密码"} type="password" required={false} /> </div>
<SwitchRow label="强制 TLS" checked={smtpRequireTls} onCheckedChange={setSmtpRequireTls} className="md:col-span-2" /> <div className="grid gap-4 md:grid-cols-2">
<Field name="smtpHost" label="发信主机" defaultValue={settings?.smtpHost || ""} placeholder="127.0.0.1" required={false} />
<Field name="smtpPort" label="发信端口" defaultValue={settings?.smtpPort || "25"} />
<Field name="smtpUsername" label="中继用户名(内置 Postfix 留空)" defaultValue={settings?.smtpUsername || ""} required={false} />
<Field name="smtpPassword" label={settings?.smtpPasswordSet ? "中继密码(留空不变)" : "中继密码(内置 Postfix 留空)"} type="password" required={false} />
<SwitchRow label="外部中继强制 TLS" checked={smtpRequireTls} onCheckedChange={setSmtpRequireTls} className="md:col-span-2" />
</div>
</CardContent> </CardContent>
</Card>} </Card>}