diff --git a/Containerfile b/Containerfile index 799c36f..f1ef53a 100644 --- a/Containerfile +++ b/Containerfile @@ -39,6 +39,6 @@ COPY --from=builder /app/static ./static # Copy .env (optional - can be overridden at runtime) COPY .env .env -EXPOSE 8080 +EXPOSE 5000 CMD ["./server"] \ No newline at end of file diff --git a/go.mod b/go.mod index 9a0cdfc..500d254 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.25.4 require ( github.com/jackc/pgx/v5 v5.7.6 github.com/joho/godotenv v1.5.1 - github.com/wneessen/go-mail v0.7.2 ) require ( diff --git a/go.sum b/go.sum index 9b8a271..c0d6ef5 100644 --- a/go.sum +++ b/go.sum @@ -18,8 +18,6 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8= -github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= diff --git a/internal/config/config.go b/internal/config/config.go index a07f3f9..d4b906f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -26,14 +26,14 @@ func LoadConfig() (*Config, error) { cfg := &Config{ Host: getEnv("HOST", "0.0.0.0"), - Port: getEnv("PORT", "8080"), - DBHost: getEnv("PG_HOST", "localhost"), - DBPort: getEnv("PG_PORT", "5432"), - DBName: getEnv("PG_DATABASE", "newsletter"), - DBUser: getEnv("PG_USER", "postgres"), + Port: getEnv("PORT", "5000"), + DBHost: getEnv("PG_HOST", ""), + DBPort: getEnv("PG_PORT", ""), + DBName: getEnv("PG_DATABASE", ""), + DBUser: getEnv("PG_USER", ""), DBPass: getEnv("PG_PASSWORD", ""), SMTPHost: getEnv("SMTP_SERVER", ""), - SMTPPort: getEnv("SMTP_PORT", "587"), + SMTPPort: getEnv("SMTP_PORT", ""), SMTPUser: getEnv("SMTP_USER", ""), SMTPPass: getEnv("SMTP_PASSWORD", ""), } diff --git a/internal/email/sender.go b/internal/email/sender.go index 06c03ff..04b902f 100644 --- a/internal/email/sender.go +++ b/internal/email/sender.go @@ -1,9 +1,13 @@ package email import ( + "crypto/tls" "fmt" + "net" + "net/smtp" + "strconv" + "time" - "github.com/wneessen/go-mail" "landing/internal/config" ) @@ -19,42 +23,111 @@ func (s *Sender) SendConfirmationEmail( email string, unsubscribeLink string, ) error { - client, err := mail.NewClient( - s.cfg.SMTPHost, - mail.WithPort(587), - mail.WithSMTPAuth(mail.SMTPAuthPlain), - mail.WithUsername(s.cfg.SMTPUser), - mail.WithPassword(s.cfg.SMTPPass), - ) + // Parse SMTP port from env + port, err := strconv.Atoi(s.cfg.SMTPPort) if err != nil { - return fmt.Errorf("failed to create mail client: %w", err) + return fmt.Errorf("invalid SMTP port '%s': %w", s.cfg.SMTPPort, err) } - msg := mail.NewMsg() - if err := msg.From(s.cfg.SMTPUser); err != nil { - return fmt.Errorf("failed to set from: %w", err) - } - if err := msg.To(email); err != nil { - return fmt.Errorf("failed to set to: %w", err) - } - - msg.Subject("Thanks for subscribing!") - + // Build email message + subject := "Thanks for subscribing!" htmlBody := fmt.Sprintf(`
-Thank you for subscribing to our newsletter.
`, unsubscribeLink) - msg.SetBodyString(mail.TypeTextHTML, htmlBody) + message := fmt.Sprintf( + "From: %s\r\nTo: %s\r\nSubject: %s\r\nContent-Type: text/html; charset=utf-8\r\n\r\n%s", + s.cfg.SMTPUser, + email, + subject, + htmlBody, + ) - if err := client.DialAndSend(msg); err != nil { - return fmt.Errorf("failed to send email: %w", err) + // Create TLS config + tlsConfig := &tls.Config{ + ServerName: s.cfg.SMTPHost, + } + + // Send email using smtp.SendMail + addr := fmt.Sprintf("%s:%d", s.cfg.SMTPHost, port) + auth := smtp.PlainAuth("", s.cfg.SMTPUser, s.cfg.SMTPPass, s.cfg.SMTPHost) + + // Use a custom dialer with timeout + conn, err := net.DialTimeout("tcp", addr, 10*time.Second) + if err != nil { + return fmt.Errorf("failed to connect to SMTP server: %w", err) + } + defer conn.Close() + + client, err := smtp.NewClient(conn, s.cfg.SMTPHost) + if err != nil { + return fmt.Errorf("failed to create SMTP client: %w", err) + } + defer client.Close() + + // Start TLS + if err := client.StartTLS(tlsConfig); err != nil { + return fmt.Errorf("failed to start TLS: %w", err) + } + + // Authenticate + if err := client.Auth(auth); err != nil { + return fmt.Errorf("failed to authenticate: %w", err) + } + + // Set recipient and send + if err := client.Mail(s.cfg.SMTPUser); err != nil { + return fmt.Errorf("failed to set mail from: %w", err) + } + + if err := client.Rcpt(email); err != nil { + return fmt.Errorf("failed to set mail to: %w", err) + } + + wc, err := client.Data() + if err != nil { + return fmt.Errorf("failed to get data writer: %w", err) + } + defer wc.Close() + + if _, err := wc.Write([]byte(message)); err != nil { + return fmt.Errorf("failed to write message: %w", err) + } + + if err := client.Quit(); err != nil { + return fmt.Errorf("failed to quit SMTP: %w", err) } + return nil +} + +// TestConnection tests SMTP connection without sending email +func (s *Sender) TestConnection() error { + port, err := strconv.Atoi(s.cfg.SMTPPort) + if err != nil { + return fmt.Errorf("invalid SMTP port '%s': %w", s.cfg.SMTPPort, err) + } + + // Test TCP connection + addr := fmt.Sprintf("%s:%d", s.cfg.SMTPHost, port) + conn, err := net.DialTimeout("tcp", addr, 10*time.Second) + if err != nil { + return fmt.Errorf("TCP connection failed to %s: %w", addr, err) + } + defer conn.Close() + + // Test SMTP connection + client, err := smtp.NewClient(conn, s.cfg.SMTPHost) + if err != nil { + return fmt.Errorf("failed to create SMTP client: %w", err) + } + defer client.Close() + return nil } \ No newline at end of file