kra-oa/app/system/internal/integration/email/email.go

146 lines
3.6 KiB
Go

package email
import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"mime"
"net"
"net/smtp"
"strings"
"time"
"kra/app/system/internal/biz"
"kra/app/system/internal/conf"
)
type emailRepo struct {
runtime *conf.Runtime
}
func NewEmailRepo(runtime *conf.Runtime) biz.EmailRepo {
return &emailRepo{runtime: runtime}
}
func (r *emailRepo) email() *conf.AdminBackend_Email {
if r.runtime == nil {
return nil
}
config := r.runtime.Admin()
if config == nil {
return nil
}
return config.Email
}
func (r *emailRepo) Enabled() bool {
config := r.email()
return config != nil && config.Host != "" && config.From != "" && config.Secret != "" && config.Port > 0
}
func (r *emailRepo) DefaultRecipients() []string {
config := r.email()
if config == nil {
return nil
}
return []string{config.To}
}
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.email()
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()
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)
}