refactor: project structure
This commit is contained in:
parent
1bbdbf751b
commit
fc005a74c3
|
@ -1,3 +1,5 @@
|
||||||
# dotenv environment variables file
|
# dotenv environment variables file
|
||||||
.env
|
.env
|
||||||
.env*
|
.env*
|
||||||
|
|
||||||
|
coverage
|
|
@ -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
|
42
README.md
42
README.md
|
@ -1,8 +1,8 @@
|
||||||
# Mail Sender
|
# Email Sender
|
||||||
|
|
||||||
## Description
|
## 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
|
## Features
|
||||||
|
|
||||||
|
@ -15,45 +15,13 @@
|
||||||
|
|
||||||
Clone this repository:
|
Clone this repository:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
git clone https://gitea.urkob.com/urko/mail-sender.git
|
git clone https://gitea.urkob.com/urko/emailsender.git
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Here's a basic example on how to use the `mail-sender`:
|
Check examples in [examples](https://gitea.urkob.com/urko/emailsender/examples)
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ const (
|
||||||
delimeter = "**=myohmy689407924327"
|
delimeter = "**=myohmy689407924327"
|
||||||
)
|
)
|
||||||
|
|
||||||
type smtpClient interface {
|
type SMTPClientIface interface {
|
||||||
StartTLS(*tls.Config) error
|
StartTLS(*tls.Config) error
|
||||||
Auth(a smtp.Auth) error
|
Auth(a smtp.Auth) error
|
||||||
Close() error
|
Close() error
|
||||||
|
@ -24,7 +24,7 @@ type smtpClient interface {
|
||||||
Rcpt(to string) error
|
Rcpt(to string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type SmtpDialFn func(hostPort string) (smtpClient, error)
|
type SmtpDialFn func(hostPort string) (SMTPClientIface, error)
|
||||||
|
|
||||||
type EmailService struct {
|
type EmailService struct {
|
||||||
auth smtp.Auth
|
auth smtp.Auth
|
||||||
|
@ -35,69 +35,6 @@ type EmailService struct {
|
||||||
dial SmtpDialFn
|
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 {
|
func NewMailService(config MailServiceConfig) *EmailService {
|
||||||
return &EmailService{
|
return &EmailService{
|
||||||
auth: config.Auth,
|
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 {
|
func (e *EmailService) SendEmail(emailData EmailMessage) 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)
|
|
@ -74,6 +74,10 @@ func (m *mockSMTP) Rcpt(to string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewConfig_MissingEnvFile(t *testing.T) {
|
||||||
|
assert.Panics(t, func() { newConfig(".missing_env_file") })
|
||||||
|
}
|
||||||
|
|
||||||
func TestMockSendEmail(t *testing.T) {
|
func TestMockSendEmail(t *testing.T) {
|
||||||
service := &EmailService{
|
service := &EmailService{
|
||||||
auth: smtp.PlainAuth("", "", "", ""),
|
auth: smtp.PlainAuth("", "", "", ""),
|
||||||
|
@ -84,7 +88,7 @@ func TestMockSendEmail(t *testing.T) {
|
||||||
InsecureSkipVerify: true,
|
InsecureSkipVerify: true,
|
||||||
ServerName: "",
|
ServerName: "",
|
||||||
},
|
},
|
||||||
dial: func(hostPort string) (smtpClient, error) {
|
dial: func(hostPort string) (SMTPClientIface, error) {
|
||||||
return &mockSMTP{}, nil
|
return &mockSMTP{}, nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -119,6 +123,41 @@ func TestSendEmail(t *testing.T) {
|
||||||
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) {
|
||||||
|
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) {
|
func TestSendEmailWithAttachments(t *testing.T) {
|
||||||
cfg := newConfig(".env.test")
|
cfg := newConfig(".env.test")
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue