diff --git a/examples/main.go b/examples/main.go index 6eab0c2..38ae0b2 100644 --- a/examples/main.go +++ b/examples/main.go @@ -9,7 +9,7 @@ import ( func main() { // 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"), Host: "smtp.youremail.com", Port: "587", diff --git a/pkg/email/email.go b/pkg/email/email.go index ef461f0..07ebb26 100644 --- a/pkg/email/email.go +++ b/pkg/email/email.go @@ -3,10 +3,13 @@ package email import ( "bytes" "crypto/tls" + "crypto/x509" "encoding/base64" "fmt" "io" "net/smtp" + "slices" + "time" ) const ( @@ -35,7 +38,7 @@ type EmailService struct { dial SmtpDialFn } -func NewMailService(config MailServiceConfig) *EmailService { +func NewInsecure(config MailServiceConfig) *EmailService { return &EmailService{ auth: config.Auth, 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 { Auth smtp.Auth Host string diff --git a/pkg/email/email_test.go b/pkg/email/email_test.go index f70ed58..480bbb9 100755 --- a/pkg/email/email_test.go +++ b/pkg/email/email_test.go @@ -57,106 +57,92 @@ func TestMockSendEmail(t *testing.T) { } } -func TestSendEmail(t *testing.T) { +func TestNewInsecure(t *testing.T) { cfg := newConfig(".env.test") - mailSrv := NewMailService(MailServiceConfig{ + mailSrv := NewInsecure(MailServiceConfig{ Auth: smtp.PlainAuth("", cfg.MailUser, cfg.MailPassword, cfg.MailHost), Host: cfg.MailHost, Port: cfg.MailPort, From: cfg.MailFrom, }) - data := EmailMessage{ - To: cfg.MailTo, - Subject: "Mail Sender", - Body: "Hello this is a test email", - } - 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, + t.Run("TestSendEmail", func(t *testing.T) { + data := EmailMessage{ + To: cfg.MailTo, + Subject: "Mail Sender", + Body: "Hello this is a test email", + } + require.NoError(t, mailSrv.SendEmail(data)) }) - 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, + t.Run("TestSendEmailWithAttachments", func(t *testing.T) { + 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) }) - 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, + 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) }) - 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() + 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) + }) - 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) + 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) + }) }