feat: responsive HTML email templates with DreamHost SMTP
- Add professional HTML email template matching CV aesthetic - Implement multipart emails (HTML + plain text fallback) - Configure DreamHost SMTP with SSL (port 465) - Add "light only" color scheme for Gmail iOS compatibility - Include Reply-To header for easy sender response - Add email validation and integration tests - Update .env.example with DreamHost/Gmail SMTP examples - Add .env to .gitignore to protect credentials - Document email template customization and dark mode approach
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
package integration_test
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/juanatsap/cv-site/internal/services"
|
||||
)
|
||||
|
||||
// TestSMTPConnection tests that SMTP credentials are valid and connection works
|
||||
// This test requires valid SMTP credentials in environment variables
|
||||
// Run with: go test -v ./tests/integration/... -run TestSMTPConnection
|
||||
func TestSMTPConnection(t *testing.T) {
|
||||
// Skip in CI or when credentials aren't available
|
||||
host := os.Getenv("SMTP_HOST")
|
||||
port := os.Getenv("SMTP_PORT")
|
||||
user := os.Getenv("SMTP_USER")
|
||||
pass := os.Getenv("SMTP_PASSWORD")
|
||||
|
||||
if host == "" || user == "" || pass == "" {
|
||||
t.Skip("Skipping SMTP test: SMTP credentials not configured")
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%s", host, port)
|
||||
|
||||
t.Run("TLS_Connection", func(t *testing.T) {
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: host,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
|
||||
if port == "465" {
|
||||
// Implicit SSL
|
||||
conn, err := tls.Dial("tcp", addr, tlsConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("TLS dial failed: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
t.Log("TLS connection established (port 465 - implicit SSL)")
|
||||
} else {
|
||||
// STARTTLS
|
||||
client, err := smtp.Dial(addr)
|
||||
if err != nil {
|
||||
t.Fatalf("SMTP dial failed: %v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
if err := client.StartTLS(tlsConfig); err != nil {
|
||||
t.Fatalf("STARTTLS failed: %v", err)
|
||||
}
|
||||
t.Log("TLS connection established (STARTTLS)")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SMTP_Authentication", func(t *testing.T) {
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: host,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
|
||||
var client *smtp.Client
|
||||
var err error
|
||||
|
||||
if port == "465" {
|
||||
// Implicit SSL - connect with TLS from the start
|
||||
conn, err := tls.Dial("tcp", addr, tlsConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("TLS dial failed: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client, err = smtp.NewClient(conn, host)
|
||||
if err != nil {
|
||||
t.Fatalf("SMTP client creation failed: %v", err)
|
||||
}
|
||||
} else {
|
||||
// STARTTLS
|
||||
client, err = smtp.Dial(addr)
|
||||
if err != nil {
|
||||
t.Fatalf("SMTP dial failed: %v", err)
|
||||
}
|
||||
|
||||
if err := client.StartTLS(tlsConfig); err != nil {
|
||||
t.Fatalf("STARTTLS failed: %v", err)
|
||||
}
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
auth := smtp.PlainAuth("", user, pass, host)
|
||||
if err := client.Auth(auth); err != nil {
|
||||
t.Fatalf("SMTP authentication failed: %v", err)
|
||||
}
|
||||
|
||||
t.Log("SMTP authentication successful")
|
||||
_ = client.Quit()
|
||||
})
|
||||
}
|
||||
|
||||
// TestEmailServiceSend tests actual email sending
|
||||
// This will send a real test email - use sparingly
|
||||
// Run with: go test -v ./tests/integration/... -run TestEmailServiceSend
|
||||
func TestEmailServiceSend(t *testing.T) {
|
||||
// Skip in CI or when credentials aren't available
|
||||
host := os.Getenv("SMTP_HOST")
|
||||
port := os.Getenv("SMTP_PORT")
|
||||
user := os.Getenv("SMTP_USER")
|
||||
pass := os.Getenv("SMTP_PASSWORD")
|
||||
from := os.Getenv("SMTP_FROM_EMAIL")
|
||||
to := os.Getenv("CONTACT_EMAIL")
|
||||
|
||||
if host == "" || user == "" || pass == "" {
|
||||
t.Skip("Skipping email send test: SMTP credentials not configured")
|
||||
}
|
||||
|
||||
if from == "" {
|
||||
from = user
|
||||
}
|
||||
if to == "" {
|
||||
t.Skip("Skipping email send test: CONTACT_EMAIL not configured")
|
||||
}
|
||||
|
||||
config := &services.EmailConfig{
|
||||
SMTPHost: host,
|
||||
SMTPPort: port,
|
||||
SMTPUser: user,
|
||||
SMTPPassword: pass,
|
||||
FromEmail: from,
|
||||
ToEmail: to,
|
||||
}
|
||||
|
||||
emailService := services.NewEmailService(config)
|
||||
|
||||
testData := &services.ContactFormData{
|
||||
Email: "test-sender@example.com",
|
||||
Name: "Integration Test",
|
||||
Company: "Test Suite",
|
||||
Subject: "Email Integration Test",
|
||||
Message: "This is an automated test email sent by the integration test suite. If you receive this, the email configuration is working correctly.",
|
||||
IP: "127.0.0.1",
|
||||
Time: time.Now(),
|
||||
}
|
||||
|
||||
t.Run("SendContactForm", func(t *testing.T) {
|
||||
err := emailService.SendContactForm(testData)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to send email: %v", err)
|
||||
}
|
||||
t.Logf("Test email sent successfully to %s", to)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEmailServiceValidation tests that the email service properly validates input
|
||||
func TestEmailServiceValidation(t *testing.T) {
|
||||
config := &services.EmailConfig{
|
||||
SMTPHost: "smtp.test.com",
|
||||
SMTPPort: "465",
|
||||
SMTPUser: "test@test.com",
|
||||
SMTPPassword: "password",
|
||||
FromEmail: "from@test.com",
|
||||
ToEmail: "to@test.com",
|
||||
}
|
||||
|
||||
emailService := services.NewEmailService(config)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
data *services.ContactFormData
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "valid data",
|
||||
data: &services.ContactFormData{
|
||||
Email: "valid@example.com",
|
||||
Name: "Valid User",
|
||||
Message: "This is a valid message with more than 10 characters.",
|
||||
Time: time.Now(),
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "missing email",
|
||||
data: &services.ContactFormData{
|
||||
Name: "No Email",
|
||||
Message: "This is a valid message.",
|
||||
Time: time.Now(),
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "email is required",
|
||||
},
|
||||
{
|
||||
name: "invalid email format",
|
||||
data: &services.ContactFormData{
|
||||
Email: "notanemail",
|
||||
Name: "Bad Email",
|
||||
Message: "This is a valid message.",
|
||||
Time: time.Now(),
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "invalid email format",
|
||||
},
|
||||
{
|
||||
name: "missing message",
|
||||
data: &services.ContactFormData{
|
||||
Email: "valid@example.com",
|
||||
Name: "No Message",
|
||||
Time: time.Now(),
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "message is required",
|
||||
},
|
||||
{
|
||||
name: "message too short",
|
||||
data: &services.ContactFormData{
|
||||
Email: "valid@example.com",
|
||||
Name: "Short Msg",
|
||||
Message: "Hi",
|
||||
Time: time.Now(),
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "message too short",
|
||||
},
|
||||
{
|
||||
name: "email with newlines (header injection)",
|
||||
data: &services.ContactFormData{
|
||||
Email: "test@example.com\nBcc: attacker@evil.com",
|
||||
Name: "Attacker",
|
||||
Message: "Trying to inject headers.",
|
||||
Time: time.Now(),
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "prohibited characters",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// We can't actually send since we don't have real SMTP
|
||||
// but we can test validation
|
||||
err := tt.data.Validate()
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("expected error containing %q, got nil", tt.errMsg)
|
||||
} else if tt.errMsg != "" && !containsString(err.Error(), tt.errMsg) {
|
||||
t.Errorf("expected error containing %q, got %q", tt.errMsg, err.Error())
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_ = emailService // Avoid unused variable warning
|
||||
}
|
||||
|
||||
func containsString(s, substr string) bool {
|
||||
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsStringHelper(s, substr))
|
||||
}
|
||||
|
||||
func containsStringHelper(s, substr string) bool {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user