158 lines
4.0 KiB
Go
158 lines
4.0 KiB
Go
package email
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"kra/internal/biz/system"
|
|
"mime"
|
|
"net"
|
|
"net/smtp"
|
|
"strings"
|
|
"time"
|
|
|
|
"kra/internal/config"
|
|
)
|
|
|
|
type emailRepo struct{ runtime *config.Store }
|
|
|
|
func NewEmailRepo(runtime *config.Store) system.EmailRepo {
|
|
return &emailRepo{runtime: runtime}
|
|
}
|
|
|
|
func (r *emailRepo) email() *config.Email {
|
|
if r.runtime == nil {
|
|
return nil
|
|
}
|
|
config := r.runtime.Admin()
|
|
if config == nil {
|
|
return nil
|
|
}
|
|
return config.Email
|
|
}
|
|
|
|
func (r *emailRepo) DefaultRecipients() []string {
|
|
config := r.email()
|
|
if config == nil || strings.TrimSpace(config.To) == "" {
|
|
return nil
|
|
}
|
|
return []string{strings.TrimSpace(config.To)}
|
|
}
|
|
|
|
func emailEnabled(config *config.Email) bool {
|
|
return config != nil && strings.TrimSpace(config.Host) != "" &&
|
|
strings.TrimSpace(config.From) != "" && strings.TrimSpace(config.Secret) != "" && config.Port > 0
|
|
}
|
|
|
|
func normalizeRecipients(values []string) []string {
|
|
result := make([]string, 0, len(values))
|
|
for _, value := range values {
|
|
if value = strings.TrimSpace(value); value != "" {
|
|
result = append(result, value)
|
|
}
|
|
}
|
|
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 {
|
|
config := r.email()
|
|
if !emailEnabled(config) {
|
|
return errors.New("邮件服务未配置")
|
|
}
|
|
to = normalizeRecipients(to)
|
|
if len(to) == 0 {
|
|
return errors.New("收件人不能为空")
|
|
}
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
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.Dialer{NetDialer: dialer, Config: &tls.Config{ServerName: config.Host, MinVersion: tls.VersionTLS12}}).DialContext(ctx, "tcp", address)
|
|
} 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)
|
|
}
|