refactor: project structure

This commit is contained in:
Urko. 2023-10-21 20:31:31 +02:00
parent 1bbdbf751b
commit fc005a74c3
12 changed files with 215 additions and 104 deletions

4
.gitignore vendored
View File

@ -1,3 +1,5 @@
# dotenv environment variables file
.env
.env*
.env*
coverage

13
Makefile Normal file
View File

@ -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

View File

@ -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: "<h1>Hello!</h1><p>This is a test email.</p>",
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

30
examples/main.go Normal file
View File

@ -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: "<html><body><p>Here your body, you can attach as html<p/></body></html>",
Attachments: []email.EmailAttachment{
{
File: bytes.NewBuffer([]byte("test")), // This is an io.Reader
Title: "document.pdf",
},
},
})
}

View File

@ -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
}

View File

@ -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)
}

82
email.go → pkg/email/email.go Executable file → Normal file
View File

@ -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)

View File

@ -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")

26
pkg/email/message.go Normal file
View File

@ -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
}