Compare commits

..

No commits in common. "83ef9c2076e4aa9618afa28931f9bbcc90f32207" and "1371889d6d5a09de9dc8b94ca4b7466522f59450" have entirely different histories.

3 changed files with 88 additions and 176 deletions

View File

@ -9,7 +9,7 @@ import (
func main() { func main() {
// Here fill with real data // Here fill with real data
emailService := email.NewInsecure(email.MailServiceConfig{ emailService := email.NewMailService(email.MailServiceConfig{
Auth: smtp.PlainAuth("", "your@email.com", "your-password", "smtp.youremail.com"), Auth: smtp.PlainAuth("", "your@email.com", "your-password", "smtp.youremail.com"),
Host: "smtp.youremail.com", Host: "smtp.youremail.com",
Port: "587", Port: "587",

View File

@ -3,13 +3,10 @@ package email
import ( import (
"bytes" "bytes"
"crypto/tls" "crypto/tls"
"crypto/x509"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"io" "io"
"net/smtp" "net/smtp"
"slices"
"time"
) )
const ( const (
@ -38,7 +35,7 @@ type EmailService struct {
dial SmtpDialFn dial SmtpDialFn
} }
func NewInsecure(config MailServiceConfig) *EmailService { func NewMailService(config MailServiceConfig) *EmailService {
return &EmailService{ return &EmailService{
auth: config.Auth, auth: config.Auth,
host: config.Host, host: config.Host,
@ -52,69 +49,6 @@ func NewInsecure(config MailServiceConfig) *EmailService {
} }
} }
var validCommonNames = []string{"ISRG Root X1", "R3", "DST Root CA X3"}
func NewSecure(config MailServiceConfig) *EmailService {
return &EmailService{
auth: config.Auth,
host: config.Host,
port: config.Port,
from: config.From,
tlsconfig: &tls.Config{
InsecureSkipVerify: true,
ServerName: config.Host,
VerifyConnection: func(cs tls.ConnectionState) error {
// // Check the server's common name
// for _, cert := range cs.PeerCertificates {
// log.Println("cert.DNSNames", cert.DNSNames)
// if err := cert.VerifyHostname(config.Host); err != nil {
// return fmt.Errorf("invalid common name: %w", err)
// }
// }
// Check the certificate chain
opts := x509.VerifyOptions{
Intermediates: x509.NewCertPool(),
}
for _, cert := range cs.PeerCertificates[1:] {
opts.Intermediates.AddCert(cert)
}
_, err := cs.PeerCertificates[0].Verify(opts)
if err != nil {
return fmt.Errorf("invalid certificate chain: %w", err)
}
// Iterate over the certificates again to perform custom checks
for _, cert := range cs.PeerCertificates {
// TODO: add more checks here...
if time.Now().After(cert.NotAfter) {
return fmt.Errorf("certificate has expired")
}
if time.Now().Add(30 * 24 * time.Hour).After(cert.NotAfter) {
return fmt.Errorf("certificate will expire within 30 days")
}
if !slices.Contains(validCommonNames, cert.Issuer.CommonName) {
return fmt.Errorf("certificate is not issued by a trusted CA")
}
// log.Println("cert.ExtKeyUsage", cert.ExtKeyUsage)
// if cert.KeyUsage&x509.KeyUsageDigitalSignature == 0 || len(cert.ExtKeyUsage) == 0 || !slices.Contains(cert.ExtKeyUsage, x509.ExtKeyUsageServerAuth) {
// log.Printf("%+v", cert)
// return fmt.Errorf("certificate cannot be used for server authentication")
// }
if cert.PublicKeyAlgorithm != x509.RSA {
return fmt.Errorf("unsupported public key algorithm")
}
}
return nil
},
},
dial: dial,
}
}
type MailServiceConfig struct { type MailServiceConfig struct {
Auth smtp.Auth Auth smtp.Auth
Host string Host string

View File

@ -57,26 +57,68 @@ func TestMockSendEmail(t *testing.T) {
} }
} }
func TestNewInsecure(t *testing.T) { func TestSendEmail(t *testing.T) {
cfg := newConfig(".env.test") cfg := newConfig(".env.test")
mailSrv := NewInsecure(MailServiceConfig{ mailSrv := NewMailService(MailServiceConfig{
Auth: smtp.PlainAuth("", cfg.MailUser, cfg.MailPassword, cfg.MailHost), Auth: smtp.PlainAuth("", cfg.MailUser, cfg.MailPassword, cfg.MailHost),
Host: cfg.MailHost, Host: cfg.MailHost,
Port: cfg.MailPort, Port: cfg.MailPort,
From: cfg.MailFrom, From: cfg.MailFrom,
}) })
t.Run("TestSendEmail", func(t *testing.T) {
data := EmailMessage{ data := EmailMessage{
To: cfg.MailTo, To: cfg.MailTo,
Subject: "Mail Sender", Subject: "Mail Sender",
Body: "Hello this is a test email", Body: "Hello this is a test email",
} }
require.NoError(t, mailSrv.SendEmail(data)) require.NoError(t, mailSrv.SendEmail(data))
}) }
t.Run("TestSendEmailWithAttachments", func(t *testing.T) { func TestSendEmail_FailedAuthentication(t *testing.T) {
cfg := newConfig(".env.test")
// set up authentication to fail
mailSrv := NewMailService(MailServiceConfig{
Auth: smtp.PlainAuth("", "wronguser", "wrongpassword", cfg.MailHost),
Host: cfg.MailHost,
Port: cfg.MailPort,
From: cfg.MailFrom,
})
data := EmailMessage{
To: cfg.MailTo,
Subject: "Test Email",
Body: "This is a test email.",
}
err := mailSrv.SendEmail(data)
assert.Error(t, err)
}
func TestSendEmail_InvalidRecipient(t *testing.T) {
cfg := newConfig(".env.test")
mailSrv := NewMailService(MailServiceConfig{
Auth: smtp.PlainAuth("", cfg.MailUser, cfg.MailPassword, cfg.MailHost),
Host: cfg.MailHost,
Port: cfg.MailPort,
From: cfg.MailFrom,
})
data := EmailMessage{
To: "invalid_email",
Subject: "Test Email",
Body: "This is a test email.",
}
err := mailSrv.SendEmail(data)
assert.Error(t, err)
}
func TestSendEmailWithAttachments(t *testing.T) {
cfg := newConfig(".env.test")
mailSrv := NewMailService(MailServiceConfig{
Auth: smtp.PlainAuth("", cfg.MailUser, cfg.MailPassword, cfg.MailHost),
Host: cfg.MailHost,
Port: cfg.MailPort,
From: cfg.MailFrom,
})
reader, err := os.Open("testdata/attachment1.txt") reader, err := os.Open("testdata/attachment1.txt")
require.NoError(t, err) require.NoError(t, err)
defer reader.Close() defer reader.Close()
@ -110,75 +152,11 @@ func TestNewInsecure(t *testing.T) {
} }
err = mailSrv.SendEmail(data) err = mailSrv.SendEmail(data)
require.NoError(t, err) require.NoError(t, err)
}) }
t.Run("TestWithAttachments", func(t *testing.T) { func TestWithAttachments(t *testing.T) {
msg := newMessage("from", "to", "subject") msg := newMessage("from", "to", "subject")
content, err := msg.withAttachments("body", nil) content, err := msg.withAttachments("body", nil)
require.NoError(t, err) require.NoError(t, err)
assert.Greater(t, len(content), 0) assert.Greater(t, len(content), 0)
})
t.Run("TestSendEmail_InvalidRecipient", func(t *testing.T) {
data := EmailMessage{
To: "invalid_email",
Subject: "Test Email",
Body: "This is a test email.",
}
err := mailSrv.SendEmail(data)
assert.Error(t, err)
})
t.Run("TestSendEmail_FailedAuthentication", func(t *testing.T) {
// set up authentication to fail
mailSrv := NewInsecure(MailServiceConfig{
Auth: smtp.PlainAuth("", "wronguser", "wrongpassword", cfg.MailHost),
Host: cfg.MailHost,
Port: cfg.MailPort,
From: cfg.MailFrom,
})
data := EmailMessage{
To: cfg.MailTo,
Subject: "Test Email",
Body: "This is a test email.",
}
err := mailSrv.SendEmail(data)
assert.Error(t, err)
})
}
func TestSecure(t *testing.T) {
cfg := newConfig(".env.test")
emailService := NewSecure(MailServiceConfig{
Auth: smtp.PlainAuth("", cfg.MailUser, cfg.MailPassword, cfg.MailHost),
Host: cfg.MailHost,
Port: cfg.MailPort,
From: cfg.MailFrom,
})
// Assert that the tls.Config is set up correctly
assert.NotNil(t, emailService.tlsconfig)
assert.True(t, emailService.tlsconfig.InsecureSkipVerify)
assert.Equal(t, cfg.MailHost, emailService.tlsconfig.ServerName)
assert.NotNil(t, emailService.tlsconfig.VerifyConnection)
t.Run("TestSendEmail", func(t *testing.T) {
// Mock the client and test the StartTLS method
var called bool
mockDialFn := func(hostPort string) (SMTPClientIface, error) {
called = true
return &mockSMTP{}, nil
}
emailService.dial = mockDialFn
data := EmailMessage{
To: cfg.MailTo,
Subject: "Mail Sender",
Body: "Hello this is a test email",
}
require.NoError(t, emailService.SendEmail(data))
assert.Equal(t, true, called)
})
} }