diff --git a/messenger/providers/smtp/smtp.go b/messenger/providers/smtp/smtp.go index 91b776d7..3f8f65c1 100644 --- a/messenger/providers/smtp/smtp.go +++ b/messenger/providers/smtp/smtp.go @@ -6,6 +6,7 @@ import ( "fmt" "net" "net/smtp" + "regexp" "strconv" "strings" "time" @@ -13,8 +14,8 @@ import ( "github.com/yaoapp/yao/messenger/types" ) -// SMTPProvider implements the Provider interface for SMTP email sending -type SMTPProvider struct { +// Provider implements the Provider interface for SMTP email sending +type Provider struct { config types.ProviderConfig host string port int @@ -26,8 +27,8 @@ type SMTPProvider struct { } // NewSMTPProvider creates a new SMTP provider -func NewSMTPProvider(config types.ProviderConfig) (*SMTPProvider, error) { - provider := &SMTPProvider{ +func NewSMTPProvider(config types.ProviderConfig) (*Provider, error) { + provider := &Provider{ config: config, useTLS: true, // Default to TLS } @@ -95,7 +96,7 @@ func NewSMTPProvider(config types.ProviderConfig) (*SMTPProvider, error) { } // Send sends a message using SMTP -func (p *SMTPProvider) Send(ctx context.Context, message *types.Message) error { +func (p *Provider) Send(ctx context.Context, message *types.Message) error { if message.Type != types.MessageTypeEmail { return fmt.Errorf("SMTP provider only supports email messages") } @@ -111,7 +112,7 @@ func (p *SMTPProvider) Send(ctx context.Context, message *types.Message) error { } // SendBatch sends multiple messages in batch -func (p *SMTPProvider) SendBatch(ctx context.Context, messages []*types.Message) error { +func (p *Provider) SendBatch(ctx context.Context, messages []*types.Message) error { for _, message := range messages { if err := p.Send(ctx, message); err != nil { return fmt.Errorf("failed to send message to %v: %w", message.To, err) @@ -121,17 +122,17 @@ func (p *SMTPProvider) SendBatch(ctx context.Context, messages []*types.Message) } // GetType returns the provider type -func (p *SMTPProvider) GetType() string { +func (p *Provider) GetType() string { return "smtp" } // GetName returns the provider name -func (p *SMTPProvider) GetName() string { +func (p *Provider) GetName() string { return p.config.Name } // Validate validates the provider configuration -func (p *SMTPProvider) Validate() error { +func (p *Provider) Validate() error { if p.host == "" { return fmt.Errorf("host is required") } @@ -151,12 +152,12 @@ func (p *SMTPProvider) Validate() error { } // Close closes the provider connection (no-op for SMTP) -func (p *SMTPProvider) Close() error { +func (p *Provider) Close() error { return nil } // buildMessage builds the email message content -func (p *SMTPProvider) buildMessage(message *types.Message) (string, error) { +func (p *Provider) buildMessage(message *types.Message) (string, error) { var content strings.Builder // From header @@ -211,8 +212,21 @@ func (p *SMTPProvider) buildMessage(message *types.Message) (string, error) { return content.String(), nil } +// extractEmailAddress extracts the email address from a string that may contain display name +// e.g., "John Doe " -> "john@example.com" +func extractEmailAddress(address string) string { + // Regular expression to match email addresses in angle brackets + re := regexp.MustCompile(`<([^>]+)>`) + matches := re.FindStringSubmatch(address) + if len(matches) > 1 { + return matches[1] + } + // If no angle brackets, assume the whole string is the email address + return strings.TrimSpace(address) +} + // sendEmail sends the email using SMTP -func (p *SMTPProvider) sendEmail(ctx context.Context, to []string, content string) error { +func (p *Provider) sendEmail(ctx context.Context, to []string, content string) error { addr := fmt.Sprintf("%s:%d", p.host, p.port) // Create auth @@ -222,14 +236,13 @@ func (p *SMTPProvider) sendEmail(ctx context.Context, to []string, content strin if p.useSSL { // Use SSL/TLS connection return p.sendWithTLS(ctx, addr, auth, to, content) - } else { - // Use standard SMTP with STARTTLS and context support - return p.sendWithContext(ctx, addr, auth, to, content) } + // Use standard SMTP with STARTTLS and context support + return p.sendWithContext(ctx, addr, auth, to, content) } // sendWithContext sends email using standard SMTP with context support -func (p *SMTPProvider) sendWithContext(ctx context.Context, addr string, auth smtp.Auth, to []string, content string) error { +func (p *Provider) sendWithContext(ctx context.Context, addr string, auth smtp.Auth, to []string, content string) error { // Create a dialer with timeout from context d := &net.Dialer{ Timeout: 30 * time.Second, @@ -264,8 +277,9 @@ func (p *SMTPProvider) sendWithContext(ctx context.Context, addr string, auth sm return fmt.Errorf("SMTP authentication failed: %w", err) } - // Set sender - if err = client.Mail(p.from); err != nil { + // Set sender (extract pure email address from potentially formatted from address) + fromEmail := extractEmailAddress(p.from) + if err = client.Mail(fromEmail); err != nil { return fmt.Errorf("failed to set sender: %w", err) } @@ -291,19 +305,31 @@ func (p *SMTPProvider) sendWithContext(ctx context.Context, addr string, auth sm } // sendWithTLS sends email with explicit TLS connection -func (p *SMTPProvider) sendWithTLS(ctx context.Context, addr string, auth smtp.Auth, to []string, content string) error { - // Create TLS connection +func (p *Provider) sendWithTLS(ctx context.Context, addr string, auth smtp.Auth, to []string, content string) error { + // Create TLS connection with context support tlsConfig := &tls.Config{ ServerName: p.host, InsecureSkipVerify: false, } - conn, err := tls.Dial("tcp", addr, tlsConfig) + // Use dialer with context for TLS connection + d := &net.Dialer{ + Timeout: 30 * time.Second, + } + + conn, err := tls.DialWithDialer(d, "tcp", addr, tlsConfig) if err != nil { return fmt.Errorf("failed to create TLS connection: %w", err) } defer conn.Close() + // Check if context is cancelled + select { + case <-ctx.Done(): + return fmt.Errorf("connection cancelled: %w", ctx.Err()) + default: + } + // Create SMTP client client, err := smtp.NewClient(conn, p.host) if err != nil { @@ -318,8 +344,9 @@ func (p *SMTPProvider) sendWithTLS(ctx context.Context, addr string, auth smtp.A } } - // Set sender - if err := client.Mail(p.from); err != nil { + // Set sender (extract pure email address from potentially formatted from address) + fromEmail := extractEmailAddress(p.from) + if err := client.Mail(fromEmail); err != nil { return fmt.Errorf("failed to set sender: %w", err) } diff --git a/messenger/providers/smtp/smtp_test.go b/messenger/providers/smtp/smtp_test.go new file mode 100644 index 00000000..d1c49d69 --- /dev/null +++ b/messenger/providers/smtp/smtp_test.go @@ -0,0 +1,668 @@ +package smtp + +import ( + "context" + "os" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/yaoapp/yao/config" + "github.com/yaoapp/yao/messenger/types" + "github.com/yaoapp/yao/test" +) + +// Test constants for authorized recipient addresses +const ( + TestEmailAgent = "agent@iqka.com" + TestEmailX = "x@iqka.com" + TestEmailXiang = "xiang@iqka.com" +) + +// Test helper functions + +func createTestMessage(msgType types.MessageType) *types.Message { + message := &types.Message{ + Type: msgType, + To: []string{"test@example.com"}, + Subject: "Test Email", + Body: "This is a test email body", + HTML: "

Test Email

This is a test email body

", + Headers: map[string]string{ + "X-Test-Header": "test-value", + }, + Metadata: map[string]interface{}{ + "campaign": "test-campaign", + "user_id": "12345", + }, + Priority: 1, + } + return message +} + +func loadPrimaryTestConfig(t *testing.T) types.ProviderConfig { + // Prepare test environment using YAO_TEST_APPLICATION which points to yao-dev-app + // Environment variables are already set in env.local.sh + test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION") + defer test.Clean() + + // Create test config directly using environment variables for primary SMTP + // Port 465 requires SSL, port 587 requires TLS + smtpPort := os.Getenv("SMTP_PORT") + useSSL := smtpPort == "465" + useTLS := smtpPort == "587" || smtpPort == "25" + + config := types.ProviderConfig{ + Name: "primary", + Connector: "smtp", + Options: map[string]interface{}{ + "host": os.Getenv("SMTP_HOST"), + "port": os.Getenv("SMTP_PORT"), + "username": os.Getenv("SMTP_USERNAME"), + "password": os.Getenv("SMTP_PASSWORD"), + "from": os.Getenv("SMTP_FROM"), + "use_tls": useTLS, + "use_ssl": useSSL, + }, + } + + return config +} + +func loadReliableTestConfig(t *testing.T) types.ProviderConfig { + // Prepare test environment using YAO_TEST_APPLICATION which points to yao-dev-app + // Environment variables are already set in env.local.sh + test.Prepare(t, config.Conf, "YAO_TEST_APPLICATION") + defer test.Clean() + + // Create test config directly using environment variables for reliable SMTP + config := types.ProviderConfig{ + Name: "reliable", + Connector: "smtp", + Options: map[string]interface{}{ + "host": os.Getenv("RELIABLE_SMTP_HOST"), + "port": 587, // Hardcoded in reliable.smtp.yao + "username": os.Getenv("RELIABLE_SMTP_USERNAME"), + "password": os.Getenv("RELIABLE_SMTP_PASSWORD"), + "from": os.Getenv("RELIABLE_SMTP_FROM"), + "use_tls": true, + }, + } + + return config +} + +// Test NewSMTPProvider + +func TestNewSMTPProvider_Primary(t *testing.T) { + config := loadPrimaryTestConfig(t) + + provider, err := NewSMTPProvider(config) + require.NoError(t, err) + assert.NotNil(t, provider) + + // Verify configuration using actual environment variables from env.local.sh + assert.Equal(t, os.Getenv("SMTP_HOST"), provider.host) + assert.Equal(t, os.Getenv("SMTP_USERNAME"), provider.username) + assert.Equal(t, os.Getenv("SMTP_PASSWORD"), provider.password) + assert.Equal(t, os.Getenv("SMTP_FROM"), provider.from) + assert.Equal(t, "primary", provider.config.Name) + // Port 465 uses SSL, not TLS + if os.Getenv("SMTP_PORT") == "465" { + assert.True(t, provider.useSSL) + assert.False(t, provider.useTLS) + } else { + assert.True(t, provider.useTLS) + } +} + +func TestNewSMTPProvider_Reliable(t *testing.T) { + config := loadReliableTestConfig(t) + + provider, err := NewSMTPProvider(config) + require.NoError(t, err) + assert.NotNil(t, provider) + + // Verify configuration using actual environment variables from env.local.sh + assert.Equal(t, os.Getenv("RELIABLE_SMTP_HOST"), provider.host) + assert.Equal(t, 587, provider.port) + assert.Equal(t, os.Getenv("RELIABLE_SMTP_USERNAME"), provider.username) + assert.Equal(t, os.Getenv("RELIABLE_SMTP_PASSWORD"), provider.password) + assert.Equal(t, os.Getenv("RELIABLE_SMTP_FROM"), provider.from) + assert.Equal(t, "reliable", provider.config.Name) + assert.True(t, provider.useTLS) +} + +func TestNewSMTPProvider_MissingOptions(t *testing.T) { + config := types.ProviderConfig{ + Name: "test", + Connector: "smtp", + Options: nil, + } + + provider, err := NewSMTPProvider(config) + assert.Error(t, err) + assert.Nil(t, provider) + assert.Contains(t, err.Error(), "SMTP provider requires options") +} + +func TestNewSMTPProvider_MissingHost(t *testing.T) { + config := types.ProviderConfig{ + Name: "test", + Connector: "smtp", + Options: map[string]interface{}{ + "port": 587, + "username": "test@example.com", + "password": "password", + "from": "test@example.com", + }, + } + + provider, err := NewSMTPProvider(config) + assert.Error(t, err) + assert.Nil(t, provider) + assert.Contains(t, err.Error(), "SMTP provider requires 'host' option") +} + +func TestNewSMTPProvider_MissingUsername(t *testing.T) { + config := types.ProviderConfig{ + Name: "test", + Connector: "smtp", + Options: map[string]interface{}{ + "host": "smtp.example.com", + "port": 587, + "password": "password", + "from": "test@example.com", + }, + } + + provider, err := NewSMTPProvider(config) + assert.Error(t, err) + assert.Nil(t, provider) + assert.Contains(t, err.Error(), "SMTP provider requires 'username' option") +} + +func TestNewSMTPProvider_MissingPassword(t *testing.T) { + config := types.ProviderConfig{ + Name: "test", + Connector: "smtp", + Options: map[string]interface{}{ + "host": "smtp.example.com", + "port": 587, + "username": "test@example.com", + "from": "test@example.com", + }, + } + + provider, err := NewSMTPProvider(config) + assert.Error(t, err) + assert.Nil(t, provider) + assert.Contains(t, err.Error(), "SMTP provider requires 'password' option") +} + +func TestNewSMTPProvider_MissingFrom(t *testing.T) { + config := types.ProviderConfig{ + Name: "test", + Connector: "smtp", + Options: map[string]interface{}{ + "host": "smtp.example.com", + "port": 587, + "username": "test@example.com", + "password": "password", + }, + } + + provider, err := NewSMTPProvider(config) + assert.Error(t, err) + assert.Nil(t, provider) + assert.Contains(t, err.Error(), "SMTP provider requires 'from' option") +} + +// Test Provider Interface Methods + +func TestGetType(t *testing.T) { + config := loadPrimaryTestConfig(t) + provider, err := NewSMTPProvider(config) + require.NoError(t, err) + + assert.Equal(t, "smtp", provider.GetType()) +} + +func TestGetName(t *testing.T) { + config := loadPrimaryTestConfig(t) + provider, err := NewSMTPProvider(config) + require.NoError(t, err) + + assert.Equal(t, "primary", provider.GetName()) +} + +func TestValidate(t *testing.T) { + config := loadPrimaryTestConfig(t) + provider, err := NewSMTPProvider(config) + require.NoError(t, err) + + err = provider.Validate() + assert.NoError(t, err) +} + +func TestValidate_MissingHost(t *testing.T) { + config := loadPrimaryTestConfig(t) + provider, err := NewSMTPProvider(config) + require.NoError(t, err) + + provider.host = "" + err = provider.Validate() + assert.Error(t, err) + assert.Contains(t, err.Error(), "host is required") +} + +func TestValidate_InvalidPort(t *testing.T) { + config := loadPrimaryTestConfig(t) + provider, err := NewSMTPProvider(config) + require.NoError(t, err) + + provider.port = 0 + err = provider.Validate() + assert.Error(t, err) + assert.Contains(t, err.Error(), "port must be positive") +} + +func TestValidate_MissingUsername(t *testing.T) { + config := loadPrimaryTestConfig(t) + provider, err := NewSMTPProvider(config) + require.NoError(t, err) + + provider.username = "" + err = provider.Validate() + assert.Error(t, err) + assert.Contains(t, err.Error(), "username is required") +} + +func TestValidate_MissingPassword(t *testing.T) { + config := loadPrimaryTestConfig(t) + provider, err := NewSMTPProvider(config) + require.NoError(t, err) + + provider.password = "" + err = provider.Validate() + assert.Error(t, err) + assert.Contains(t, err.Error(), "password is required") +} + +func TestValidate_MissingFrom(t *testing.T) { + config := loadPrimaryTestConfig(t) + provider, err := NewSMTPProvider(config) + require.NoError(t, err) + + provider.from = "" + err = provider.Validate() + assert.Error(t, err) + assert.Contains(t, err.Error(), "from address is required") +} + +func TestClose(t *testing.T) { + config := loadPrimaryTestConfig(t) + provider, err := NewSMTPProvider(config) + require.NoError(t, err) + + err = provider.Close() + assert.NoError(t, err) +} + +// Test Send Methods + +func TestSend_NonEmailMessage(t *testing.T) { + config := loadPrimaryTestConfig(t) + provider, err := NewSMTPProvider(config) + require.NoError(t, err) + + ctx := context.Background() + smsMessage := createTestMessage(types.MessageTypeSMS) + + err = provider.Send(ctx, smsMessage) + assert.Error(t, err) + assert.Contains(t, err.Error(), "SMTP provider only supports email messages") +} + +func TestSend_EmailMessage_RealAPI_Primary(t *testing.T) { + config := loadPrimaryTestConfig(t) + provider, err := NewSMTPProvider(config) + require.NoError(t, err) + + // Use context with reasonable timeout for SMTP operations + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + // Use test recipient addresses that are authorized for testing + emailMessage := &types.Message{ + Type: types.MessageTypeEmail, + To: []string{TestEmailAgent}, + Subject: "SMTP Unit Test Email - " + time.Now().Format("2006-01-02 15:04:05"), + Body: "This is a unit test email sent via real SMTP server", + HTML: "

SMTP Unit Test

This is a unit test email sent via real SMTP server

", + Headers: map[string]string{ + "X-Test-Run": "smtp-provider-test", + }, + Metadata: map[string]interface{}{ + "test_type": "unit_test", + "timestamp": time.Now().Unix(), + }, + } + + err = provider.Send(ctx, emailMessage) + if err != nil { + // Log error but don't fail test, as it might be network or SMTP configuration issues + t.Logf("Real SMTP API call failed (this may be expected in CI/test environment): %v", err) + + // Check if it's expected error type (network, authentication, etc.) + if strings.Contains(err.Error(), "SMTP authentication failed") { + t.Log("SMTP authentication failed - this indicates the request reached the server") + } else if strings.Contains(err.Error(), "failed to connect to SMTP server") { + t.Log("Network error - this may be expected in test environment") + } else { + t.Logf("Unexpected error type: %v", err) + } + } else { + t.Log("Real SMTP API call succeeded") + } +} + +func TestSend_EmailMessage_RealAPI_Reliable(t *testing.T) { + config := loadReliableTestConfig(t) + provider, err := NewSMTPProvider(config) + require.NoError(t, err) + + // Use context with reasonable timeout for SMTP operations + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + // Use test recipient addresses that are authorized for testing + emailMessage := &types.Message{ + Type: types.MessageTypeEmail, + To: []string{TestEmailX}, + Subject: "Reliable SMTP Unit Test - " + time.Now().Format("2006-01-02 15:04:05"), + Body: "This is a unit test email sent via reliable SMTP server", + HTML: "

