Files
sub2api/backend/internal/service/email_message.go
T
李建琦 6d655c9903
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

112 lines
3.1 KiB
Go

package service
import (
"bytes"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"mime"
"mime/quotedprintable"
"net/mail"
"strings"
"time"
)
type smtpMessage struct {
envelopeFrom string
envelopeTo string
data []byte
}
func buildSMTPMessage(config *SMTPConfig, to, subject, body string) (smtpMessage, error) {
if config == nil {
return smtpMessage{}, errors.New("missing SMTP configuration")
}
fromAddress, err := parseSMTPAddress(config.From, "from")
if err != nil {
return smtpMessage{}, err
}
recipientAddress, err := parseSMTPAddress(to, "recipient")
if err != nil {
return smtpMessage{}, err
}
messageID, err := generateEmailMessageID(fromAddress.Address, config.Host)
if err != nil {
return smtpMessage{}, fmt.Errorf("generate message ID: %w", err)
}
fromName := sanitizeEmailHeader(config.FromName)
if strings.TrimSpace(fromName) == "" {
fromName = fromAddress.Name
}
fromHeader := (&mail.Address{
Name: fromName,
Address: fromAddress.Address,
}).String()
toHeader := (&mail.Address{
Name: recipientAddress.Name,
Address: recipientAddress.Address,
}).String()
subjectHeader := mime.QEncoding.Encode("UTF-8", sanitizeEmailHeader(subject))
var message bytes.Buffer
fmt.Fprintf(&message, "From: %s\r\n", fromHeader)
fmt.Fprintf(&message, "To: %s\r\n", toHeader)
fmt.Fprintf(&message, "Date: %s\r\n", time.Now().UTC().Format(time.RFC1123Z))
fmt.Fprintf(&message, "Message-ID: %s\r\n", messageID)
fmt.Fprintf(&message, "Subject: %s\r\n", subjectHeader)
fmt.Fprint(&message, "MIME-Version: 1.0\r\n"+
"Content-Type: text/html; charset=UTF-8\r\n"+
"Content-Transfer-Encoding: quoted-printable\r\n\r\n")
bodyWriter := quotedprintable.NewWriter(&message)
if _, err := bodyWriter.Write([]byte(body)); err != nil {
return smtpMessage{}, fmt.Errorf("encode email body: %w", err)
}
if err := bodyWriter.Close(); err != nil {
return smtpMessage{}, fmt.Errorf("close email body encoder: %w", err)
}
return smtpMessage{
envelopeFrom: fromAddress.Address,
envelopeTo: recipientAddress.Address,
data: message.Bytes(),
}, nil
}
func parseSMTPAddress(value, field string) (*mail.Address, error) {
if strings.ContainsAny(value, "\r\n") {
return nil, fmt.Errorf("invalid SMTP %s address: contains a line break", field)
}
cleaned := strings.TrimSpace(value)
address, err := mail.ParseAddress(cleaned)
if err != nil || strings.TrimSpace(address.Address) == "" {
if err == nil {
err = fmt.Errorf("address is empty")
}
return nil, fmt.Errorf("invalid SMTP %s address: %w", field, err)
}
return address, nil
}
func generateEmailMessageID(fromAddress, smtpHost string) (string, error) {
randomID := make([]byte, 16)
if _, err := rand.Read(randomID); err != nil {
return "", err
}
domain := strings.TrimSpace(sanitizeEmailHeader(smtpHost))
if at := strings.LastIndexByte(fromAddress, '@'); at >= 0 && at < len(fromAddress)-1 {
domain = fromAddress[at+1:]
}
domain = strings.Trim(domain, "[]<>")
if domain == "" {
domain = "localhost"
}
return fmt.Sprintf("<%s@%s>", hex.EncodeToString(randomID), domain), nil
}