Skip to content

Commit

Permalink
feat: smtp server for sending email can now be set by config
Browse files Browse the repository at this point in the history
  • Loading branch information
JordanKnott committed Dec 23, 2020
1 parent e25a426 commit 9f27bd1
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 87 deletions.
11 changes: 6 additions & 5 deletions conf/taskcafe.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ user = 'taskcafe'
password = 'taskcafe_test'

[smtp]
username = '[email protected]'
password = 'example'
server = 'mail.example.com'
port = 465
connection_security = 'STARTTLS'
username = '[email protected]'
password = ''
from = '[email protected]'
host = 'localhost'
port = 11500
skip_verify = false
17 changes: 16 additions & 1 deletion internal/commands/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

"github.com/jmoiron/sqlx"
"github.com/jordanknott/taskcafe/internal/route"
"github.com/jordanknott/taskcafe/internal/utils"
log "github.com/sirupsen/logrus"
)

Expand Down Expand Up @@ -75,11 +76,25 @@ func newWebCmd() *cobra.Command {
log.Warn("server.secret is not set, generating a random secret")
secret = uuid.New().String()
}
r, _ := route.NewRouter(db, []byte(secret))
r, _ := route.NewRouter(db, utils.EmailConfig{
From: viper.GetString("smtp.from"),
Host: viper.GetString("smtp.host"),
Port: viper.GetInt("smtp.port"),
Username: viper.GetString("smtp.username"),
Password: viper.GetString("smtp.password"),
InsecureSkipVerify: viper.GetBool("smtp.skip_verify"),
}, []byte(secret))
return http.ListenAndServe(viper.GetString("server.hostname"), r)
},
}

viper.SetDefault("smtp.from", "[email protected]")
viper.SetDefault("smtp.host", "localhost")
viper.SetDefault("smtp.port", 587)
viper.SetDefault("smtp.username", "")
viper.SetDefault("smtp.password", "")
viper.SetDefault("smtp.skip_verify", false)

cc.Flags().Bool("migrate", false, "if true, auto run's schema migrations before starting the web server")

viper.BindPFlag("migrate", cc.Flags().Lookup("migrate"))
Expand Down
5 changes: 3 additions & 2 deletions internal/graph/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ import (
)