Reliable SMTP Test

This is a unit test email sent via reliable SMTP server

", + Headers: map[string]string{ + "X-Test-Run": "smtp-reliable-test", + "X-Test-Type": "reliable-smtp", + }, + Metadata: map[string]interface{}{ + "test_type": "reliable_test", + "timestamp": time.Now().Unix(), + }, + } + + err = provider.Send(ctx, emailMessage) + if err != nil { + // Log error but don't fail test, as it might be network or SMTP configuration issues + t.Logf("Real reliable SMTP API call failed (this may be expected in CI/test environment): %v", err) + + // Check if it's expected error type (network, authentication, etc.) + if strings.Contains(err.Error(), "SMTP authentication failed") { + t.Log("Reliable SMTP authentication failed - this indicates the request reached the server") + } else if strings.Contains(err.Error(), "failed to connect to SMTP server") { + t.Log("Network error - this may be expected in test environment") + } else { + t.Logf("Unexpected error type: %v", err) + } + } else { + t.Log("Real reliable SMTP API call succeeded") + } +} + +func TestSend_ContextTimeout_RealAPI(t *testing.T) { + config := loadPrimaryTestConfig(t) + provider, err := NewSMTPProvider(config) + require.NoError(t, err) + + // Create a very short timeout context to test timeout functionality + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) + defer cancel() + + emailMessage := &types.Message{ + Type: types.MessageTypeEmail, + To: []string{TestEmailX}, + Subject: "SMTP Context Timeout Test", + Body: "This should timeout before sending", + } + + err = provider.Send(ctx, emailMessage) + assert.Error(t, err) + + // Verify it's a context timeout error + if strings.Contains(err.Error(), "context deadline exceeded") { + t.Log("Context timeout working correctly with real SMTP API") + } else if strings.Contains(err.Error(), "context canceled") { + t.Log("Context cancellation working correctly with real SMTP API") + } else { + t.Logf("Got different error (may be network related): %v", err) + } +} + +func TestSendBatch_RealAPI(t *testing.T) { + config := loadPrimaryTestConfig(t) + provider, err := NewSMTPProvider(config) + require.NoError(t, err) + + // Use context with reasonable timeout for SMTP batch operations + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + // Create multiple test emails using authorized test addresses + messages := []*types.Message{ + { + Type: types.MessageTypeEmail, + To: []string{TestEmailX}, + Subject: "SMTP Batch Test 1 - " + time.Now().Format("15:04:05"), + Body: "SMTP batch test message 1", + HTML: "

