Skip to content
This repository has been archived by the owner on Aug 16, 2022. It is now read-only.

Commit

Permalink
feat: create mailer (#63)
Browse files Browse the repository at this point in the history
* feat: create mailer (go-mailer is used)

* fix linter

* sendGrid implementation

* refactor: separate smtp and sendgrid

* refactor: add sender name to config

* fix newWithSMTP wrong parameter

* mixed email content
break down MailerConfig struct

* resolve notes

* resolve notes
testing

* testing

* test cases

* test cases

* Merge branch 'feat/authentication-system' of https://github.com/reearth/reearth-backend into auth/mailer

# Conflicts:
#	go.mod
#	internal/app/config.go

* Merge branch 'feat/authentication-system' of https://github.com/reearth/reearth-backend into auth/mailer

# Conflicts:
#	go.mod

* swap plain and html contents

* switch to alternative

* remove unused functions

* validate email and smtp url
  • Loading branch information
mimoham24 committed Nov 25, 2021
1 parent 58aeb13 commit 2632fe9
Show file tree
Hide file tree
Showing 10 changed files with 376 additions and 1 deletion.
4 changes: 4 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ services:
environment:
REEARTH_ENV: docker
REEARTH_DB_URL: mongodb://reearth-mongo
REEARTH_MAILER: smtp
REEARTH_SMTP_URL: #add later
REEARTH_SMTP_USER: #add later
REEARTH_SMTP_PASSWORD: #add later
depends_on:
- reearth-mongo
reearth-mongo:
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ require (
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/paulmach/go.geojson v1.4.0
github.com/pkg/errors v0.9.1
github.com/sendgrid/sendgrid-go v3.10.3+incompatible
github.com/sirupsen/logrus v1.8.1
github.com/smartystreets/assertions v1.1.1 // indirect
github.com/spf13/afero v1.6.0
Expand Down Expand Up @@ -91,6 +92,7 @@ require (
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sendgrid/rest v2.6.5+incompatible // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,10 @@ github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sendgrid/rest v2.6.5+incompatible h1:MZsDqRdwKTHXNABhVgiZFLgVDN698H4QtFrTX3WlrN0=
github.com/sendgrid/rest v2.6.5+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
github.com/sendgrid/sendgrid-go v3.10.3+incompatible h1:p+gXRxF51RJTBlMUpVuWopM+UkLoZQ5ETyR2KLrH4zw=
github.com/sendgrid/sendgrid-go v3.10.3+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
Expand Down
17 changes: 17 additions & 0 deletions internal/app/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ type Config struct {
DB string `default:"mongodb://localhost"`
Auth0 Auth0Config
Auth AuthConfig
Mailer string
SMTP SMTPConfig
SendGrid SendGridConfig
GraphQL GraphQLConfig
Published PublishedConfig
GCPProject string `envconfig:"GOOGLE_CLOUD_PROJECT"`
Expand Down Expand Up @@ -70,6 +73,20 @@ type GCSConfig struct {
PublicationCacheControl string
}

type SendGridConfig struct {
Email string
Name string
API string
}

type SMTPConfig struct {
Host string
Port string
SMTPUsername string
Email string
Password string
}

func ReadConfig(debug bool) (*Config, error) {
// load .env
if err := godotenv.Load(".env"); err != nil && !os.IsNotExist(err) {
Expand Down
14 changes: 14 additions & 0 deletions internal/app/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"
"time"

"github.com/reearth/reearth-backend/internal/infrastructure/mailer"

"github.com/reearth/reearth-backend/internal/infrastructure/github"
"github.com/reearth/reearth-backend/internal/infrastructure/google"
"github.com/spf13/afero"
Expand Down Expand Up @@ -71,10 +73,22 @@ func initReposAndGateways(ctx context.Context, conf *Config, debug bool) (*repo.
// google
gateways.Google = google.NewGoogle()

// SMTP Mailer
gateways.Mailer = initMailer(conf)

// release lock of all scenes
if err := repos.SceneLock.ReleaseAllLock(context.Background()); err != nil {
log.Fatalln(fmt.Sprintf("repo initialization error: %+v", err))
}

return repos, gateways
}

func initMailer(conf *Config) gateway.Mailer {
if conf.Mailer == "sendgrid" {
return mailer.NewWithSendGrid(conf.SendGrid.Name, conf.SendGrid.Email, conf.SendGrid.API)
} else if conf.Mailer == "smtp" {
return mailer.NewWithSMTP(conf.SMTP.Host, conf.SMTP.Port, conf.SMTP.SMTPUsername, conf.SMTP.Password)
}
return nil
}
32 changes: 32 additions & 0 deletions internal/infrastructure/mailer/sendgrid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package mailer

import (
"github.com/reearth/reearth-backend/internal/usecase/gateway"
"github.com/sendgrid/sendgrid-go"
"github.com/sendgrid/sendgrid-go/helpers/mail"
)

type sendgridMailer struct {
api string
// sender data
name string
email string
}

func NewWithSendGrid(senderName, senderEmail, api string) gateway.Mailer {
return &sendgridMailer{
name: senderName,
email: senderEmail,
api: api,
}
}

func (m *sendgridMailer) SendMail(to []gateway.Contact, subject, plainContent, htmlContent string) error {
contact := to[0]
sender := mail.NewEmail(m.name, m.email)
receiver := mail.NewEmail(contact.Name, contact.Email)
message := mail.NewSingleEmail(sender, subject, receiver, plainContent, htmlContent)
client := sendgrid.NewSendClient(m.api)
_, err := client.Send(message)
return err
}
41 changes: 41 additions & 0 deletions internal/infrastructure/mailer/sendgrid_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package mailer

import (
"testing"

"github.com/reearth/reearth-backend/internal/usecase/gateway"
"github.com/stretchr/testify/assert"
)

func TestNewWithSendGrid(t *testing.T) {
type args struct {
senderName string
senderEmail string
api string
}
tests := []struct {
name string
args args
want gateway.Mailer
}{
{
name: "should create a sendGrid mailer",
args: args{
senderName: "test sender",
senderEmail: "sender@test.com",
api: "TEST_API",
},
want: &sendgridMailer{
api: "TEST_API",
name: "test sender",
email: "sender@test.com",
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(tt *testing.T) {
got := NewWithSendGrid(tc.args.senderName, tc.args.senderEmail, tc.args.api)
assert.Equal(tt, tc.want, got)
})
}
}
116 changes: 116 additions & 0 deletions internal/infrastructure/mailer/smtp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package mailer

import (
"bytes"
"errors"
"fmt"
"io"
"mime/multipart"
"net/mail"
"net/smtp"
"net/textproto"
"strings"

"github.com/reearth/reearth-backend/internal/usecase/gateway"
)

type smtpMailer struct {
host string
port string
username string
password string
}

type message struct {
to []string
subject string
plainContent string
htmlContent string
}

func (m *message) encodeContent() (string, error) {
buf := bytes.NewBuffer(nil)
writer := multipart.NewWriter(buf)
boundary := writer.Boundary()

altBuffer, err := writer.CreatePart(textproto.MIMEHeader{"Content-Type": {"multipart/alternative; boundary=" + boundary}})
if err != nil {
return "", err
}
altWriter := multipart.NewWriter(altBuffer)
err = altWriter.SetBoundary(boundary)
if err != nil {
return "", err
}
var content io.Writer
content, err = altWriter.CreatePart(textproto.MIMEHeader{"Content-Type": {"text/plain"}})
if err != nil {
return "", err
}

_, err = content.Write([]byte(m.plainContent + "\r\n\r\n"))
if err != nil {
return "", err
}
content, err = altWriter.CreatePart(textproto.MIMEHeader{"Content-Type": {"text/html"}})
if err != nil {
return "", err
}
_, err = content.Write([]byte(m.htmlContent + "\r\n"))
if err != nil {
return "", err
}
_ = altWriter.Close()
return buf.String(), nil
}

func (m *message) encodeMessage() ([]byte, error) {
buf := bytes.NewBuffer(nil)
buf.WriteString(fmt.Sprintf("Subject: %s\n", m.subject))
buf.WriteString(fmt.Sprintf("To: %s\n", strings.Join(m.to, ",")))
content, err := m.encodeContent()
if err != nil {
return nil, err
}
buf.WriteString(content)

return buf.Bytes(), nil
}

func NewWithSMTP(host, port, username, password string) gateway.Mailer {
return &smtpMailer{
host: host,
port: port,
username: username,
password: password,
}
}

func (m *smtpMailer) SendMail(to []gateway.Contact, subject, plainContent, htmlContent string) error {
emails := make([]string, 0, len(to))
for _, c := range to {
_, err := mail.ParseAddress(c.Email)
if err != nil {
return fmt.Errorf("invalid email %s", c.Email)
}
emails = append(emails, c.Email)
}

msg := &message{
to: emails,
subject: subject,
plainContent: plainContent,
htmlContent: htmlContent,
}

encodedMsg, err := msg.encodeMessage()
if err != nil {
return err
}

auth := smtp.PlainAuth("", m.username, m.password, m.host)
if len(m.host) == 0 {
return errors.New("invalid smtp url")
}
return smtp.SendMail(m.host+":"+m.port, auth, m.username, emails, encodedMsg)
}
Loading

0 comments on commit 2632fe9

Please sign in to comment.