From fc005a74c3937437ebfeab60856c2ae228001a92 Mon Sep 17 00:00:00 2001 From: "Urko." Date: Sat, 21 Oct 2023 20:31:31 +0200 Subject: [PATCH] refactor: project structure --- .gitignore | 4 +- Makefile | 13 +++ README.md | 42 ++-------- examples/main.go | 30 +++++++ internal/attachments/attachments.go | 42 ++++++++++ internal/smtpclient/smtpclient.go | 39 +++++++++ email.go => pkg/email/email.go | 82 ++++--------------- email_test.go => pkg/email/email_test.go | 41 +++++++++- pkg/email/message.go | 26 ++++++ .../email/testdata}/attachment1.txt | 0 .../email/testdata}/attachment2.txt | 0 .../email/testdata}/attachment3.txt | 0 12 files changed, 215 insertions(+), 104 deletions(-) create mode 100644 Makefile create mode 100644 examples/main.go create mode 100644 internal/attachments/attachments.go create mode 100644 internal/smtpclient/smtpclient.go rename email.go => pkg/email/email.go (77%) mode change 100755 => 100644 rename email_test.go => pkg/email/email_test.go (76%) create mode 100644 pkg/email/message.go rename {testdata => pkg/email/testdata}/attachment1.txt (100%) rename {testdata => pkg/email/testdata}/attachment2.txt (100%) rename {testdata => pkg/email/testdata}/attachment3.txt (100%) diff --git a/.gitignore b/.gitignore index 6501ee4..8af5452 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ # dotenv environment variables file .env -.env* \ No newline at end of file +.env* + +coverage \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..dea936d --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +COVERAGE_DIR=coverage + +lint: + golangci-lint run ./... +goreportcard: + goreportcard-cli -v +test: + go test ./... +test-coverage: + rm -rf ${COVERAGE_DIR} + mkdir ${COVERAGE_DIR} + go test -v -coverprofile ${COVERAGE_DIR}/cover.out ./... + go tool cover -html ${COVERAGE_DIR}/cover.out -o ${COVERAGE_DIR}/cover.html \ No newline at end of file diff --git a/README.md b/README.md index dff7ef0..426f057 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Mail Sender +# Email Sender ## Description -`mail-sender` is a simple Go library designed to send emails with optional attachments. It's built on top of the standard Go `net/smtp` library with additional support for sending HTML emails and handling multiple attachments. +`email-sender` is a simple Go library designed to send emails with optional attachments. It's built on top of the standard Go `net/smtp` library with additional support for sending HTML emails and handling multiple attachments. ## Features @@ -15,45 +15,13 @@ Clone this repository: -``` -git clone https://gitea.urkob.com/urko/mail-sender.git +```bash +git clone https://gitea.urkob.com/urko/emailsender.git ``` ## Usage -Here's a basic example on how to use the `mail-sender`: - -1. **Initialize the Email Service** - -```go -config := email.MailServiceConfig{ - Auth: smtp.PlainAuth("", "your@email.com", "your-password", "smtp.youremail.com"), - Host: "smtp.youremail.com", - Port: "587", - From: "your@email.com", -} -mailService := email.NewMailService(config) -``` - -2. **Send an Email with an Attachment** - -```go -emailData := email.EmailMessage{ - To: "receiver@email.com", - Subject: "Test Email", - Body: "

Hello!

This is a test email.

", - Attachments: []email.EmailAttachment{ - { - File: attachmentFile, // This is an io.Reader - Title: "document.pdf", - }, - }, -} -err := mailService.SendEmail(emailData) -if err != nil { - log.Fatal(err) -} -``` +Check examples in [examples](https://gitea.urkob.com/urko/emailsender/examples) ## Dependencies diff --git a/examples/main.go b/examples/main.go new file mode 100644 index 0000000..6eab0c2 --- /dev/null +++ b/examples/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "bytes" + "net/smtp" + + "gitea.urkob.com/urko/emailsender/pkg/email" +) + +func main() { + // Here fill with real data + emailService := email.NewMailService(email.MailServiceConfig{ + Auth: smtp.PlainAuth("", "your@email.com", "your-password", "smtp.youremail.com"), + Host: "smtp.youremail.com", + Port: "587", + From: "your@email.com", + }) + + emailService.SendEmail(email.EmailMessage{ + To: "other@email.com", + Subject: "Test Email", + Body: "

Here your body, you can attach as html

