146 lines
3.5 KiB
Go
146 lines
3.5 KiB
Go
package email
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"mime"
|
|
"net"
|
|
"net/smtp"
|
|
"strings"
|
|
"time"
|
|
|
|
"kra/internal/biz"
|
|
"kra/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)
|
|
}
|