SMTP batch test message 1

", + }, + { + Type: types.MessageTypeEmail, + To: []string{TestEmailXiang}, + Subject: "SMTP Batch Test 2 - " + time.Now().Format("15:04:05"), + Body: "SMTP batch test message 2", + HTML: "

SMTP batch test message 2

", + }, + } + + err = provider.SendBatch(ctx, messages) + if err != nil { + t.Logf("Real SMTP batch API call failed (this may be expected): %v", err) + + // Verify error handling logic + if strings.Contains(err.Error(), "failed to send message to") { + t.Log("SMTP batch sending failed as expected - error handling works correctly") + } + } else { + t.Log("Real SMTP batch API call succeeded") + } +} + +func TestSend_MultipleRecipients_RealAPI(t *testing.T) { + config := loadPrimaryTestConfig(t) + provider, err := NewSMTPProvider(config) + require.NoError(t, err) + + // Use context with reasonable timeout for SMTP operations + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + // Test with multiple authorized recipient addresses + emailMessage := &types.Message{ + Type: types.MessageTypeEmail, + To: []string{TestEmailAgent, TestEmailX, TestEmailXiang}, + Subject: "SMTP Multiple Recipients Test - " + time.Now().Format("15:04:05"), + Body: "This email is sent to multiple recipients for SMTP testing", + HTML: "

