first commit
This commit is contained in:
123
pkg/mailer/mailer.go
Normal file
123
pkg/mailer/mailer.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package mailer
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func envBool(key string) bool {
|
||||
v, err := strconv.ParseBool(strings.TrimSpace(os.Getenv(key)))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func Send(to, subject, body string) error {
|
||||
host := strings.TrimSpace(os.Getenv("EMAIL_HOST"))
|
||||
port := strings.TrimSpace(os.Getenv("EMAIL_PORT"))
|
||||
from := strings.TrimSpace(os.Getenv("EMAIL_FROM"))
|
||||
username := strings.TrimSpace(os.Getenv("EMAIL_HOST_USER"))
|
||||
password := strings.TrimSpace(os.Getenv("EMAIL_HOST_PASSWORD"))
|
||||
|
||||
if host == "" || port == "" || from == "" {
|
||||
return fmt.Errorf("email configuration is incomplete")
|
||||
}
|
||||
|
||||
addr := host + ":" + port
|
||||
msg := strings.Join([]string{
|
||||
"From: " + from,
|
||||
"To: " + to,
|
||||
"Subject: " + subject,
|
||||
"MIME-Version: 1.0",
|
||||
"Content-Type: text/plain; charset=UTF-8",
|
||||
"",
|
||||
body,
|
||||
}, "\r\n")
|
||||
|
||||
var auth smtp.Auth
|
||||
if username != "" {
|
||||
auth = smtp.PlainAuth("", username, password, host)
|
||||
}
|
||||
|
||||
useSSL := envBool("EMAIL_USE_SSL")
|
||||
useTLS := envBool("EMAIL_USE_TLS")
|
||||
|
||||
if useSSL {
|
||||
conn, err := tls.Dial("tcp", addr, &tls.Config{ServerName: host})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client, err := smtp.NewClient(conn, host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Quit()
|
||||
|
||||
if auth != nil {
|
||||
if err := client.Auth(auth); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := client.Mail(from); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := client.Rcpt(to); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w, err := client.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.Write([]byte(msg)); err != nil {
|
||||
_ = w.Close()
|
||||
return err
|
||||
}
|
||||
return w.Close()
|
||||
}
|
||||
|
||||
if !useTLS {
|
||||
return smtp.SendMail(addr, auth, from, []string{to}, []byte(msg))
|
||||
}
|
||||
|
||||
client, err := smtp.Dial(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Quit()
|
||||
|
||||
if err := client.StartTLS(&tls.Config{ServerName: host}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if auth != nil {
|
||||
if err := client.Auth(auth); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := client.Mail(from); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := client.Rcpt(to); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w, err := client.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.Write([]byte(msg)); err != nil {
|
||||
_ = w.Close()
|
||||
return err
|
||||
}
|
||||
return w.Close()
|
||||
}
|
||||
130
pkg/mailer/mailer_test.go
Normal file
130
pkg/mailer/mailer_test.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package mailer
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSendFailsWhenConfigMissing(t *testing.T) {
|
||||
t.Setenv("EMAIL_HOST", "")
|
||||
t.Setenv("EMAIL_PORT", "")
|
||||
t.Setenv("EMAIL_FROM", "")
|
||||
|
||||
if err := Send("user@example.com", "subj", "body"); err == nil {
|
||||
t.Fatal("expected error for incomplete email config")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendPlainSMTP(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to listen: %v", err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
r := bufio.NewReader(conn)
|
||||
w := bufio.NewWriter(conn)
|
||||
write := func(s string) error {
|
||||
if _, err := w.WriteString(s); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.Flush()
|
||||
}
|
||||
|
||||
if err := write("220 localhost Simple Mail Transfer Service Ready\r\n"); err != nil {
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
line, err := r.ReadString('\n')
|
||||
if err != nil {
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
cmd := strings.TrimSpace(line)
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(strings.ToUpper(cmd), "EHLO") || strings.HasPrefix(strings.ToUpper(cmd), "HELO"):
|
||||
if err := write("250-localhost\r\n250 AUTH PLAIN\r\n"); err != nil {
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
case strings.HasPrefix(strings.ToUpper(cmd), "MAIL FROM"):
|
||||
if err := write("250 OK\r\n"); err != nil {
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
case strings.HasPrefix(strings.ToUpper(cmd), "RCPT TO"):
|
||||
if err := write("250 OK\r\n"); err != nil {
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
case strings.HasPrefix(strings.ToUpper(cmd), "DATA"):
|
||||
if err := write("354 End data with <CR><LF>.<CR><LF>\r\n"); err != nil {
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
for {
|
||||
d, err := r.ReadString('\n')
|
||||
if err != nil {
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(d) == "." {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err := write("250 OK queued\r\n"); err != nil {
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
case strings.HasPrefix(strings.ToUpper(cmd), "QUIT"):
|
||||
_ = write("221 Bye\r\n")
|
||||
done <- nil
|
||||
return
|
||||
default:
|
||||
if err := write("250 OK\r\n"); err != nil {
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
host, portStr, err := net.SplitHostPort(ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse listener address: %v", err)
|
||||
}
|
||||
if _, err := strconv.Atoi(portStr); err != nil {
|
||||
t.Fatalf("invalid test smtp port: %v", err)
|
||||
}
|
||||
|
||||
t.Setenv("EMAIL_HOST", host)
|
||||
t.Setenv("EMAIL_PORT", portStr)
|
||||
t.Setenv("EMAIL_FROM", "noreply@example.com")
|
||||
t.Setenv("EMAIL_HOST_USER", "")
|
||||
t.Setenv("EMAIL_HOST_PASSWORD", "")
|
||||
t.Setenv("EMAIL_USE_TLS", "false")
|
||||
t.Setenv("EMAIL_USE_SSL", "false")
|
||||
|
||||
if err := Send("user@example.com", "Verify", "Hello"); err != nil {
|
||||
t.Fatalf("expected send success, got error: %v", err)
|
||||
}
|
||||
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("smtp server finished with error: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user