package data import ( "context" "crypto/tls" "errors" "fmt" "io" "mime" "net" "net/smtp" "strings" "time" "kra/internal/biz" "kra/internal/conf" ) type emailRepo struct { config *conf.AdminBackend_Email } func NewEmailRepo(config *conf.AdminBackend) biz.EmailRepo { if config == nil { return &emailRepo{} } return &emailRepo{config: config.Email} } func (r *emailRepo) Enabled() bool { return r.config != nil && r.config.Host != "" && r.config.From != "" && r.config.Secret != "" && r.config.Port > 0 } func (r *emailRepo) DefaultRecipients() []string { if r.config == nil { return nil } parts := strings.Split(r.config.To, ",") result := make([]string, 0, len(parts)) for _, part := range parts { if recipient := strings.TrimSpace(part); recipient != "" { result = append(result, recipient) } } return result } func cleanHeader(value string) string { return strings.NewReplacer("\r", "", "\n", "").Replace(value) } func (r *emailRepo) Send(ctx context.Context, to []string, subject, body string) error { if !r.Enabled() { return errors.New("邮件服务未配置") } if len(to) == 0 { return errors.New("收件人不能为空") } config := r.config address := net.JoinHostPort(config.Host, fmt.Sprint(config.Port)) dialer := &net.Dialer{Timeout: 10 * time.Second} var conn net.Conn var err error if config.IsSsl { conn, err = tls.DialWithDialer(dialer, "tcp", address, &tls.Config{ServerName: config.Host, MinVersion: tls.VersionTLS12}) } else { conn, err = dialer.DialContext(ctx, "tcp", address) } if err != nil { return err } defer conn.Close() _ = conn.SetDeadline(time.Now().Add(15 * time.Second)) client, err := smtp.NewClient(conn, config.Host) if err != nil { return err } defer client.Close() if !config.IsSsl { if supported, _ := client.Extension("STARTTLS"); supported { if err = client.StartTLS(&tls.Config{ServerName: config.Host, MinVersion: tls.VersionTLS12}); err != nil { return err } } } var auth smtp.Auth if config.IsLoginAuth { auth = &loginAuth{username: config.From, password: config.Secret} } else { auth = smtp.PlainAuth("", config.From, config.Secret, config.Host) } if ok, _ := client.Extension("AUTH"); ok { if err = client.Auth(auth); err != nil { return err } } if err = client.Mail(config.From); err != nil { return err } for _, recipient := range to { if err = client.Rcpt(recipient); err != nil { return err } } writer, err := client.Data() if err != nil { return err } fromHeader := cleanHeader(config.From) if config.Nickname != "" { fromHeader = fmt.Sprintf("%s <%s>", mime.QEncoding.Encode("UTF-8", cleanHeader(config.Nickname)), fromHeader) } toHeader := make([]string, 0, len(to)) for _, recipient := range to { toHeader = append(toHeader, cleanHeader(recipient)) } message := "From: " + fromHeader + "\r\n" + "To: " + strings.Join(toHeader, ",") + "\r\n" + "Subject: " + mime.QEncoding.Encode("UTF-8", cleanHeader(subject)) + "\r\n" + "MIME-Version: 1.0\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n" + body if _, err = io.WriteString(writer, message); err != nil { _ = writer.Close() return err } if err = writer.Close(); err != nil { return err } return client.Quit() } type loginAuth struct{ username, password string } func (a *loginAuth) Start(*smtp.ServerInfo) (string, []byte, error) { return "LOGIN", nil, nil } func (a *loginAuth) Next(challenge []byte, more bool) ([]byte, error) { if !more { return nil, nil } prompt := strings.ToLower(strings.TrimSpace(string(challenge))) if strings.Contains(prompt, "username") || strings.Contains(prompt, "user") { return []byte(a.username), nil } if strings.Contains(prompt, "password") || strings.Contains(prompt, "pass") { return []byte(a.password), nil } return nil, fmt.Errorf("unsupported SMTP LOGIN challenge %q", prompt) }