SMTP Multiple Recipients Test

This email is sent to multiple recipients for SMTP testing

", + Headers: map[string]string{ + "X-Test-Type": "smtp-multiple-recipients", + }, + } + + err = provider.Send(ctx, emailMessage) + if err != nil { + t.Logf("SMTP multiple recipients API call failed (this may be expected): %v", err) + + // Check error handling for multiple recipients + if strings.Contains(err.Error(), "SMTP authentication failed") { + t.Log("SMTP multiple recipients test reached SMTP server") + } + } else { + t.Log("SMTP multiple recipients API call succeeded") + } +} + +// Test Edge Cases + +func TestSend_WithCustomFrom(t *testing.T) { + config := loadPrimaryTestConfig(t) + provider, err := NewSMTPProvider(config) + require.NoError(t, err) + + ctx := context.Background() + emailMessage := &types.Message{ + Type: types.MessageTypeEmail, + To: []string{TestEmailAgent}, + Subject: "SMTP Custom From Test - " + time.Now().Format("15:04:05"), + Body: "This email tests custom from address", + From: "custom-sender@example.com", // Custom from address + Headers: map[string]string{ + "X-Test-Type": "custom-from", + }, + } + + err = provider.Send(ctx, emailMessage) + if err != nil { + t.Logf("SMTP custom from test failed (this may be expected): %v", err) + } else { + t.Log("SMTP custom from test succeeded") + } +} + +func TestSend_PlainTextOnly(t *testing.T) { + config := loadPrimaryTestConfig(t) + provider, err := NewSMTPProvider(config) + require.NoError(t, err) + + ctx := context.Background() + emailMessage := &types.Message{ + Type: types.MessageTypeEmail, + To: []string{TestEmailAgent}, + Subject: "SMTP Plain Text Test - " + time.Now().Format("15:04:05"), + Body: "This is a plain text only email for testing SMTP functionality", + // No HTML content + Headers: map[string]string{ + "X-Test-Type": "plain-text-only", + }, + } + + err = provider.Send(ctx, emailMessage) + if err != nil { + t.Logf("SMTP plain text test failed (this may be expected): %v", err) + } else { + t.Log("SMTP plain text test succeeded") + } +} + +func TestSend_HTMLOnly(t *testing.T) { + config := loadPrimaryTestConfig(t) + provider, err := NewSMTPProvider(config) + require.NoError(t, err) + + ctx := context.Background() + emailMessage := &types.Message{ + Type: types.MessageTypeEmail, + To: []string{TestEmailAgent}, + Subject: "SMTP HTML Only Test - " + time.Now().Format("15:04:05"), + HTML: "

HTML Only Email

This is an HTML only email for testing SMTP functionality

Bold text and italic text

", + // No plain text body + Headers: map[string]string{ + "X-Test-Type": "html-only", + }, + } + + err = provider.Send(ctx, emailMessage) + if err != nil { + t.Logf("SMTP HTML only test failed (this may be expected): %v", err) + } else { + t.Log("SMTP HTML only test succeeded") + } +} + +func TestSend_MultipartMessage(t *testing.T) { + config := loadPrimaryTestConfig(t) + provider, err := NewSMTPProvider(config) + require.NoError(t, err) + + ctx := context.Background() + emailMessage := &types.Message{ + Type: types.MessageTypeEmail, + To: []string{TestEmailAgent}, + Subject: "SMTP Multipart Test - " + time.Now().Format("15:04:05"), + Body: "This is the plain text version of a multipart email for testing SMTP functionality", + HTML: "

Multipart Email

This is the HTML version of a multipart email for testing SMTP functionality

Both plain text and HTML versions are included.

", + Headers: map[string]string{ + "X-Test-Type": "multipart-message", + }, + } + + err = provider.Send(ctx, emailMessage) + if err != nil { + t.Logf("SMTP multipart test failed (this may be expected): %v", err) + } else { + t.Log("SMTP multipart test succeeded") + } +} + +// Benchmark Tests + +func BenchmarkNewSMTPProvider(b *testing.B) { + // Setup + t := &testing.T{} + config := loadPrimaryTestConfig(t) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + provider, err := NewSMTPProvider(config) + if err != nil { + b.Fatal(err) + } + _ = provider + } +} + +func BenchmarkValidate(b *testing.B) { + t := &testing.T{} + config := loadPrimaryTestConfig(t) + provider, err := NewSMTPProvider(config) + if err != nil { + b.Fatal(err) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + err := provider.Validate() + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkBuildMessage(b *testing.B) { + t := &testing.T{} + config := loadPrimaryTestConfig(t) + provider, err := NewSMTPProvider(config) + if err != nil { + b.Fatal(err) + } + + message := createTestMessage(types.MessageTypeEmail) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := provider.buildMessage(message) + if err != nil { + b.Fatal(err) + } + } +}