├── .circleci └── config.yml ├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── emails └── welcome_mail │ └── welcome_mail.go ├── go.mod ├── go.sum ├── handlers └── welcome │ ├── welcome.go │ └── welcome_test.go ├── helpers └── helpers.go ├── main.go └── templates └── welcome_mail.html /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 # use CircleCI 2.0 2 | jobs: # basic units of work in a run 3 | build: # runs not using Workflows must have a `build` job as entry point 4 | docker: # run the steps with Docker 5 | - image: circleci/golang:1.14 6 | 7 | environment: # environment variables for the build itself 8 | GO111MODULE: "on" #we don't rely on GOPATH 9 | 10 | working_directory: ~/usr/src/app # Go module is used, so we dont need to worry about GOPATH 11 | 12 | steps: # steps that comprise the `build` job 13 | - checkout # check out source code to working directory 14 | - run: 15 | name: "Fetch dependencies" 16 | command: go mod download 17 | 18 | - run: 19 | name: Run unit tests 20 | command: go test -v ./... # our test is inside the "tests" folder, so target only that 21 | 22 | workflows: 23 | version: 2 24 | build-workflow: 25 | jobs: 26 | - build 27 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_PORT=8080 2 | 3 | GMAIL_USERNAME=ur-gmail 4 | GMAIL_PASSWORD=ur-password 5 | GMAIL_SERVER=smtp.gmail.com 6 | GMAIL_PORT=587 7 | 8 | 9 | SENDGRID_API_KEY=ur-sendgrid-api-key 10 | SENDGRID_FROM=ur-sendgrid-email 11 | 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Steven Victor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [](https://circleci.com/gh/victorsteven/sending-and-testing-emails-in-Go) 2 | 3 | A sample repository to understanding sending and testing emails. 4 | 5 | Rename the .env.example file to .env 6 | Then update with your credentials. 7 | 8 | Read the blog post about this respository [here](https://medium.com/@victorsteven/sending-and-faking-emails-with-custom-templates-in-golang-15a017c191ec) 9 | 10 | -------------------------------------------------------------------------------- /emails/welcome_mail/welcome_mail.go: -------------------------------------------------------------------------------- 1 | package welcome_mail 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/sendgrid/sendgrid-go" 7 | "github.com/sendgrid/sendgrid-go/helpers/mail" 8 | "html/template" 9 | "mail-sending/helpers" 10 | "net/smtp" 11 | "os" 12 | "strings" 13 | ) 14 | 15 | type service struct{} 16 | 17 | type Service interface { 18 | SendWelcomeMail(*helpers.WelcomeMail) (bool, error) 19 | } 20 | 21 | func NewService() *service { 22 | return &service{} 23 | } 24 | 25 | //The service struct implements the Service interface 26 | var _ Service = &service{} 27 | 28 | func (s *service) SendWelcomeMail(cred *helpers.WelcomeMail) (bool, error) { 29 | 30 | em := &helpers.WelcomeMail{ 31 | Temp: "welcome_mail.html", 32 | Name: cred.Name, 33 | Email: cred.Email, 34 | Subject: "Welcome Onboard", 35 | } 36 | 37 | //Just to demonstrate how you can use multiple mail services 38 | if strings.Contains(em.Email, "@yahoo") { 39 | _, err := s.sendEmailUsingGmail(em) 40 | if err != nil { 41 | return false, err 42 | } 43 | } else { 44 | _, err := s.sendEmailUsingSendGrid(em) 45 | if err != nil { 46 | return false, err 47 | } 48 | } 49 | 50 | return true, nil 51 | 52 | } 53 | 54 | func (s *service) sendEmailUsingGmail(m *helpers.WelcomeMail) (bool, error) { 55 | 56 | gmailUsername := os.Getenv("GMAIL_USERNAME") 57 | gmailPassword := os.Getenv("GMAIL_PASSWORD") 58 | gmailServer := os.Getenv("GMAIL_SERVER") 59 | gmailPort := os.Getenv("GMAIL_PORT") 60 | 61 | smtpData := &helpers.WelcomeMail{ 62 | Name: m.Name, 63 | Email: m.Email, 64 | } 65 | 66 | auth := smtp.PlainAuth("", 67 | gmailUsername, 68 | gmailPassword, 69 | gmailServer, 70 | ) 71 | 72 | dir, _ := os.Getwd() 73 | file := fmt.Sprintf("%s/templates/%s", dir, m.Temp) 74 | 75 | t, err := template.ParseFiles(file) 76 | if err != nil { 77 | return false, err 78 | } 79 | 80 | mime := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n" 81 | 82 | var doc bytes.Buffer 83 | 84 | doc.Write([]byte(fmt.Sprintf("Subject:"+m.Subject+"\n%s\n\n", mime))) 85 | 86 | err = t.Execute(&doc, smtpData) 87 | if err != nil { 88 | return false, err 89 | } 90 | 91 | emails := []string{m.Email} //or 92 | 93 | err = smtp.SendMail(gmailServer+":"+gmailPort, 94 | auth, 95 | gmailUsername, 96 | emails, 97 | doc.Bytes()) 98 | 99 | if err != nil { 100 | return false, err 101 | } 102 | 103 | return true, nil 104 | 105 | } 106 | 107 | func (s *service) sendEmailUsingSendGrid(m *helpers.WelcomeMail) (bool, error) { 108 | 109 | fromAdmin := os.Getenv("SENDGRID_FROM") 110 | apiKey := os.Getenv("SENDGRID_API_KEY") 111 | 112 | from := mail.NewEmail("Wonderful Company", fromAdmin) 113 | subject := m.Subject 114 | to := mail.NewEmail("Client", m.Email) 115 | 116 | dir, _ := os.Getwd() 117 | file := fmt.Sprintf("%s/templates/%s", dir, m.Temp) 118 | 119 | t, err := template.ParseFiles(file) 120 | if err != nil { 121 | return false, err 122 | } 123 | 124 | smtpData := &helpers.WelcomeMail{ 125 | Name: m.Name, 126 | Email: m.Email, 127 | } 128 | 129 | var doc bytes.Buffer 130 | 131 | err = t.Execute(&doc, smtpData) 132 | if err != nil { 133 | return false, err 134 | } 135 | 136 | message := mail.NewSingleEmail(from, subject, to, doc.String(), doc.String()) 137 | client := sendgrid.NewSendClient(apiKey) 138 | _, err = client.Send(message) 139 | if err != nil { 140 | return false, err 141 | } 142 | 143 | return true, nil 144 | } 145 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module mail-sending 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.6.3 7 | github.com/go-playground/validator/v10 v10.2.0 8 | github.com/joho/godotenv v1.3.0 9 | github.com/sendgrid/rest v2.6.1+incompatible // indirect 10 | github.com/sendgrid/sendgrid-go v3.6.3+incompatible 11 | github.com/stretchr/testify v1.6.1 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 6 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 7 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= 8 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 9 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 10 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 11 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 12 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 13 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 14 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 15 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= 16 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 17 | github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= 18 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 19 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 20 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 21 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 22 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 23 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 24 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 25 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 26 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 27 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 28 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 29 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 30 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 31 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 32 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 33 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 34 | github.com/sendgrid/rest v2.6.1+incompatible h1:8DyG9t24pTGYb9D7PsyCHlLsqAm4rUbSel0GQtNpN3Y= 35 | github.com/sendgrid/rest v2.6.1+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= 36 | github.com/sendgrid/sendgrid-go v3.6.3+incompatible h1:N0+lZtQOkUcLYQaIJeOnBnXEFV73BSd1++ALIIYZ7gE= 37 | github.com/sendgrid/sendgrid-go v3.6.3+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= 38 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 39 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 40 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 41 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 42 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 43 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 44 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 45 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 46 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 47 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= 48 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 49 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 50 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 51 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 52 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 53 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 54 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 55 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 56 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 57 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 58 | -------------------------------------------------------------------------------- /handlers/welcome/welcome.go: -------------------------------------------------------------------------------- 1 | package welcome 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "mail-sending/emails/welcome_mail" 6 | "mail-sending/helpers" 7 | "net/http" 8 | ) 9 | 10 | type service struct { 11 | email welcome_mail.Service 12 | } 13 | 14 | func NewWelcome(email welcome_mail.Service) *service { 15 | return &service{email} 16 | } 17 | 18 | func (h *service) WelcomeMail(c *gin.Context) { 19 | 20 | welcome := &helpers.WelcomeModel{} 21 | 22 | if err := c.ShouldBindJSON(&welcome); err != nil { 23 | c.JSON(http.StatusUnprocessableEntity, gin.H{ 24 | "status": http.StatusUnprocessableEntity, 25 | "body": "Please provide valid inputs", 26 | }) 27 | return 28 | } 29 | 30 | if err := helpers.ValidateInputs(*welcome); err != nil { 31 | c.JSON(http.StatusBadRequest, gin.H{ 32 | "status": http.StatusBadRequest, 33 | "body": err.Error(), 34 | }) 35 | return 36 | } 37 | 38 | cred := &helpers.WelcomeMail{ 39 | Name: welcome.Name, 40 | Email: welcome.Email, 41 | } 42 | 43 | _, err := h.email.SendWelcomeMail(cred) 44 | if err != nil { 45 | c.JSON(http.StatusUnprocessableEntity, gin.H{ 46 | "status": http.StatusUnprocessableEntity, 47 | "body": err.Error(), 48 | }) 49 | return 50 | } 51 | 52 | c.JSON(http.StatusOK, gin.H{ 53 | "status": http.StatusOK, 54 | "body": "Please check your mail", 55 | }) 56 | return 57 | 58 | } 59 | -------------------------------------------------------------------------------- /handlers/welcome/welcome_test.go: -------------------------------------------------------------------------------- 1 | package welcome_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "github.com/gin-gonic/gin" 8 | "github.com/stretchr/testify/assert" 9 | "mail-sending/handlers/welcome" 10 | "mail-sending/helpers" 11 | "net/http" 12 | "net/http/httptest" 13 | "testing" 14 | ) 15 | 16 | //SendWelcomeMailService is a mock Mail Service Interface 17 | type fakeWelcomeMailService struct { 18 | SendWelcomeMailFn func(cred *helpers.WelcomeMail) (bool, error) 19 | } 20 | 21 | func (u *fakeWelcomeMailService) SendWelcomeMail(cred *helpers.WelcomeMail) (bool, error) { 22 | return u.SendWelcomeMailFn(cred) 23 | } 24 | 25 | var ( 26 | fakeWelcomeMail fakeWelcomeMailService 27 | 28 | w = welcome.NewWelcome(&fakeWelcomeMail) 29 | ) 30 | 31 | type MailResponse struct { 32 | Status int `json:"status"` 33 | Body string `json:"body"` 34 | } 35 | 36 | //We dont need to mock the email layer, because we will never get there. 37 | func TestNewWelcome_WrongInput(t *testing.T) { 38 | 39 | inputJSON := `{"name": "Victor Steven", "email": "wrongInput"}` 40 | 41 | req, err := http.NewRequest(http.MethodPost, "/welcome", bytes.NewBufferString(inputJSON)) 42 | if err != nil { 43 | t.Errorf("this is the error: %v\n", err) 44 | } 45 | 46 | r := gin.Default() 47 | r.POST("/welcome", w.WelcomeMail) 48 | rr := httptest.NewRecorder() 49 | r.ServeHTTP(rr, req) 50 | 51 | var response = MailResponse{} 52 | err = json.Unmarshal(rr.Body.Bytes(), &response) 53 | if err != nil { 54 | t.Errorf("cannot unmarshal response: %v\n", err) 55 | } 56 | 57 | assert.EqualValues(t, rr.Code, 400) 58 | assert.EqualValues(t, response.Status, 400) 59 | assert.EqualValues(t, response.Body, "The email should be a valid email") 60 | } 61 | 62 | func TestNewWelcome_RequiredNotCorrect(t *testing.T) { 63 | 64 | inputJSON := `{"name": 123, "email": "victor@example.com"}` 65 | 66 | req, err := http.NewRequest(http.MethodPost, "/welcome", bytes.NewBufferString(inputJSON)) 67 | if err != nil { 68 | t.Errorf("this is the error: %v\n", err) 69 | } 70 | 71 | r := gin.Default() 72 | r.POST("/welcome", w.WelcomeMail) 73 | rr := httptest.NewRecorder() 74 | r.ServeHTTP(rr, req) 75 | 76 | var response = MailResponse{} 77 | err = json.Unmarshal(rr.Body.Bytes(), &response) 78 | if err != nil { 79 | t.Errorf("cannot unmarshal response: %v\n", err) 80 | } 81 | 82 | assert.EqualValues(t, rr.Code, 422) 83 | assert.EqualValues(t, response.Status, 422) 84 | assert.EqualValues(t, response.Body, "Please provide valid inputs") 85 | } 86 | 87 | 88 | func TestNewWelcome_Success(t *testing.T) { 89 | 90 | fakeWelcomeMail.SendWelcomeMailFn = func(cred *helpers.WelcomeMail) (bool, error) { 91 | return true, nil 92 | } 93 | 94 | inputJSON := `{"name": "Victor Steven", "email": "victor@example.com"}` 95 | 96 | req, err := http.NewRequest(http.MethodPost, "/welcome", bytes.NewBufferString(inputJSON)) 97 | if err != nil { 98 | t.Errorf("this is the error: %v\n", err) 99 | } 100 | 101 | r := gin.Default() 102 | r.POST("/welcome", w.WelcomeMail) 103 | rr := httptest.NewRecorder() 104 | r.ServeHTTP(rr, req) 105 | 106 | var response = MailResponse{} 107 | err = json.Unmarshal(rr.Body.Bytes(), &response) 108 | if err != nil { 109 | t.Errorf("cannot unmarshal response: %v\n", err) 110 | } 111 | 112 | assert.EqualValues(t, rr.Code, 200) 113 | assert.EqualValues(t, response.Status, 200) 114 | assert.EqualValues(t, response.Body, "Please check your mail") 115 | } 116 | 117 | func TestNewWelcome_Failure(t *testing.T) { 118 | 119 | fakeWelcomeMail.SendWelcomeMailFn = func(cred *helpers.WelcomeMail) (bool, error) { 120 | return false, errors.New("something went wrong sending mail") 121 | } 122 | 123 | inputJSON := `{"name": "Victor Steven", "email": "victor@example.com"}` 124 | 125 | req, err := http.NewRequest(http.MethodPost, "/welcome", bytes.NewBufferString(inputJSON)) 126 | if err != nil { 127 | t.Errorf("this is the error: %v\n", err) 128 | } 129 | 130 | r := gin.Default() 131 | r.POST("/welcome", w.WelcomeMail) 132 | rr := httptest.NewRecorder() 133 | r.ServeHTTP(rr, req) 134 | 135 | var response = MailResponse{} 136 | err = json.Unmarshal(rr.Body.Bytes(), &response) 137 | if err != nil { 138 | t.Errorf("cannot unmarshal response: %v\n", err) 139 | } 140 | 141 | assert.EqualValues(t, rr.Code, 422) 142 | assert.EqualValues(t, response.Status, 422) 143 | assert.EqualValues(t, response.Body, "something went wrong sending mail") 144 | } 145 | 146 | -------------------------------------------------------------------------------- /helpers/helpers.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/go-playground/validator/v10" 7 | "reflect" 8 | "strings" 9 | ) 10 | 11 | type WelcomeMail struct { 12 | FromAdmin string 13 | Temp string 14 | Name string 15 | Email string 16 | Emails []string 17 | Subject string 18 | } 19 | 20 | type EmailResponse struct { 21 | Status int 22 | RespBody string 23 | } 24 | 25 | type WelcomeModel struct { 26 | Name string `json:"name" validate:"required"` 27 | Email string `json:"email" validate:"required,email"` 28 | } 29 | 30 | func ValidateInputs(dataSet interface{}) error { 31 | 32 | var validate *validator.Validate 33 | 34 | validate = validator.New() 35 | 36 | err := validate.Struct(dataSet) 37 | 38 | if err != nil { 39 | //Validation syntax is invalid 40 | if _, ok := err.(*validator.InvalidValidationError); ok { 41 | return errors.New("validation syntax is invalid") 42 | } 43 | 44 | reflected := reflect.ValueOf(dataSet) 45 | 46 | for _, err := range err.(validator.ValidationErrors) { 47 | 48 | // Attempt to find field by name and get json tag name 49 | field, _ := reflected.Type().FieldByName(err.StructField()) 50 | 51 | //If json tag doesn't exist, use lower case of name 52 | name := field.Tag.Get("json") 53 | if name == "" { 54 | name = strings.ToLower(err.StructField()) 55 | } 56 | 57 | switch err.Tag() { 58 | case "required": 59 | return errors.New(fmt.Sprintf("The %s is required", name)) 60 | case "email": 61 | return errors.New(fmt.Sprintf("The %s should be a valid email", name)) 62 | default: 63 | return errors.New(fmt.Sprintf("The %s is invalid", name)) 64 | } 65 | } 66 | } 67 | 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/gin-gonic/gin" 7 | "github.com/joho/godotenv" 8 | "log" 9 | "mail-sending/emails/welcome_mail" 10 | "mail-sending/handlers/welcome" 11 | "net/http" 12 | "os" 13 | "os/signal" 14 | "time" 15 | ) 16 | 17 | func init() { 18 | // loads values from .env into the system 19 | if err := godotenv.Load(); err != nil { 20 | log.Print("No .env file found") 21 | } 22 | } 23 | 24 | func main() { 25 | 26 | appAddr := ":" + os.Getenv("APP_PORT") 27 | 28 | r := gin.Default() 29 | 30 | sendWelcomeMail := welcome_mail.NewService() 31 | 32 | welcomeMail := welcome.NewWelcome(sendWelcomeMail) 33 | 34 | r.POST("/welcome", welcomeMail.WelcomeMail) 35 | 36 | //Starting and Shutting down Server 37 | 38 | srv := &http.Server{ 39 | Addr: appAddr, 40 | Handler: r, 41 | } 42 | fmt.Println("App port: ", appAddr) 43 | go func() { 44 | //service connections 45 | if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { 46 | log.Fatalf("listen: %s\n", err) 47 | } 48 | }() 49 | //Wait for interrupt signal to gracefully shutdown the server with a timeout of 10 seconds 50 | quit := make(chan os.Signal) 51 | signal.Notify(quit, os.Interrupt) 52 | <-quit 53 | log.Println("Shutdown Server ...") 54 | 55 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 56 | defer cancel() 57 | if err := srv.Shutdown(ctx); err != nil { 58 | log.Fatal("Server Shutdown:", err) 59 | } 60 | log.Println("Server exiting") 61 | 62 | } 63 | -------------------------------------------------------------------------------- /templates/welcome_mail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 |