", + Attachments: []email.EmailAttachment{ + { + File: bytes.NewBuffer([]byte("test")), // This is an io.Reader + Title: "document.pdf", + }, + }, + }) +} diff --git a/internal/attachments/attachments.go b/internal/attachments/attachments.go new file mode 100644 index 0000000..2df85eb --- /dev/null +++ b/internal/attachments/attachments.go @@ -0,0 +1,42 @@ +package attachments + +import ( + "bytes" + "encoding/base64" + "fmt" + "io" +) + +const delimeter = "**=myohmy689407924327" + +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 +} + +func AttachmentsToBytes(body string, attachments []EmailAttachment) ([]byte, error) { + var message bytes.Buffer + message.WriteString("Content-Type: text/html; charset=\"UTF-8\"\r\n\r\n") + message.WriteString(body + "\r\n\r\n") + for _, attachment := range attachments { + attachmentRawFile, err := attachment.ReadContent() + if err != nil { + return nil, err + } + message.WriteString("--" + delimeter + "\r\n") + message.WriteString("Content-Disposition: attachment; filename=\"" + attachment.Title + "\"\r\n") + message.WriteString("Content-Type: application/octet-stream\r\n") + message.WriteString("Content-Transfer-Encoding: base64\r\n\r\n") + message.WriteString(base64.StdEncoding.EncodeToString(attachmentRawFile) + "\r\n") + } + message.WriteString("--" + delimeter + "--") // End the message + return message.Bytes(), nil +} diff --git a/internal/smtpclient/smtpclient.go b/internal/smtpclient/smtpclient.go new file mode 100644 index 0000000..d740062 --- /dev/null +++ b/internal/smtpclient/smtpclient.go @@ -0,0 +1,39 @@ +package smtpclient + +import ( + "crypto/tls" + "io" + "net/smtp" +) + +type SMTPClient struct { + client smtp.Client +} + +func (c *SMTPClient) Auth(a smtp.Auth) error { + return c.client.Auth(a) +} + +func (c *SMTPClient) Close() error { + return c.client.Close() +} + +func (c *SMTPClient) Data() (io.WriteCloser, error) { + return c.client.Data() +} + +func (c *SMTPClient) Mail(from string) error { + return c.client.Mail(from) +} + +func (c *SMTPClient) Quit() error { + return c.client.Quit() +} + +func (c *SMTPClient) Rcpt(to string) error { + return c.client.Rcpt(to) +} + +func (c *SMTPClient) StartTLS(config *tls.Config) error { + return c.client.StartTLS(config) +} diff --git a/email.go b/pkg/email/email.go old mode 100755 new mode 100644 similarity index 77% rename from email.go rename to pkg/email/email.go index b7752a6..ef461f0 --- a/email.go +++ b/pkg/email/email.go @@ -14,7 +14,7 @@ const ( delimeter = "**=myohmy689407924327" ) -type smtpClient interface { +type SMTPClientIface interface { StartTLS(*tls.Config) error Auth(a smtp.Auth) error Close() error @@ -24,7 +24,7 @@ type smtpClient interface { Rcpt(to string) error } -type SmtpDialFn func(hostPort string) (smtpClient, error) +type SmtpDialFn func(hostPort string) (SMTPClientIface, error) type EmailService struct { auth smtp.Auth @@ -35,69 +35,6 @@ type EmailService struct { dial SmtpDialFn } -type SMTPClient struct { - client smtp.Client -} - -func (c *SMTPClient) Auth(a smtp.Auth) error { - return c.client.Auth(a) -} -func (c *SMTPClient) Close() error { - return c.client.Close() -} -func (c *SMTPClient) Data() (io.WriteCloser, error) { - return c.client.Data() -} -func (c *SMTPClient) Mail(from string) error { - return c.client.Mail(from) -} - -func (c *SMTPClient) Quit() error { - return c.client.Quit() -} -func (c *SMTPClient) Rcpt(to string) error { - return c.client.Rcpt(to) -} - -func (c *SMTPClient) StartTLS(config *tls.Config) error { - return c.client.StartTLS(config) -} - -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 -} - -type MailServiceConfig struct { - Auth smtp.Auth - Host string - Port string - From string // Sender email address -} - -func dial(hostPort string) (smtpClient, error) { - client, err := smtp.Dial(hostPort) - if err != nil { - return nil, err - } - return client, nil -} - func NewMailService(config MailServiceConfig) *EmailService { return &EmailService{ auth: config.Auth, @@ -112,6 +49,21 @@ func NewMailService(config MailServiceConfig) *EmailService { } } +type MailServiceConfig struct { + Auth smtp.Auth + Host string + Port string + From string // Sender email address +} + +func dial(hostPort string) (SMTPClientIface, error) { + client, err := smtp.Dial(hostPort) + if err != nil { + return nil, err + } + return client, nil +} + func (e *EmailService) SendEmail(emailData EmailMessage) error { msg, err := newMessage(e.from, emailData.To, emailData.Subject). withAttachments(emailData.Body, emailData.Attachments) diff --git a/email_test.go b/pkg/email/email_test.go similarity index 76% rename from email_test.go rename to pkg/email/email_test.go index 86e6cf2..0b0ec85 100755 --- a/email_test.go +++ b/pkg/email/email_test.go @@ -74,6 +74,10 @@ func (m *mockSMTP) Rcpt(to string) error { return nil } +func TestNewConfig_MissingEnvFile(t *testing.T) { + assert.Panics(t, func() { newConfig(".missing_env_file") }) +} + func TestMockSendEmail(t *testing.T) { service := &EmailService{ auth: smtp.PlainAuth("", "", "", ""), @@ -84,7 +88,7 @@ func TestMockSendEmail(t *testing.T) { InsecureSkipVerify: true, ServerName: "", }, - dial: func(hostPort string) (smtpClient, error) { + dial: func(hostPort string) (SMTPClientIface, error) { return &mockSMTP{}, nil }, } @@ -119,6 +123,41 @@ func TestSendEmail(t *testing.T) { 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) { + 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") diff --git a/pkg/email/message.go b/pkg/email/message.go new file mode 100644 index 0000000..16bcd08 --- /dev/null +++ b/pkg/email/message.go @@ -0,0 +1,26 @@ +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 +} diff --git a/testdata/attachment1.txt b/pkg/email/testdata/attachment1.txt similarity index 100% rename from testdata/attachment1.txt rename to pkg/email/testdata/attachment1.txt diff --git a/testdata/attachment2.txt b/pkg/email/testdata/attachment2.txt similarity index 100% rename from testdata/attachment2.txt rename to pkg/email/testdata/attachment2.txt diff --git a/testdata/attachment3.txt b/pkg/email/testdata/attachment3.txt similarity index 100% rename from testdata/attachment3.txt rename to pkg/email/testdata/attachment3.txt