// NewHandler returns a new graphql endpoint handler.
func NewHandler(repo db.Repository) http.Handler {
func NewHandler(repo db.Repository, emailConfig utils.EmailConfig) http.Handler {
c := Config{
Resolvers: &Resolver{
Repository: repo,
Repository: repo,
EmailConfig: emailConfig,
},
}
c.Directives.HasRole = func(ctx context.Context, obj interface{}, next graphql.Resolver, roles []RoleLevel, level ActionLevel, typeArg ObjectType) (interface{}, error) {
Expand Down
6 changes: 4 additions & 2 deletions internal/graph/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import (
"sync"

"github.com/jordanknott/taskcafe/internal/db"
"github.com/jordanknott/taskcafe/internal/utils"
)

// Resolver handles resolving GraphQL queries & mutations
type Resolver struct {
Repository db.Repository
mu sync.Mutex
Repository db.Repository
EmailConfig utils.EmailConfig
mu sync.Mutex
}
80 changes: 5 additions & 75 deletions internal/graph/schema.resolvers.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ package graph

import (
"context"
"crypto/tls"
"database/sql"
"encoding/json"
"errors"
Expand All @@ -16,12 +15,11 @@ import (
"github.com/jordanknott/taskcafe/internal/auth"
"github.com/jordanknott/taskcafe/internal/db"
"github.com/jordanknott/taskcafe/internal/logger"
"github.com/jordanknott/taskcafe/internal/utils"
"github.com/lithammer/fuzzysearch/fuzzy"
hermes "github.com/matcornic/hermes/v2"
log "github.com/sirupsen/logrus"
"github.com/vektah/gqlparser/v2/gqlerror"
"golang.org/x/crypto/bcrypt"
gomail "gopkg.in/mail.v2"
)

func (r *labelColorResolver) ID(ctx context.Context, obj *db.LabelColor) (uuid.UUID, error) {
Expand Down Expand Up @@ -193,79 +191,11 @@ func (r *mutationResolver) InviteProjectMembers(ctx context.Context, input Invit
if err != nil {
return &InviteProjectMembersPayload{Ok: false}, err
}
// send out invitation
// add project invite entry
// send out notification?
h := hermes.Hermes{
// Optional Theme
Product: hermes.Product{
// Appears in header & footer of e-mails
Name: "Taskscafe",
Link: "http://localhost:3333/",
// Optional product logo
Logo: "https://github.com/JordanKnott/taskcafe/raw/master/.github/taskcafe-full.png",
},
}

email := hermes.Email{
Body: hermes.Body{
Name: "Jordan Knott",
Intros: []string{
"You have been invited to join Taskcafe",
},
Actions: []hermes.Action{
{
Instructions: "To get started with Taskcafe, please click here:",
Button: hermes.Button{
Color: "#7367F0", // Optional action button color
TextColor: "#FFFFFF",
Text: "Register your account",
Link: "http://localhost:3000/register?confirmToken=" + confirmToken.ConfirmTokenID.String(),
},
},
},
Outros: []string{
"Need help, or have questions? Just reply to this email, we'd love to help.",
},
},
}

// Generate an HTML email with the provided contents (for modern clients)
emailBody, err := h.GenerateHTML(email)
invite := utils.EmailInvite{To: *invitedMember.Email, FullName: *invitedMember.Email, ConfirmToken: confirmToken.ConfirmTokenID.String()}
err = utils.SendEmailInvite(r.EmailConfig, invite)
if err != nil {
panic(err) // Tip: Handle error with something else than a panic ;)
}
emailBodyPlain, err := h.GeneratePlainText(email)
if err != nil {
panic(err) // Tip: Handle error with something else than a panic ;)
}

m := gomail.NewMessage()

// Set E-Mail sender
m.SetHeader("From", "[email protected]")

// Set E-Mail receivers
m.SetHeader("To", invitedUser.Email)

// Set E-Mail subject
m.SetHeader("Subject", "You have been invited to Taskcafe")

// Set E-Mail body. You can set plain text or html with text/html
m.SetBody("text/html", emailBody)
m.AddAlternative("text/plain", emailBodyPlain)

// Settings for SMTP server
d := gomail.NewDialer("127.0.0.1", 11500, "[email protected]", "")

// This is only needed when SSL/TLS certificate is not valid on server.
// In production this should be set to false.
d.TLSConfig = &tls.Config{InsecureSkipVerify: true}

// Now send E-Mail
if err := d.DialAndSend(m); err != nil {
fmt.Println(err)
panic(err)
logger.New(ctx).WithError(err).Error("issue sending email")
return &InviteProjectMembersPayload{Ok: false}, err
}
} else {
return &InviteProjectMembersPayload{Ok: false}, err
Expand Down
5 changes: 3 additions & 2 deletions internal/route/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/jordanknott/taskcafe/internal/frontend"
"github.com/jordanknott/taskcafe/internal/graph"
"github.com/jordanknott/taskcafe/internal/logger"
"github.com/jordanknott/taskcafe/internal/utils"
)

// FrontendHandler serves an embed React client through chi
Expand Down Expand Up @@ -64,7 +65,7 @@ type TaskcafeHandler struct {
}

// NewRouter creates a new router for chi
func NewRouter(dbConnection *sqlx.DB, jwtKey []byte) (chi.Router, error) {
func NewRouter(dbConnection *sqlx.DB, emailConfig utils.EmailConfig, jwtKey []byte) (chi.Router, error) {
formatter := new(log.TextFormatter)
formatter.TimestampFormat = "02-01-2006 15:04:05"
formatter.FullTimestamp = true
Expand Down Expand Up @@ -94,7 +95,7 @@ func NewRouter(dbConnection *sqlx.DB, jwtKey []byte) (chi.Router, error) {
r.Group(func(mux chi.Router) {
mux.Use(auth.Middleware)
mux.Post("/users/me/avatar", taskcafeHandler.ProfileImageUpload)
mux.Handle("/graphql", graph.NewHandler(*repository))
mux.Handle("/graphql", graph.NewHandler(*repository, emailConfig))
})

frontend := FrontendHandler{staticPath: "build", indexPath: "index.html"}
Expand Down
94 changes: 94 additions & 0 deletions internal/utils/mail.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package utils

import (
"crypto/tls"

hermes "github.com/matcornic/hermes/v2"
gomail "gopkg.in/mail.v2"
)

type EmailConfig struct {
Host string
Port int
From string
Username string
Password string
SiteURL string
InsecureSkipVerify bool
}

type EmailInvite struct {
ConfirmToken string
FullName string
To string
}

func SendEmailInvite(config EmailConfig, invite EmailInvite) error {
h := hermes.Hermes{
Product: hermes.Product{
Name: "Taskscafe",
Link: config.SiteURL,
Logo: "https://github.com/JordanKnott/taskcafe/raw/master/.github/taskcafe-full.png",
},
}

email := hermes.Email{
Body: hermes.Body{
Name: invite.FullName,
Intros: []string{
"You have been invited to join Taskcafe",
},
Actions: []hermes.Action{
{
Instructions: "To get started with Taskcafe, please click here:",
Button: hermes.Button{
Color: "#7367F0", // Optional action button color
TextColor: "#FFFFFF",
Text: "Register your account",
Link: config.SiteURL + "/register?confirmToken=" + invite.ConfirmToken,
},
},
},
Outros: []string{
"Need help, or have questions? Just reply to this email, we'd love to help.",
},
},
}

emailBody, err := h.GenerateHTML(email)
if err != nil {
return err
}
emailBodyPlain, err := h.GeneratePlainText(email)
if err != nil {
return err
}

m := gomail.NewMessage()

// Set E-Mail sender
m.SetHeader("From", config.From)

// Set E-Mail receivers
m.SetHeader("To", invite.To)

// Set E-Mail subject
m.SetHeader("Subject", "You have been invited to Taskcafe")

// Set E-Mail body. You can set plain text or html with text/html
m.SetBody("text/html", emailBody)
m.AddAlternative("text/plain", emailBodyPlain)

// Settings for SMTP server
d := gomail.NewDialer(config.Host, config.Port, config.Username, config.Password)

// This is only needed when SSL/TLS certificate is not valid on server.
// In production this should be set to false.
d.TLSConfig = &tls.Config{InsecureSkipVerify: config.InsecureSkipVerify}

// Now send E-Mail
if err := d.DialAndSend(m); err != nil {
return err
}
return nil
}

0 comments on commit 9f27bd1

Please sign in to comment.