Compare commits
2 Commits
1371889d6d
...
83ef9c2076
Author | SHA1 | Date |
---|---|---|
Urko. | 83ef9c2076 | |
Urko. | 624a269454 |
|
@ -9,7 +9,7 @@ import (
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Here fill with real data
|
// Here fill with real data
|
||||||
emailService := email.NewMailService(email.MailServiceConfig{
|
emailService := email.NewInsecure(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",
|
||||||
|
|
|
@ -3,10 +3,13 @@ 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 (
|
||||||
|
@ -35,7 +38,7 @@ type EmailService struct {
|
||||||
dial SmtpDialFn
|
dial SmtpDialFn
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMailService(config MailServiceConfig) *EmailService {
|
func NewInsecure(config MailServiceConfig) *EmailService {
|
||||||
return &EmailService{
|
return &EmailService{
|
||||||
auth: config.Auth,
|
auth: config.Auth,
|
||||||
host: config.Host,
|
host: config.Host,
|
||||||
|
@ -49,6 +52,69 @@ func NewMailService(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
|
||||||
|
|
|
@ -57,106 +57,128 @@ func TestMockSendEmail(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSendEmail(t *testing.T) {
|
func TestNewInsecure(t *testing.T) {
|
||||||
cfg := newConfig(".env.test")
|
cfg := newConfig(".env.test")
|
||||||
|
|
||||||
mailSrv := NewMailService(MailServiceConfig{
|
mailSrv := NewInsecure(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,
|
||||||
})
|
})
|
||||||
|
|
||||||
data := EmailMessage{
|
t.Run("TestSendEmail", func(t *testing.T) {
|
||||||
To: cfg.MailTo,
|
data := EmailMessage{
|
||||||
Subject: "Mail Sender",
|
To: cfg.MailTo,
|
||||||
Body: "Hello this is a test email",
|
Subject: "Mail Sender",
|
||||||
}
|
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")
|
t.Run("TestSendEmailWithAttachments", func(t *testing.T) {
|
||||||
// set up authentication to fail
|
reader, err := os.Open("testdata/attachment1.txt")
|
||||||
mailSrv := NewMailService(MailServiceConfig{
|
require.NoError(t, err)
|
||||||
Auth: smtp.PlainAuth("", "wronguser", "wrongpassword", cfg.MailHost),
|
defer reader.Close()
|
||||||
Host: cfg.MailHost,
|
|
||||||
Port: cfg.MailPort,
|
reader2, err := os.Open("testdata/attachment2.txt")
|
||||||
From: cfg.MailFrom,
|
require.NoError(t, err)
|
||||||
|
defer reader2.Close()
|
||||||
|
|
||||||
|
reader3, err := os.Open("testdata/attachment3.txt")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer reader3.Close()
|
||||||
|
|
||||||
|
data := EmailMessage{
|
||||||
|
To: cfg.MailTo,
|
||||||
|
Subject: "Mail Sender",
|
||||||
|
Body: "Hello this is a test email",
|
||||||
|
Attachments: []EmailAttachment{
|
||||||
|
{
|
||||||
|
Title: "attachment1.txt",
|
||||||
|
File: reader,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "attachment2.txt",
|
||||||
|
File: reader2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "attachment3.txt",
|
||||||
|
File: reader3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err = mailSrv.SendEmail(data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("TestWithAttachments", func(t *testing.T) {
|
||||||
|
msg := newMessage("from", "to", "subject")
|
||||||
|
content, err := msg.withAttachments("body", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
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)
|
||||||
})
|
})
|
||||||
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) {
|
func TestSecure(t *testing.T) {
|
||||||
cfg := newConfig(".env.test")
|
cfg := newConfig(".env.test")
|
||||||
mailSrv := NewMailService(MailServiceConfig{
|
|
||||||
|
emailService := NewSecure(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,
|
||||||
})
|
})
|
||||||
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) {
|
// Assert that the tls.Config is set up correctly
|
||||||
cfg := newConfig(".env.test")
|
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)
|
||||||
|
|
||||||
mailSrv := NewMailService(MailServiceConfig{
|
t.Run("TestSendEmail", func(t *testing.T) {
|
||||||
Auth: smtp.PlainAuth("", cfg.MailUser, cfg.MailPassword, cfg.MailHost),
|
// Mock the client and test the StartTLS method
|
||||||
Host: cfg.MailHost,
|
var called bool
|
||||||
Port: cfg.MailPort,
|
mockDialFn := func(hostPort string) (SMTPClientIface, error) {
|
||||||
From: cfg.MailFrom,
|
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)
|
||||||
})
|
})
|
||||||
reader, err := os.Open("testdata/attachment1.txt")
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer reader.Close()
|
|
||||||
|
|
||||||
reader2, err := os.Open("testdata/attachment2.txt")
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer reader2.Close()
|
|
||||||
|
|
||||||
reader3, err := os.Open("testdata/attachment3.txt")
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer reader3.Close()
|
|
||||||
|
|
||||||
data := EmailMessage{
|
|
||||||
To: cfg.MailTo,
|
|
||||||
Subject: "Mail Sender",
|
|
||||||
Body: "Hello this is a test email",
|
|
||||||
Attachments: []EmailAttachment{
|
|
||||||
{
|
|
||||||
Title: "attachment1.txt",
|
|
||||||
File: reader,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Title: "attachment2.txt",
|
|
||||||
File: reader2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Title: "attachment3.txt",
|
|
||||||
File: reader3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
err = mailSrv.SendEmail(data)
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWithAttachments(t *testing.T) {
|
|
||||||
msg := newMessage("from", "to", "subject")
|
|
||||||
content, err := msg.withAttachments("body", nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Greater(t, len(content), 0)
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue