mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-04 07:06:36 +08:00
refactor(mail): modernize smtp sending with go-mail and unify message gateway pusher
This commit is contained in:
@@ -5,12 +5,7 @@
|
||||
package mail
|
||||
|
||||
const (
|
||||
errDialTLSFailed = "dial tls failed: %w"
|
||||
errSMTPClientCreationFailed = "smtp client creation failed: %w"
|
||||
errSMTPAuthFailed = "smtp auth failed: %w"
|
||||
errSMTPMailCommandFailed = "smtp mail command failed: %w"
|
||||
errSMTPRcptCommandFailed = "smtp rcpt command failed: %w"
|
||||
errSMTPDataCommandFailed = "smtp data command failed: %w"
|
||||
errSMTPWritingBodyFailed = "smtp writing body failed: %w" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
||||
errSendMailFailed = "send mail failed: %w"
|
||||
errCreateMailMessageFailed = "create mail message failed: %w"
|
||||
errCreateMailClientFailed = "create mail client failed: %w"
|
||||
errSendMailFailed = "send mail failed: %w"
|
||||
)
|
||||
|
||||
+117
-205
@@ -8,33 +8,39 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/smtp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
gomail "github.com/wneessen/go-mail"
|
||||
golog "github.com/wneessen/go-mail/log"
|
||||
)
|
||||
|
||||
const (
|
||||
smtpSSLPort = 465 // SMTP SSL 端口
|
||||
smtpDialTimeout = 5 * time.Second // SMTP 连接超时
|
||||
smtpSessionDeadline = 10 * time.Second // SMTP 会话截止时间
|
||||
smtpSSLPort = 465 // SMTP SSL 端口
|
||||
smtpDialTimeout = 5 * time.Second // SMTP 连接超时
|
||||
)
|
||||
|
||||
// Config represents SMTP mail configuration
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
Username string
|
||||
Password string
|
||||
Host string
|
||||
Port int
|
||||
Username string
|
||||
Password string
|
||||
FromName string // 可选发件人显示名称
|
||||
InsecureSkipVerify bool // 是否跳过证书校验 (默认跳过以兼容自签名证书)
|
||||
}
|
||||
|
||||
// sanitizeHeaderValue removes CR/LF bytes so untrusted values cannot inject
|
||||
// additional email headers (email header injection).
|
||||
func sanitizeHeaderValue(v string) string {
|
||||
v = strings.ReplaceAll(v, "\r", "")
|
||||
v = strings.ReplaceAll(v, "\n", "")
|
||||
return v
|
||||
// Option modifies internal mail send options
|
||||
type Option func(*clientOptions)
|
||||
|
||||
type clientOptions struct {
|
||||
debugLogger golog.Logger
|
||||
}
|
||||
|
||||
func withLogger(l golog.Logger) Option {
|
||||
return func(co *clientOptions) {
|
||||
co.debugLogger = l
|
||||
}
|
||||
}
|
||||
|
||||
// SendMail sends an HTML email using the provided config and message details
|
||||
@@ -44,206 +50,112 @@ func SendMail(ctx context.Context, cfg Config, to, subject, body string) error {
|
||||
|
||||
// SendMailHTML sends an HTML format email
|
||||
func SendMailHTML(ctx context.Context, cfg Config, to, subject, body string) error {
|
||||
addr := net.JoinHostPort(cfg.Host, strconv.Itoa(cfg.Port))
|
||||
return send(ctx, cfg, to, subject, body)
|
||||
}
|
||||
|
||||
// Header & MIME settings for HTML email
|
||||
header := make(map[string]string)
|
||||
header["From"] = sanitizeHeaderValue(cfg.Username)
|
||||
header["To"] = sanitizeHeaderValue(to)
|
||||
header["Subject"] = sanitizeHeaderValue(subject)
|
||||
header["MIME-Version"] = "1.0"
|
||||
header["Content-Type"] = "text/html; charset=UTF-8"
|
||||
// SendMailWithLog sends a test email and records a detailed SMTP connection log
|
||||
func SendMailWithLog(ctx context.Context, cfg Config, to, subject, body string) (string, error) {
|
||||
var logBuf bytes.Buffer
|
||||
logger := &bufferLogger{buf: &logBuf}
|
||||
|
||||
message := ""
|
||||
for k, v := range header {
|
||||
message += fmt.Sprintf("%s: %s\r\n", k, v)
|
||||
}
|
||||
message += "\r\n" + body
|
||||
fmt.Fprintf(&logBuf, "[System] Connecting to %s:%d...\n", cfg.Host, cfg.Port)
|
||||
|
||||
auth := smtp.PlainAuth("", cfg.Username, cfg.Password, cfg.Host)
|
||||
|
||||
// If using SSL port 465, we connection via TLS dial
|
||||
if cfg.Port == smtpSSLPort {
|
||||
return sendMailViaSSL(ctx, addr, auth, cfg, to, message)
|
||||
}
|
||||
|
||||
// For standard port (587 / 25), use smtp.SendMail directly (handles STARTTLS automatically if server supports it)
|
||||
err := smtp.SendMail(addr, auth, cfg.Username, []string{to}, []byte(message))
|
||||
err := send(ctx, cfg, to, subject, body, withLogger(logger))
|
||||
if err != nil {
|
||||
fmt.Fprintf(&logBuf, "[Error] Mail sending failed: %v\n", err)
|
||||
return logBuf.String(), err
|
||||
}
|
||||
|
||||
fmt.Fprintf(&logBuf, "[System] Mail sent successfully!\n")
|
||||
return logBuf.String(), nil
|
||||
}
|
||||
|
||||
func send(ctx context.Context, cfg Config, to, subject, body string, opts ...Option) error {
|
||||
var co clientOptions
|
||||
for _, opt := range opts {
|
||||
opt(&co)
|
||||
}
|
||||
|
||||
msg := gomail.NewMsg()
|
||||
var err error
|
||||
if cfg.FromName != "" {
|
||||
err = msg.FromFormat(cfg.FromName, cfg.Username)
|
||||
} else {
|
||||
err = msg.From(cfg.Username)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf(errCreateMailMessageFailed, err)
|
||||
}
|
||||
|
||||
if err = msg.To(to); err != nil {
|
||||
return fmt.Errorf(errCreateMailMessageFailed, err)
|
||||
}
|
||||
|
||||
msg.Subject(subject)
|
||||
msg.SetBodyString(gomail.TypeTextHTML, body)
|
||||
|
||||
clientOpts := []gomail.Option{
|
||||
gomail.WithPort(cfg.Port),
|
||||
gomail.WithTimeout(smtpDialTimeout),
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: true, //nolint:gosec // SMTP servers might use self-signed certificates
|
||||
ServerName: cfg.Host,
|
||||
}
|
||||
clientOpts = append(clientOpts, gomail.WithTLSConfig(tlsConfig))
|
||||
|
||||
if cfg.Port == smtpSSLPort {
|
||||
clientOpts = append(clientOpts, gomail.WithSSL())
|
||||
} else {
|
||||
clientOpts = append(clientOpts, gomail.WithTLSPolicy(gomail.TLSOpportunistic))
|
||||
}
|
||||
|
||||
if cfg.Username != "" && cfg.Password != "" {
|
||||
clientOpts = append(clientOpts,
|
||||
gomail.WithSMTPAuth(gomail.SMTPAuthPlain),
|
||||
gomail.WithUsername(cfg.Username),
|
||||
gomail.WithPassword(cfg.Password),
|
||||
)
|
||||
}
|
||||
|
||||
if co.debugLogger != nil {
|
||||
clientOpts = append(clientOpts, gomail.WithDebugLog(), gomail.WithLogger(co.debugLogger))
|
||||
}
|
||||
|
||||
client, err := gomail.NewClient(cfg.Host, clientOpts...)
|
||||
if err != nil {
|
||||
return fmt.Errorf(errCreateMailClientFailed, err)
|
||||
}
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
if err = client.DialAndSendWithContext(ctx, msg); err != nil {
|
||||
return fmt.Errorf(errSendMailFailed, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendMailViaSSL 通过 TLS 直接连接 SMTP SSL 端口发送邮件
|
||||
func sendMailViaSSL(ctx context.Context, addr string, auth smtp.Auth, cfg Config, to, message string) error {
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: true, //nolint:gosec // SMTP servers might use self-signed certificates
|
||||
ServerName: cfg.Host,
|
||||
}
|
||||
dialer := &net.Dialer{Timeout: smtpDialTimeout}
|
||||
tlsDialer := &tls.Dialer{
|
||||
NetDialer: dialer,
|
||||
Config: tlsConfig,
|
||||
}
|
||||
conn, err := tlsDialer.DialContext(ctx, "tcp", addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf(errDialTLSFailed, err)
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
_ = conn.SetDeadline(time.Now().Add(smtpSessionDeadline))
|
||||
|
||||
client, err := smtp.NewClient(conn, cfg.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf(errSMTPClientCreationFailed, err)
|
||||
}
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
if err = client.Auth(auth); err != nil {
|
||||
return fmt.Errorf(errSMTPAuthFailed, err)
|
||||
}
|
||||
if err = client.Mail(cfg.Username); err != nil {
|
||||
return fmt.Errorf(errSMTPMailCommandFailed, err)
|
||||
}
|
||||
if err = client.Rcpt(to); err != nil {
|
||||
return fmt.Errorf(errSMTPRcptCommandFailed, err)
|
||||
}
|
||||
|
||||
w, err := client.Data()
|
||||
if err != nil {
|
||||
return fmt.Errorf(errSMTPDataCommandFailed, err)
|
||||
}
|
||||
defer func() { _ = w.Close() }()
|
||||
|
||||
_, err = w.Write([]byte(message))
|
||||
if err != nil {
|
||||
return fmt.Errorf(errSMTPWritingBodyFailed, err)
|
||||
}
|
||||
return nil
|
||||
type bufferLogger struct {
|
||||
buf *bytes.Buffer
|
||||
}
|
||||
|
||||
// SendMailWithLog sends a test email and records a detailed SMTP connection log
|
||||
func SendMailWithLog(ctx context.Context, cfg Config, to, subject, body string) (string, error) {
|
||||
var logBuf bytes.Buffer
|
||||
logLine := func(dir, format string, args ...interface{}) {
|
||||
fmt.Fprintf(&logBuf, "[%s] %s\n", dir, fmt.Sprintf(format, args...))
|
||||
func (l *bufferLogger) log(level string, entry golog.Log) {
|
||||
msg := fmt.Sprintf(entry.Format, entry.Messages...)
|
||||
msg = strings.TrimRight(msg, "\r\n")
|
||||
var dir string
|
||||
switch entry.Direction {
|
||||
case golog.DirClientToServer:
|
||||
dir = "C"
|
||||
case golog.DirServerToClient:
|
||||
dir = "S"
|
||||
default:
|
||||
dir = level
|
||||
}
|
||||
|
||||
addr := net.JoinHostPort(cfg.Host, strconv.Itoa(cfg.Port))
|
||||
logLine("System", "Connecting to %s...", addr)
|
||||
|
||||
var conn net.Conn
|
||||
var err error
|
||||
dialer := &net.Dialer{Timeout: smtpDialTimeout}
|
||||
if cfg.Port == smtpSSLPort {
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: true, //nolint:gosec // SMTP servers might use self-signed certificates
|
||||
ServerName: cfg.Host,
|
||||
}
|
||||
tlsDialer := &tls.Dialer{
|
||||
NetDialer: dialer,
|
||||
Config: tlsConfig,
|
||||
}
|
||||
conn, err = tlsDialer.DialContext(ctx, "tcp", addr)
|
||||
} else {
|
||||
conn, err = dialer.DialContext(ctx, "tcp", addr)
|
||||
}
|
||||
if err != nil {
|
||||
logLine("Error", "Connection failed: %v", err)
|
||||
return logBuf.String(), err
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
logLine("System", "Connected successfully.")
|
||||
|
||||
// Set a 10-second session deadline for read/write operations
|
||||
_ = conn.SetDeadline(time.Now().Add(smtpSessionDeadline))
|
||||
|
||||
client, err := smtp.NewClient(conn, cfg.Host)
|
||||
if err != nil {
|
||||
logLine("Error", "SMTP client handshake failed: %v", err)
|
||||
return logBuf.String(), err
|
||||
}
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
// If not 465, support STARTTLS if available
|
||||
if cfg.Port != smtpSSLPort {
|
||||
if ok, _ := client.Extension("STARTTLS"); ok {
|
||||
logLine("C", "STARTTLS")
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: true, //nolint:gosec // SMTP servers might use self-signed certificates
|
||||
ServerName: cfg.Host,
|
||||
}
|
||||
if err = client.StartTLS(tlsConfig); err != nil {
|
||||
logLine("Error", "STARTTLS failed: %v", err)
|
||||
return logBuf.String(), err
|
||||
}
|
||||
logLine("S", "220 Ready to start TLS")
|
||||
}
|
||||
}
|
||||
|
||||
// Authentication
|
||||
if cfg.Username != "" && cfg.Password != "" {
|
||||
auth := smtp.PlainAuth("", cfg.Username, cfg.Password, cfg.Host)
|
||||
logLine("C", "AUTH PLAIN **********")
|
||||
if err = client.Auth(auth); err != nil {
|
||||
logLine("Error", "Authentication failed: %v", err)
|
||||
return logBuf.String(), err
|
||||
}
|
||||
logLine("S", "235 Authentication successful")
|
||||
}
|
||||
|
||||
// Mail command
|
||||
logLine("C", "MAIL FROM:<%s>", cfg.Username)
|
||||
if err = client.Mail(cfg.Username); err != nil {
|
||||
logLine("Error", "MAIL FROM command failed: %v", err)
|
||||
return logBuf.String(), err
|
||||
}
|
||||
logLine("S", "250 OK")
|
||||
|
||||
// Rcpt command
|
||||
logLine("C", "RCPT TO:<%s>", to)
|
||||
if err = client.Rcpt(to); err != nil {
|
||||
logLine("Error", "RCPT TO command failed: %v", err)
|
||||
return logBuf.String(), err
|
||||
}
|
||||
logLine("S", "250 OK")
|
||||
|
||||
// Data command
|
||||
logLine("C", "DATA")
|
||||
w, err := client.Data()
|
||||
if err != nil {
|
||||
logLine("Error", "DATA command failed: %v", err)
|
||||
return logBuf.String(), err
|
||||
}
|
||||
logLine("S", "354 Start mail input")
|
||||
|
||||
// Header & MIME settings for HTML email
|
||||
header := make(map[string]string)
|
||||
header["From"] = sanitizeHeaderValue(cfg.Username)
|
||||
header["To"] = sanitizeHeaderValue(to)
|
||||
header["Subject"] = sanitizeHeaderValue(subject)
|
||||
header["MIME-Version"] = "1.0"
|
||||
header["Content-Type"] = "text/html; charset=UTF-8"
|
||||
|
||||
message := ""
|
||||
for k, v := range header {
|
||||
message += fmt.Sprintf("%s: %s\r\n", k, v)
|
||||
}
|
||||
message += "\r\n" + body
|
||||
|
||||
logLine("System", "Sending message body...")
|
||||
if _, err = w.Write([]byte(message)); err != nil {
|
||||
_ = w.Close()
|
||||
logLine("Error", "Writing message body failed: %v", err)
|
||||
return logBuf.String(), err
|
||||
}
|
||||
_ = w.Close()
|
||||
logLine("S", "250 OK")
|
||||
|
||||
logLine("C", "QUIT")
|
||||
_ = client.Quit()
|
||||
logLine("System", "Mail sent successfully!")
|
||||
|
||||
return logBuf.String(), nil
|
||||
fmt.Fprintf(l.buf, "[%s] %s\n", dir, msg)
|
||||
}
|
||||
|
||||
func (l *bufferLogger) Debugf(e golog.Log) { l.log("Debug", e) }
|
||||
func (l *bufferLogger) Infof(e golog.Log) { l.log("Info", e) }
|
||||
func (l *bufferLogger) Warnf(e golog.Log) { l.log("Warn", e) }
|
||||
func (l *bufferLogger) Errorf(e golog.Log) { l.log("Error", e) }
|
||||
|
||||
+101
-69
@@ -8,74 +8,105 @@ import (
|
||||
"context"
|
||||
"net"
|
||||
"net/textproto"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSendMailMock(t *testing.T) {
|
||||
// Start a mock SMTP server
|
||||
func startMockSMTPServer(t *testing.T) (int, func()) {
|
||||
t.Helper()
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to start mock smtp server: %v", err)
|
||||
}
|
||||
defer func() { _ = l.Close() }()
|
||||
|
||||
port := l.Addr().(*net.TCPAddr).Port
|
||||
|
||||
go func() {
|
||||
conn, err := l.Accept()
|
||||
for {
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go handleMockSMTPConn(conn)
|
||||
}
|
||||
}()
|
||||
|
||||
return port, func() { _ = l.Close() }
|
||||
}
|
||||
|
||||
func handleMockSMTPConn(conn net.Conn) {
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
writer := bufio.NewWriter(conn)
|
||||
reader := bufio.NewReader(conn)
|
||||
tp := textproto.NewReader(reader)
|
||||
|
||||
// 220 Ready
|
||||
_, _ = writer.WriteString("220 mock.smtp.com SMTP Ready\r\n")
|
||||
_ = writer.Flush()
|
||||
|
||||
for {
|
||||
line, err := tp.ReadLine()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
|
||||
writer := bufio.NewWriter(conn)
|
||||
reader := bufio.NewReader(conn)
|
||||
tp := textproto.NewReader(reader)
|
||||
|
||||
// 220 Ready
|
||||
_, _ = writer.WriteString("220 mock.smtp.com SMTP Ready\r\n")
|
||||
_ = writer.Flush()
|
||||
|
||||
// Read HELO/EHLO
|
||||
_, _ = tp.ReadLine()
|
||||
_, _ = writer.WriteString("250-mock.smtp.com\r\n250 AUTH PLAIN\r\n")
|
||||
_ = writer.Flush()
|
||||
|
||||
// Read AUTH PLAIN
|
||||
_, _ = tp.ReadLine()
|
||||
_, _ = writer.WriteString("235 Authentication successful\r\n")
|
||||
_ = writer.Flush()
|
||||
|
||||
// Read MAIL FROM
|
||||
_, _ = tp.ReadLine()
|
||||
_, _ = writer.WriteString("250 OK\r\n")
|
||||
_ = writer.Flush()
|
||||
|
||||
// Read RCPT TO
|
||||
_, _ = tp.ReadLine()
|
||||
_, _ = writer.WriteString("250 OK\r\n")
|
||||
_ = writer.Flush()
|
||||
|
||||
// Read DATA
|
||||
_, _ = tp.ReadLine()
|
||||
_, _ = writer.WriteString("354 Start mail input\r\n")
|
||||
_ = writer.Flush()
|
||||
|
||||
// Read body lines until dot
|
||||
for {
|
||||
line, err := tp.ReadLine()
|
||||
if err != nil || line == "." {
|
||||
break
|
||||
upper := strings.ToUpper(line)
|
||||
switch {
|
||||
case strings.HasPrefix(upper, "EHLO") || strings.HasPrefix(upper, "HELO"):
|
||||
_, _ = writer.WriteString("250-mock.smtp.com\r\n250 AUTH PLAIN\r\n")
|
||||
_ = writer.Flush()
|
||||
case strings.HasPrefix(upper, "AUTH PLAIN"):
|
||||
_, _ = writer.WriteString("235 2.7.0 Authentication successful\r\n")
|
||||
_ = writer.Flush()
|
||||
case strings.HasPrefix(upper, "MAIL FROM:"):
|
||||
_, _ = writer.WriteString("250 2.1.0 Ok\r\n")
|
||||
_ = writer.Flush()
|
||||
case strings.HasPrefix(upper, "RCPT TO:"):
|
||||
_, _ = writer.WriteString("250 2.1.5 Ok\r\n")
|
||||
_ = writer.Flush()
|
||||
case strings.HasPrefix(upper, "DATA"):
|
||||
_, _ = writer.WriteString("354 Start mail input; end with <CRLF>.<CRLF>\r\n")
|
||||
_ = writer.Flush()
|
||||
for {
|
||||
dataLine, err := tp.ReadLine()
|
||||
if err != nil || dataLine == "." {
|
||||
break
|
||||
}
|
||||
}
|
||||
_, _ = writer.WriteString("250 2.0.0 Ok: queued\r\n")
|
||||
_ = writer.Flush()
|
||||
case strings.HasPrefix(upper, "QUIT"):
|
||||
_, _ = writer.WriteString("221 2.0.0 Bye\r\n")
|
||||
_ = writer.Flush()
|
||||
return
|
||||
default:
|
||||
_, _ = writer.WriteString("250 Ok\r\n")
|
||||
_ = writer.Flush()
|
||||
}
|
||||
_, _ = writer.WriteString("250 OK\r\n")
|
||||
_ = writer.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
// Read QUIT
|
||||
_, _ = tp.ReadLine()
|
||||
_, _ = writer.WriteString("221 Bye\r\n")
|
||||
_ = writer.Flush()
|
||||
}()
|
||||
func TestSendMailMock(t *testing.T) {
|
||||
port, cleanup := startMockSMTPServer(t)
|
||||
defer cleanup()
|
||||
|
||||
cfg := Config{
|
||||
Host: "127.0.0.1",
|
||||
Port: port,
|
||||
Username: "test@example.com",
|
||||
Password: "password",
|
||||
FromName: "Wavelet Notifier",
|
||||
}
|
||||
|
||||
err := SendMail(context.Background(), cfg, "recipient@example.com", "Test Subject", "<h1>Test Body</h1>")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to send mail: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendMailWithLog(t *testing.T) {
|
||||
port, cleanup := startMockSMTPServer(t)
|
||||
defer cleanup()
|
||||
|
||||
cfg := Config{
|
||||
Host: "127.0.0.1",
|
||||
@@ -84,28 +115,29 @@ func TestSendMailMock(t *testing.T) {
|
||||
Password: "password",
|
||||
}
|
||||
|
||||
err = SendMail(context.Background(), cfg, "recipient@example.com", "Test Subject", "<h1>Test Body</h1>")
|
||||
logs, err := SendMailWithLog(context.Background(), cfg, "recipient@example.com", "Test Subject", "<p>Test Log</p>")
|
||||
if err != nil {
|
||||
t.Errorf("failed to send mail: %v", err)
|
||||
t.Fatalf("failed to send mail with log: %v, log output:\n%s", err, logs)
|
||||
}
|
||||
|
||||
if !strings.Contains(logs, "[System] Connecting to") {
|
||||
t.Errorf("expected connection log in output, got: %s", logs)
|
||||
}
|
||||
if !strings.Contains(logs, "[System] Mail sent successfully!") {
|
||||
t.Errorf("expected success log in output, got: %s", logs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeHeaderValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{"plain", "System Notification", "System Notification"},
|
||||
{"crlf stripped", "alert\r\nBcc: attacker@example.com", "alertBcc: attacker@example.com"},
|
||||
{"cr stripped", "a\rb", "ab"},
|
||||
{"lf stripped", "a\nb", "ab"},
|
||||
func TestSendMailInvalidAddress(t *testing.T) {
|
||||
cfg := Config{
|
||||
Host: "127.0.0.1",
|
||||
Port: 25,
|
||||
Username: "test@example.com",
|
||||
Password: "password",
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := sanitizeHeaderValue(tt.input); got != tt.want {
|
||||
t.Errorf("sanitizeHeaderValue(%q) = %q, want %q", tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
|
||||
err := SendMail(context.Background(), cfg, "invalid address with \n newline", "Subject", "Body")
|
||||
if err == nil {
|
||||
t.Errorf("expected error for invalid address, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user