diff --git a/apps/api/internal/app/mime.go b/apps/api/internal/app/mime.go index 0dafaad..59a6962 100644 --- a/apps/api/internal/app/mime.go +++ b/apps/api/internal/app/mime.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto/tls" "encoding/base64" + "errors" "fmt" "io" "mime" @@ -132,18 +133,63 @@ func sendSMTPWithConfig(cfg Config, from string, recipients []string, mimeBytes auth = smtp.PlainAuth("", cfg.SMTPUsername, cfg.SMTPPassword, cfg.SMTPHost) } 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 { return err } - defer conn.Close() - client, err := smtp.NewClient(conn, cfg.SMTPHost) + client, err := smtp.NewClient(conn, host) if err != nil { + _ = conn.Close() return err } 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 err := client.Auth(auth); err != nil { return err diff --git a/apps/web/src/pages/admin.tsx b/apps/web/src/pages/admin.tsx index 4996b07..e3bbf97 100644 --- a/apps/web/src/pages/admin.tsx +++ b/apps/web/src/pages/admin.tsx @@ -9,7 +9,7 @@ import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" 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 { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog" 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: "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: "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 }, ] } @@ -569,16 +569,25 @@ function SystemSettingsSection({ settings, domains }: { settings?: SystemSetting {settingsTab === "smtp" &&
- SMTP 设置 +
+ 发信通道 + 单容器默认使用内置 Postfix(127.0.0.1:25),不需要再配置外部 SMTP。只有需要走第三方中继时才修改这里。 +
- - - - - - + +
+
当前默认:内置 Postfix
+
Host 填 127.0.0.1、端口 25、用户名/密码留空、强制 TLS 关闭。这里的“强制 TLS”仅用于外部 SMTP 中继(587 STARTTLS 或 465 TLS)。
+
+
+ + + + + +
}