refactor: project structure

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

172
pkg/email/email.go Normal file
View File

@@ -0,0 +1,172 @@
package email
import (
"bytes"
"crypto/tls"
"encoding/base64"
"fmt"
"io"
"net/smtp"
)
const (
mime = "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n"
delimeter = "**=myohmy689407924327"
)
type SMTPClientIface interface {
StartTLS(*tls.Config) error
Auth(a smtp.Auth) error
Close() error
Data() (io.WriteCloser, error)
Mail(from string) error
Quit() error
Rcpt(to string) error
}
type SmtpDialFn func(hostPort string) (SMTPClientIface, error)
type EmailService struct {
auth smtp.Auth
host string
port string
from string
tlsconfig *tls.Config
dial SmtpDialFn
}
func NewMailService(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,
},
dial: dial,
}
}
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)
if err != nil {
return fmt.Errorf("error while preparing email: %w", err)
}
return e.send(emailData.To, msg)
}
func (e *EmailService) send(to string, msg []byte) error {
c, err := e.dial(e.host + ":" + e.port)
if err != nil {
return fmt.Errorf("DIAL: %s", err)
}
if err = c.StartTLS(e.tlsconfig); err != nil {
return fmt.Errorf("c.StartTLS: %s", err)
}
// Auth
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
}
type message struct {
from string
to string
subject string
}
func newMessage(from, to, subject string) message {
return message{from: from, to: to, subject: subject}
}
func (m message) withAttachments(body string, attachments []EmailAttachment) ([]byte, error) {
headers := make(map[string]string)
headers["From"] = m.from
headers["To"] = m.to
headers["Subject"] = m.subject
headers["MIME-Version"] = "1.0"
var message bytes.Buffer
for k, v := range headers {
message.WriteString(k)
message.WriteString(": ")
message.WriteString(v)
message.WriteString("\r\n")
}
message.WriteString("Content-Type: " + fmt.Sprintf("multipart/mixed; boundary=\"%s\"\r\n", delimeter))
message.WriteString("--" + delimeter + "\r\n")
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
}

210
pkg/email/email_test.go Executable file
View File

@@ -0,0 +1,210 @@
package email
import (
"crypto/tls"
"fmt"
"io"
"net/smtp"
"os"
"testing"
"github.com/joho/godotenv"
"github.com/kelseyhightower/envconfig"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type config struct {
MailUser string `required:"false" split_words:"true"`
MailPassword string `required:"false" split_words:"true"`
MailHost string `required:"false" split_words:"true"`
MailPort string `required:"false" split_words:"true"`
MailFrom string `required:"false" split_words:"true"`
MailTo string `required:"false" split_words:"true"`
}
func newConfig(envFile string) *config {
if envFile != "" {
err := godotenv.Load(envFile)
if err != nil {
panic(fmt.Errorf("godotenv.Load: %w", err))
}
}
cfg := &config{}
err := envconfig.Process("", cfg)
if err != nil {
panic(fmt.Errorf("envconfig.Process: %w", err))
}
return cfg
}
type mockWriter struct{}
func (w *mockWriter) Close() error {
return nil
}
func (w *mockWriter) Write(p []byte) (n int, err error) {
return 10, nil
}
// Mock SMTP Client
type mockSMTP struct{}
func (m *mockSMTP) StartTLS(*tls.Config) error {
return nil
}
func (m *mockSMTP) Auth(a smtp.Auth) error {
return nil
}
func (m *mockSMTP) Close() error {
return nil
}
func (m *mockSMTP) Data() (io.WriteCloser, error) {
return &mockWriter{}, nil
}
func (m *mockSMTP) Mail(from string) error {
return nil
}
func (m *mockSMTP) Quit() error {
return nil
}
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("", "", "", ""),
host: "",
port: "",
from: "",
tlsconfig: &tls.Config{
InsecureSkipVerify: true,
ServerName: "",
},
dial: func(hostPort string) (SMTPClientIface, error) {
return &mockSMTP{}, nil
},
}
emailData := EmailMessage{
To: "test@example.com",
Subject: "Test Email",
Body: "This is a test email.",
}
err := service.SendEmail(emailData)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
}
func TestSendEmail(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: 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,
})
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")
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")
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)
}

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
}

1
pkg/email/testdata/attachment1.txt vendored Executable file
View File

@@ -0,0 +1 @@
this is txt 1 attachment

1
pkg/email/testdata/attachment2.txt vendored Executable file
View File

@@ -0,0 +1 @@
this is txt 2 attachment

1
pkg/email/testdata/attachment3.txt vendored Executable file
View File

@@ -0,0 +1 @@
this is txt 3 attachment