137 lines
3.9 KiB
Go
137 lines
3.9 KiB
Go
package services
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"net/smtp"
|
|
"strings"
|
|
)
|
|
|
|
type EmailConfig struct {
|
|
Host string
|
|
Port string
|
|
Username string
|
|
Password string
|
|
From string
|
|
}
|
|
|
|
type EmailService struct {
|
|
cfg EmailConfig
|
|
configured bool
|
|
}
|
|
|
|
func NewEmailService(cfg EmailConfig) *EmailService {
|
|
configured := cfg.Host != "" && cfg.Username != "" && cfg.Password != ""
|
|
if !configured {
|
|
log.Println("[Email] SMTP not configured, will print codes to console")
|
|
}
|
|
return &EmailService{cfg: cfg, configured: configured}
|
|
}
|
|
|
|
// SendVerificationCode 发送验证码邮件
|
|
func (s *EmailService) SendVerificationCode(to, code, purpose string) error {
|
|
var subject, body string
|
|
switch purpose {
|
|
case "register":
|
|
subject = "Kenaito Notebook - 注册验证码"
|
|
body = fmt.Sprintf(`
|
|
<div style="font-family: Arial, sans-serif; padding: 20px;">
|
|
<h2 style="color: #333;">Kenaito Notebook</h2>
|
|
<p>你正在注册账号,验证码如下:</p>
|
|
<div style="background: #f5f7fa; padding: 16px; border-radius: 8px; text-align: center; margin: 20px 0;">
|
|
<span style="font-size: 32px; font-weight: bold; color: #409eff; letter-spacing: 8px;">%s</span>
|
|
</div>
|
|
<p style="color: #909399; font-size: 14px;">验证码 5 分钟内有效,请勿泄露给他人。</p>
|
|
</div>`, code)
|
|
case "reset":
|
|
subject = "Kenaito Notebook - 重置密码验证码"
|
|
body = fmt.Sprintf(`
|
|
<div style="font-family: Arial, sans-serif; padding: 20px;">
|
|
<h2 style="color: #333;">Kenaito Notebook</h2>
|
|
<p>你正在重置密码,验证码如下:</p>
|
|
<div style="background: #f5f7fa; padding: 16px; border-radius: 8px; text-align: center; margin: 20px 0;">
|
|
<span style="font-size: 32px; font-weight: bold; color: #e6a23c; letter-spacing: 8px;">%s</span>
|
|
</div>
|
|
<p style="color: #909399; font-size: 14px;">验证码 5 分钟内有效,请勿泄露给他人。</p>
|
|
</div>`, code)
|
|
default:
|
|
return fmt.Errorf("unknown purpose: %s", purpose)
|
|
}
|
|
|
|
if !s.configured {
|
|
log.Printf("[Email] SMTP not configured. Verification code for %s (%s): %s", to, purpose, code)
|
|
return nil
|
|
}
|
|
|
|
return s.send(to, subject, body)
|
|
}
|
|
|
|
func (s *EmailService) send(to, subject, htmlBody string) error {
|
|
addr := net.JoinHostPort(s.cfg.Host, s.cfg.Port)
|
|
|
|
headers := make(map[string]string)
|
|
headers["From"] = s.cfg.From
|
|
headers["To"] = to
|
|
headers["Subject"] = subject
|
|
headers["MIME-Version"] = "1.0"
|
|
headers["Content-Type"] = "text/html; charset=UTF-8"
|
|
|
|
var msg strings.Builder
|
|
for k, v := range headers {
|
|
msg.WriteString(fmt.Sprintf("%s: %s\r\n", k, v))
|
|
}
|
|
msg.WriteString("\r\n")
|
|
msg.WriteString(htmlBody)
|
|
|
|
auth := smtp.PlainAuth("", s.cfg.Username, s.cfg.Password, s.cfg.Host)
|
|
|
|
// SSL connection (port 465)
|
|
if s.cfg.Port == "465" {
|
|
return s.sendTLS(addr, auth, to, msg.String())
|
|
}
|
|
|
|
// STARTTLS connection (port 587)
|
|
return smtp.SendMail(addr, auth, s.cfg.From, []string{to}, []byte(msg.String()))
|
|
}
|
|
|
|
func (s *EmailService) sendTLS(addr string, auth smtp.Auth, to, msg string) error {
|
|
conn, err := tls.Dial("tcp", addr, &tls.Config{
|
|
ServerName: s.cfg.Host,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to connect to SMTP server: %w", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
client, err := smtp.NewClient(conn, s.cfg.Host)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create SMTP client: %w", err)
|
|
}
|
|
defer client.Close()
|
|
|
|
if err = client.Auth(auth); err != nil {
|
|
return fmt.Errorf("SMTP auth failed: %w", err)
|
|
}
|
|
if err = client.Mail(s.cfg.From); err != nil {
|
|
return fmt.Errorf("SMTP MAIL FROM failed: %w", err)
|
|
}
|
|
if err = client.Rcpt(to); err != nil {
|
|
return fmt.Errorf("SMTP RCPT TO failed: %w", err)
|
|
}
|
|
|
|
w, err := client.Data()
|
|
if err != nil {
|
|
return fmt.Errorf("SMTP DATA failed: %w", err)
|
|
}
|
|
if _, err = w.Write([]byte(msg)); err != nil {
|
|
return fmt.Errorf("SMTP write failed: %w", err)
|
|
}
|
|
if err = w.Close(); err != nil {
|
|
return fmt.Errorf("SMTP close data failed: %w", err)
|
|
}
|
|
|
|
return client.Quit()
|
|
}
|