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,128 +57,106 @@ 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)) }
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)
}
t.Run("TestSendEmailWithAttachments", func(t *testing.T) { func TestSendEmail_InvalidRecipient(t *testing.T) {
reader, err := os.Open("testdata/attachment1.txt") cfg := newConfig(".env.test")
require.NoError(t, err) mailSrv := NewMailService(MailServiceConfig{
defer reader.Close() 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)
}
reader2, err := os.Open("testdata/attachment2.txt") func TestSendEmailWithAttachments(t *testing.T) {
require.NoError(t, err) cfg := newConfig(".env.test")
defer reader2.Close()
reader3, err := os.Open("testdata/attachment3.txt") mailSrv := NewMailService(MailServiceConfig{
require.NoError(t, err) Auth: smtp.PlainAuth("", cfg.MailUser, cfg.MailPassword, cfg.MailHost),
defer reader3.Close() Host: cfg.MailHost,
Port: cfg.MailPort,
From: cfg.MailFrom,
})
reader, err := os.Open("testdata/attachment1.txt")
require.NoError(t, err)
defer reader.Close()
data := EmailMessage{ reader2, err := os.Open("testdata/attachment2.txt")
To: cfg.MailTo, require.NoError(t, err)
Subject: "Mail Sender", defer reader2.Close()
Body: "Hello this is a test email",
Attachments: []EmailAttachment{ reader3, err := os.Open("testdata/attachment3.txt")
{ require.NoError(t, err)
Title: "attachment1.txt", defer reader3.Close()
File: reader,
}, data := EmailMessage{
{ To: cfg.MailTo,
Title: "attachment2.txt", Subject: "Mail Sender",
File: reader2, Body: "Hello this is a test email",
}, Attachments: []EmailAttachment{
{ {
Title: "attachment3.txt", Title: "attachment1.txt",
File: reader3, File: reader,
},
}, },
} {
err = mailSrv.SendEmail(data) Title: "attachment2.txt",
require.NoError(t, err) File: reader2,
}) },
{
t.Run("TestWithAttachments", func(t *testing.T) { Title: "attachment3.txt",
msg := newMessage("from", "to", "subject") File: reader3,
content, err := msg.withAttachments("body", nil) },
require.NoError(t, err) },
assert.Greater(t, len(content), 0) }
}) err = mailSrv.SendEmail(data)
require.NoError(t, err)
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) { func TestWithAttachments(t *testing.T) {
cfg := newConfig(".env.test") msg := newMessage("from", "to", "subject")
content, err := msg.withAttachments("body", nil)
emailService := NewSecure(MailServiceConfig{ require.NoError(t, err)
Auth: smtp.PlainAuth("", cfg.MailUser, cfg.MailPassword, cfg.MailHost), assert.Greater(t, len(content), 0)
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)
})
} }