Compare commits
14 Commits
1371889d6d
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 537fbeebd9 | |||
| 34f9238cf0 | |||
| e04ed76fee | |||
| b57eb95497 | |||
| 9844ebc65d | |||
| 2028190971 | |||
| 85ca7512c6 | |||
| dca0236b06 | |||
| 5b9abc5af1 | |||
| 2dd66cbaac | |||
| aca3105039 | |||
| 529b7dd153 | |||
| 83ef9c2076 | |||
| 624a269454 |
79
.golangci.yml
Executable file
79
.golangci.yml
Executable file
@@ -0,0 +1,79 @@
|
|||||||
|
run:
|
||||||
|
tests: false
|
||||||
|
timeout: 3m
|
||||||
|
skip-dirs:
|
||||||
|
- cmd/local
|
||||||
|
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- errcheck
|
||||||
|
- gosimple
|
||||||
|
- govet
|
||||||
|
- ineffassign
|
||||||
|
- staticcheck
|
||||||
|
- typecheck
|
||||||
|
- unused
|
||||||
|
- unconvert
|
||||||
|
- gocritic
|
||||||
|
- exportloopref
|
||||||
|
- whitespace
|
||||||
|
- misspell
|
||||||
|
- thelper
|
||||||
|
- revive
|
||||||
|
|
||||||
|
linters-settings:
|
||||||
|
errcheck:
|
||||||
|
exclude-functions:
|
||||||
|
- (*github.com/gin-gonic/gin.Context).AbortWithError
|
||||||
|
- (*github.com/gin-gonic/gin.Context).Error
|
||||||
|
- (github.com/gin-gonic/gin.ResponseWriter).WriteString
|
||||||
|
- (net/http.ResponseWriter).Write
|
||||||
|
- fmt.Fprintf
|
||||||
|
- fmt.Fprintln
|
||||||
|
- (*github.com/jlaffaye/ftp.Response).Close
|
||||||
|
- (*github.com/jlaffaye/ftp.ServerConn).Quit
|
||||||
|
- (golang.org/x/crypto/ssh.Conn).Close
|
||||||
|
- (*github.com/pkg/sftp.File).Close
|
||||||
|
- (*github.com/pkg/sftp.clientConn).Close
|
||||||
|
- (*compress/gzip.Reader).Close
|
||||||
|
- (io.Closer).Close
|
||||||
|
- (*os.File).Close
|
||||||
|
- (io/fs.File).Close
|
||||||
|
- (*github.com/gocraft/work.Enqueuer).Enqueue
|
||||||
|
- (*encoding/xml.Encoder).EncodeToken
|
||||||
|
- (*encoding/xml.Encoder).EncodeElement
|
||||||
|
- (*encoding/xml.Encoder).Flush
|
||||||
|
- (*encoding/xml.Encoder).Encode
|
||||||
|
- (io.Writer).Write
|
||||||
|
- (*encoding/csv.Writer).Write
|
||||||
|
- os.Remove
|
||||||
|
- (*os.File).Seek
|
||||||
|
- (*os.File).WriteString
|
||||||
|
- (*go.uber.org/zap.Logger).Sync
|
||||||
|
- io.Copy
|
||||||
|
|
||||||
|
revive:
|
||||||
|
rules:
|
||||||
|
- name: var-naming
|
||||||
|
severity: error
|
||||||
|
disabled: false
|
||||||
|
gocritic:
|
||||||
|
enabled-tags:
|
||||||
|
- diagnostic
|
||||||
|
- style
|
||||||
|
- performance
|
||||||
|
disabled-checks:
|
||||||
|
- singleCaseSwitch
|
||||||
|
- unnecessaryBlock
|
||||||
|
- unnamedResult
|
||||||
|
- paramTypeCombine
|
||||||
|
- emptyStringTest
|
||||||
|
- regexpSimplify
|
||||||
|
- preferStringWriter
|
||||||
|
- badRegexp
|
||||||
|
- emptyFallthrough
|
||||||
|
- unlabelStmt
|
||||||
|
- nestingReduce
|
||||||
|
- hugeParam
|
||||||
|
# TODO: enable after testing
|
||||||
|
- rangeValCopy
|
||||||
2
Makefile
2
Makefile
@@ -1,7 +1,7 @@
|
|||||||
COVERAGE_DIR=coverage
|
COVERAGE_DIR=coverage
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
golangci-lint run ./...
|
golangci-lint run
|
||||||
goreportcard:
|
goreportcard:
|
||||||
goreportcard-cli -v
|
goreportcard-cli -v
|
||||||
test:
|
test:
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ import (
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Here fill with real data
|
// Here fill with real data
|
||||||
emailService := email.NewMailService(email.MailServiceConfig{
|
emailService := email.NewInsecure(email.SecureConfig{
|
||||||
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",
|
||||||
From: "your@email.com",
|
From: "your@email.com",
|
||||||
})
|
})
|
||||||
|
|
||||||
emailService.SendEmail(email.EmailMessage{
|
emailService.SendEmail(email.MessageWithAttachments{
|
||||||
To: "other@email.com",
|
To: "other@email.com",
|
||||||
Subject: "Test Email",
|
Subject: "Test Email",
|
||||||
Body: "<html><body><p>Here your body, you can attach as html<p/></body></html>",
|
Body: "<html><body><p>Here your body, you can attach as html<p/></body></html>",
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -1,6 +1,6 @@
|
|||||||
module gitea.urkob.com/urko/emailsender
|
module gitea.urkob.com/urko/emailsender
|
||||||
|
|
||||||
go 1.21.1
|
go 1.23.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
|
|||||||
@@ -3,10 +3,14 @@ package email
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -14,6 +18,31 @@ const (
|
|||||||
delimeter = "**=myohmy689407924327"
|
delimeter = "**=myohmy689407924327"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type InsecureConfig struct {
|
||||||
|
Host string
|
||||||
|
Port string
|
||||||
|
From string // Sender email address
|
||||||
|
}
|
||||||
|
|
||||||
|
type SecureConfig struct {
|
||||||
|
Auth smtp.Auth
|
||||||
|
Host string
|
||||||
|
Port string
|
||||||
|
From string // Sender email address
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageWithAttachments struct {
|
||||||
|
To string
|
||||||
|
Subject string
|
||||||
|
Body string
|
||||||
|
Attachments []EmailAttachment
|
||||||
|
}
|
||||||
|
|
||||||
|
type RawMessage struct {
|
||||||
|
To string
|
||||||
|
Body string
|
||||||
|
}
|
||||||
|
|
||||||
type SMTPClientIface interface {
|
type SMTPClientIface interface {
|
||||||
StartTLS(*tls.Config) error
|
StartTLS(*tls.Config) error
|
||||||
Auth(a smtp.Auth) error
|
Auth(a smtp.Auth) error
|
||||||
@@ -35,7 +64,16 @@ type EmailService struct {
|
|||||||
dial SmtpDialFn
|
dial SmtpDialFn
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMailService(config MailServiceConfig) *EmailService {
|
func NewInsecureNoAuth(config InsecureConfig) *EmailService {
|
||||||
|
return &EmailService{
|
||||||
|
host: config.Host,
|
||||||
|
port: config.Port,
|
||||||
|
from: config.From,
|
||||||
|
dial: dial,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInsecure(config SecureConfig) *EmailService {
|
||||||
return &EmailService{
|
return &EmailService{
|
||||||
auth: config.Auth,
|
auth: config.Auth,
|
||||||
host: config.Host,
|
host: config.Host,
|
||||||
@@ -49,11 +87,107 @@ func NewMailService(config MailServiceConfig) *EmailService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type MailServiceConfig struct {
|
var validCommonNames = []string{
|
||||||
Auth smtp.Auth
|
"ISRG Root X1",
|
||||||
Host string
|
"R3",
|
||||||
Port string
|
"R10",
|
||||||
From string // Sender email address
|
"R13",
|
||||||
|
"R11",
|
||||||
|
"E5",
|
||||||
|
"E7",
|
||||||
|
"DST Root CA X3",
|
||||||
|
"DigiCert Global Root G2",
|
||||||
|
"DigiCert Global G2 TLS RSA SHA256 2020 CA1",
|
||||||
|
}
|
||||||
|
|
||||||
|
func customVerify(host string) func(cs tls.ConnectionState) error {
|
||||||
|
return func(cs tls.ConnectionState) error {
|
||||||
|
// Ensure we have at least one peer certificate
|
||||||
|
if len(cs.PeerCertificates) == 0 {
|
||||||
|
return fmt.Errorf("no peer certificates provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// Set up verification options with a DNSName check.
|
||||||
|
// This will perform hostname verification automatically.
|
||||||
|
opts := x509.VerifyOptions{
|
||||||
|
CurrentTime: now,
|
||||||
|
DNSName: host, // assuming config.Host is accessible here
|
||||||
|
Intermediates: x509.NewCertPool(),
|
||||||
|
}
|
||||||
|
// Add all certificates except the leaf as intermediates.
|
||||||
|
for i := 1; i < len(cs.PeerCertificates); i++ {
|
||||||
|
opts.Intermediates.AddCert(cs.PeerCertificates[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the certificate chain (including hostname check via opts.DNSName)
|
||||||
|
if _, err := cs.PeerCertificates[0].Verify(opts); err != nil {
|
||||||
|
return fmt.Errorf("certificate chain verification failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform additional custom checks
|
||||||
|
for _, cert := range cs.PeerCertificates {
|
||||||
|
if now.After(cert.NotAfter) {
|
||||||
|
return fmt.Errorf("certificate expired on %s", cert.NotAfter)
|
||||||
|
}
|
||||||
|
if now.Add(30 * 24 * time.Hour).After(cert.NotAfter) {
|
||||||
|
return fmt.Errorf("certificate will expire soon on %s", cert.NotAfter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the issuer's CommonName is in our allowed list.
|
||||||
|
if !slices.Contains(validCommonNames, cert.Issuer.CommonName) {
|
||||||
|
return fmt.Errorf("untrusted certificate issuer: %s", cert.Issuer.CommonName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the public key algorithms
|
||||||
|
switch cert.PublicKeyAlgorithm {
|
||||||
|
case x509.RSA, x509.ECDSA:
|
||||||
|
// OK
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported public key algorithm: %v",
|
||||||
|
cert.PublicKeyAlgorithm)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSecure(config SecureConfig) *EmailService {
|
||||||
|
return &EmailService{
|
||||||
|
auth: config.Auth,
|
||||||
|
host: config.Host,
|
||||||
|
port: config.Port,
|
||||||
|
from: config.From,
|
||||||
|
tlsconfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
ServerName: config.Host,
|
||||||
|
VerifyConnection: customVerify(config.Host),
|
||||||
|
},
|
||||||
|
dial: dial,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSecure465(config SecureConfig) *EmailService {
|
||||||
|
tlsCfg := tls.Config{
|
||||||
|
// Ideally, InsecureSkipVerify: false,
|
||||||
|
// or do a proper certificate validation
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
ServerName: config.Host,
|
||||||
|
VerifyConnection: customVerify(config.Host),
|
||||||
|
}
|
||||||
|
return &EmailService{
|
||||||
|
auth: config.Auth,
|
||||||
|
host: config.Host,
|
||||||
|
port: config.Port,
|
||||||
|
from: config.From,
|
||||||
|
tlsconfig: &tlsCfg,
|
||||||
|
dial: func(hostPort string) (SMTPClientIface, error) {
|
||||||
|
return dialTLS(hostPort, &tlsCfg)
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func dial(hostPort string) (SMTPClientIface, error) {
|
func dial(hostPort string) (SMTPClientIface, error) {
|
||||||
@@ -64,7 +198,26 @@ func dial(hostPort string) (SMTPClientIface, error) {
|
|||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *EmailService) SendEmail(emailData EmailMessage) error {
|
func dialTLS(hostPort string, tlsConfig *tls.Config) (SMTPClientIface, error) {
|
||||||
|
// 1) Create a raw TCP connection
|
||||||
|
conn, err := net.Dial("tcp", hostPort)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Wrap it with TLS
|
||||||
|
tlsConn := tls.Client(conn, tlsConfig)
|
||||||
|
|
||||||
|
// 3) Now create the SMTP client on this TLS connection
|
||||||
|
host, _, _ := net.SplitHostPort(hostPort)
|
||||||
|
c, err := smtp.NewClient(tlsConn, host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EmailService) SendEmail(emailData MessageWithAttachments) error {
|
||||||
msg, err := newMessage(e.from, emailData.To, emailData.Subject).
|
msg, err := newMessage(e.from, emailData.To, emailData.Subject).
|
||||||
withAttachments(emailData.Body, emailData.Attachments)
|
withAttachments(emailData.Body, emailData.Attachments)
|
||||||
|
|
||||||
@@ -72,8 +225,22 @@ func (e *EmailService) SendEmail(emailData EmailMessage) error {
|
|||||||
return fmt.Errorf("error while preparing email: %w", err)
|
return fmt.Errorf("error while preparing email: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch e.port {
|
||||||
|
case "465":
|
||||||
|
return e.sendTLS(emailData.To, msg)
|
||||||
|
default:
|
||||||
return e.send(emailData.To, msg)
|
return e.send(emailData.To, msg)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EmailService) SendRaw(emailData RawMessage) error {
|
||||||
|
switch e.port {
|
||||||
|
case "465":
|
||||||
|
return e.sendTLS(emailData.To, []byte(emailData.Body))
|
||||||
|
default:
|
||||||
|
return e.send(emailData.To, []byte(emailData.Body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (e *EmailService) send(to string, msg []byte) error {
|
func (e *EmailService) send(to string, msg []byte) error {
|
||||||
c, err := e.dial(e.host + ":" + e.port)
|
c, err := e.dial(e.host + ":" + e.port)
|
||||||
@@ -81,9 +248,59 @@ func (e *EmailService) send(to string, msg []byte) error {
|
|||||||
return fmt.Errorf("DIAL: %s", err)
|
return fmt.Errorf("DIAL: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if e.tlsconfig != nil {
|
||||||
if err = c.StartTLS(e.tlsconfig); err != nil {
|
if err = c.StartTLS(e.tlsconfig); err != nil {
|
||||||
return fmt.Errorf("c.StartTLS: %s", err)
|
return fmt.Errorf("c.StartTLS: %s", err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth
|
||||||
|
if e.auth != nil {
|
||||||
|
if err = c.Auth(e.auth); err != nil {
|
||||||
|
return fmt.Errorf("c.Auth: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// To && From
|
||||||
|
if err = c.Mail(e.from); err != nil {
|
||||||
|
return fmt.Errorf("c.Mail: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = c.Rcpt(to); err != nil {
|
||||||
|
return fmt.Errorf("c.Rcpt: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data
|
||||||
|
w, err := c.Data()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("c.Data: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
written, err := w.Write(msg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("w.Write: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if written <= 0 {
|
||||||
|
return fmt.Errorf("%d bytes written", written)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = w.Close(); err != nil {
|
||||||
|
return fmt.Errorf("w.Close: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = c.Quit(); err != nil {
|
||||||
|
return fmt.Errorf("w.Quit: %s", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EmailService) sendTLS(to string, msg []byte) error {
|
||||||
|
c, err := e.dial(e.host + ":" + e.port) // dialTLS
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("DIAL: %s", err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
if err = c.Auth(e.auth); err != nil {
|
if err = c.Auth(e.auth); err != nil {
|
||||||
@@ -121,6 +338,7 @@ func (e *EmailService) send(to string, msg []byte) error {
|
|||||||
if err = c.Quit(); err != nil {
|
if err = c.Quit(); err != nil {
|
||||||
return fmt.Errorf("w.Quit: %s", err)
|
return fmt.Errorf("w.Quit: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,3 +388,16 @@ func (m message) withAttachments(body string, attachments []EmailAttachment) ([]
|
|||||||
message.WriteString("--" + delimeter + "--") // End the message
|
message.WriteString("--" + delimeter + "--") // End the message
|
||||||
return message.Bytes(), nil
|
return message.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EmailAttachment struct {
|
||||||
|
File io.Reader
|
||||||
|
Title string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e EmailAttachment) ReadContent() ([]byte, error) {
|
||||||
|
bts, err := io.ReadAll(e.File)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error loading attachment: %s", err)
|
||||||
|
}
|
||||||
|
return bts, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func TestNewConfig_MissingEnvFile(t *testing.T) {
|
|||||||
func TestMockSendEmail(t *testing.T) {
|
func TestMockSendEmail(t *testing.T) {
|
||||||
service := NewMockMailService(func(params ...interface{}) {})
|
service := NewMockMailService(func(params ...interface{}) {})
|
||||||
|
|
||||||
emailData := EmailMessage{
|
emailData := MessageWithAttachments{
|
||||||
To: "test@example.com",
|
To: "test@example.com",
|
||||||
Subject: "Test Email",
|
Subject: "Test Email",
|
||||||
Body: "This is a test email.",
|
Body: "This is a test email.",
|
||||||
@@ -57,68 +57,26 @@ 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(SecureConfig{
|
||||||
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) {
|
||||||
|
data := MessageWithAttachments{
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSendEmail_InvalidRecipient(t *testing.T) {
|
t.Run("TestSendEmailWithAttachments", func(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()
|
||||||
@@ -131,7 +89,7 @@ func TestSendEmailWithAttachments(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer reader3.Close()
|
defer reader3.Close()
|
||||||
|
|
||||||
data := EmailMessage{
|
data := MessageWithAttachments{
|
||||||
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",
|
||||||
@@ -152,11 +110,75 @@ func TestSendEmailWithAttachments(t *testing.T) {
|
|||||||
}
|
}
|
||||||
err = mailSrv.SendEmail(data)
|
err = mailSrv.SendEmail(data)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
})
|
||||||
|
|
||||||
func TestWithAttachments(t *testing.T) {
|
t.Run("TestWithAttachments", func(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 := MessageWithAttachments{
|
||||||
|
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(SecureConfig{
|
||||||
|
Auth: smtp.PlainAuth("", "wronguser", "wrongpassword", cfg.MailHost),
|
||||||
|
Host: cfg.MailHost,
|
||||||
|
Port: cfg.MailPort,
|
||||||
|
From: cfg.MailFrom,
|
||||||
|
})
|
||||||
|
data := MessageWithAttachments{
|
||||||
|
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(SecureConfig{
|
||||||
|
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 := MessageWithAttachments{
|
||||||
|
To: cfg.MailTo,
|
||||||
|
Subject: "Mail Sender",
|
||||||
|
Body: "Hello this is a test email",
|
||||||
|
}
|
||||||
|
require.NoError(t, emailService.SendEmail(data))
|
||||||
|
assert.Equal(t, true, called)
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
package email
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
type EmailMessage struct {
|
|
||||||
To string
|
|
||||||
Subject string
|
|
||||||
Body string
|
|
||||||
Attachments []EmailAttachment
|
|
||||||
}
|
|
||||||
|
|
||||||
type EmailAttachment struct {
|
|
||||||
File io.Reader
|
|
||||||
Title string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e EmailAttachment) ReadContent() ([]byte, error) {
|
|
||||||
bts, err := io.ReadAll(e.File)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error loading attachment: %s", err)
|
|
||||||
}
|
|
||||||
return bts, nil
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user