package email import ( "bytes" "context" "crypto/tls" "encoding/json" "fmt" "io" "net/http" "net/mail" "net/smtp" "regexp" "strings" "sync" "gitea.com/texpixel/document_ai/config" "gitea.com/texpixel/document_ai/pkg/log" ) var ( once sync.Once client *Client ) // chineseDomainRe matches email domains that should be routed via Aliyun SMTP. var chineseDomainRe = regexp.MustCompile(`(?i)(\.edu\.cn|qq\.com|163\.com|126\.com|sina\.com|sohu\.com)$`) type Client struct { cfg config.EmailConfig } func InitEmailClient() *Client { once.Do(func() { client = &Client{cfg: config.GlobalConfig.Email} }) return client } // Send routes the email to the appropriate provider based on the recipient domain. func Send(ctx context.Context, to, subject, body string) error { if client == nil { return fmt.Errorf("email client not initialized, call InitEmailClient first") } return client.Send(ctx, to, subject, body) } func (c *Client) Send(ctx context.Context, to, subject, body string) error { if _, err := mail.ParseAddress(to); err != nil { return fmt.Errorf("invalid email address %q: %w", to, err) } domain := to[strings.LastIndex(to, "@")+1:] if chineseDomainRe.MatchString(domain) { return c.sendViaAliyunSMTP(ctx, to, subject, body) } return c.sendViaResend(ctx, to, subject, body) } func (c *Client) sendViaAliyunSMTP(ctx context.Context, to, subject, body string) error { cfg := c.cfg.AliyunSMTP addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) tlsConfig := &tls.Config{ServerName: cfg.Host} conn, err := tls.Dial("tcp", addr, tlsConfig) if err != nil { log.Error(ctx, "func", "sendViaAliyunSMTP", "msg", "tls dial failed", "error", err) return err } smtpClient, err := smtp.NewClient(conn, cfg.Host) if err != nil { conn.Close() log.Error(ctx, "func", "sendViaAliyunSMTP", "msg", "smtp new client failed", "error", err) return err } defer smtpClient.Close() auth := smtp.PlainAuth("", cfg.Username, cfg.Password, cfg.Host) if err = smtpClient.Auth(auth); err != nil { log.Error(ctx, "func", "sendViaAliyunSMTP", "msg", "smtp auth failed", "error", err) return err } from := c.cfg.FromAddr if err = smtpClient.Mail(from); err != nil { return err } if err = smtpClient.Rcpt(to); err != nil { return err } wc, err := smtpClient.Data() if err != nil { return err } defer wc.Close() if _, err = wc.Write([]byte(buildMessage(c.cfg.FromName, from, to, subject, body))); err != nil { return err } log.Info(ctx, "func", "sendViaAliyunSMTP", "msg", "email sent via aliyun smtp", "to", to) return nil } type resendRequest struct { From string `json:"from"` To []string `json:"to"` Subject string `json:"subject"` Html string `json:"html"` } func (c *Client) sendViaResend(ctx context.Context, to, subject, body string) error { payload := resendRequest{ From: fmt.Sprintf("%s <%s>", c.cfg.FromName, c.cfg.FromAddr), To: []string{to}, Subject: subject, Html: body, } jsonData, err := json.Marshal(payload) if err != nil { return err } req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.resend.com/emails", bytes.NewReader(jsonData)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+c.cfg.Resend.APIKey) resp, err := (&http.Client{}).Do(req) if err != nil { log.Error(ctx, "func", "sendViaResend", "msg", "http request failed", "error", err) return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 1024)) log.Error(ctx, "func", "sendViaResend", "msg", "resend api returned non-2xx", "status", resp.StatusCode, "to", to, "body", string(respBody)) return fmt.Errorf("resend api returned status %d: %s", resp.StatusCode, string(respBody)) } log.Info(ctx, "func", "sendViaResend", "msg", "email sent via resend", "to", to) return nil } func buildMessage(fromName, fromAddr, to, subject, body string) string { var buf bytes.Buffer buf.WriteString(fmt.Sprintf("From: %s <%s>\r\n", fromName, fromAddr)) buf.WriteString(fmt.Sprintf("To: %s\r\n", to)) buf.WriteString(fmt.Sprintf("Subject: %s\r\n", subject)) buf.WriteString("MIME-Version: 1.0\r\n") buf.WriteString("Content-Type: text/html; charset=UTF-8\r\n") buf.WriteString("\r\n") buf.WriteString(body) return buf.String() }