feat: smtp server for sending email can now be set by config
This commit is contained in:
parent
e25a426e7b
commit
9f27bd157f
@ -17,8 +17,9 @@ user = 'taskcafe'
|
|||||||
password = 'taskcafe_test'
|
password = 'taskcafe_test'
|
||||||
|
|
||||||
[smtp]
|
[smtp]
|
||||||
username = 'admin@example.com'
|
username = 'taskcafe@example.com'
|
||||||
password = 'example'
|
password = ''
|
||||||
server = 'mail.example.com'
|
from = 'no-reply@taskcafe.com'
|
||||||
port = 465
|
host = 'localhost'
|
||||||
connection_security = 'STARTTLS'
|
port = 11500
|
||||||
|
skip_verify = false
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/jordanknott/taskcafe/internal/route"
|
"github.com/jordanknott/taskcafe/internal/route"
|
||||||
|
"github.com/jordanknott/taskcafe/internal/utils"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -75,11 +76,25 @@ func newWebCmd() *cobra.Command {
|
|||||||
log.Warn("server.secret is not set, generating a random secret")
|
log.Warn("server.secret is not set, generating a random secret")
|
||||||
secret = uuid.New().String()
|
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)
|
return http.ListenAndServe(viper.GetString("server.hostname"), r)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viper.SetDefault("smtp.from", "no-reply@example.com")
|
||||||
|
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")
|
cc.Flags().Bool("migrate", false, "if true, auto run's schema migrations before starting the web server")
|
||||||
|
|
||||||
viper.BindPFlag("migrate", cc.Flags().Lookup("migrate"))
|
viper.BindPFlag("migrate", cc.Flags().Lookup("migrate"))
|
||||||
|
@ -26,10 +26,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// NewHandler returns a new graphql endpoint handler.
|
// 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{
|
c := Config{
|
||||||
Resolvers: &Resolver{
|
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) {
|
c.Directives.HasRole = func(ctx context.Context, obj interface{}, next graphql.Resolver, roles []RoleLevel, level ActionLevel, typeArg ObjectType) (interface{}, error) {
|
||||||
|
@ -7,10 +7,12 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/jordanknott/taskcafe/internal/db"
|
"github.com/jordanknott/taskcafe/internal/db"
|
||||||
|
"github.com/jordanknott/taskcafe/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Resolver handles resolving GraphQL queries & mutations
|
// Resolver handles resolving GraphQL queries & mutations
|
||||||
type Resolver struct {
|
type Resolver struct {
|
||||||
Repository db.Repository
|
Repository db.Repository
|
||||||
|
EmailConfig utils.EmailConfig
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ package graph
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
@ -16,12 +15,11 @@ import (
|
|||||||
"github.com/jordanknott/taskcafe/internal/auth"
|
"github.com/jordanknott/taskcafe/internal/auth"
|
||||||
"github.com/jordanknott/taskcafe/internal/db"
|
"github.com/jordanknott/taskcafe/internal/db"
|
||||||
"github.com/jordanknott/taskcafe/internal/logger"
|
"github.com/jordanknott/taskcafe/internal/logger"
|
||||||
|
"github.com/jordanknott/taskcafe/internal/utils"
|
||||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||||
hermes "github.com/matcornic/hermes/v2"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/vektah/gqlparser/v2/gqlerror"
|
"github.com/vektah/gqlparser/v2/gqlerror"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
gomail "gopkg.in/mail.v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *labelColorResolver) ID(ctx context.Context, obj *db.LabelColor) (uuid.UUID, error) {
|
func (r *labelColorResolver) ID(ctx context.Context, obj *db.LabelColor) (uuid.UUID, error) {
|
||||||
@ -193,79 +191,11 @@ func (r *mutationResolver) InviteProjectMembers(ctx context.Context, input Invit
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return &InviteProjectMembersPayload{Ok: false}, err
|
return &InviteProjectMembersPayload{Ok: false}, err
|
||||||
}
|
}
|
||||||
// send out invitation
|
invite := utils.EmailInvite{To: *invitedMember.Email, FullName: *invitedMember.Email, ConfirmToken: confirmToken.ConfirmTokenID.String()}
|
||||||
// add project invite entry
|
err = utils.SendEmailInvite(r.EmailConfig, invite)
|
||||||
// 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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err) // Tip: Handle error with something else than a panic ;)
|
logger.New(ctx).WithError(err).Error("issue sending email")
|
||||||
}
|
return &InviteProjectMembersPayload{Ok: false}, err
|
||||||
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", "no-reply@taskcafe.com")
|
|
||||||
|
|
||||||
// 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, "no-reply@taskcafe.com", "")
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return &InviteProjectMembersPayload{Ok: false}, err
|
return &InviteProjectMembersPayload{Ok: false}, err
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/jordanknott/taskcafe/internal/frontend"
|
"github.com/jordanknott/taskcafe/internal/frontend"
|
||||||
"github.com/jordanknott/taskcafe/internal/graph"
|
"github.com/jordanknott/taskcafe/internal/graph"
|
||||||
"github.com/jordanknott/taskcafe/internal/logger"
|
"github.com/jordanknott/taskcafe/internal/logger"
|
||||||
|
"github.com/jordanknott/taskcafe/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FrontendHandler serves an embed React client through chi
|
// FrontendHandler serves an embed React client through chi
|
||||||
@ -64,7 +65,7 @@ type TaskcafeHandler struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewRouter creates a new router for chi
|
// 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 := new(log.TextFormatter)
|
||||||
formatter.TimestampFormat = "02-01-2006 15:04:05"
|
formatter.TimestampFormat = "02-01-2006 15:04:05"
|
||||||
formatter.FullTimestamp = true
|
formatter.FullTimestamp = true
|
||||||
@ -94,7 +95,7 @@ func NewRouter(dbConnection *sqlx.DB, jwtKey []byte) (chi.Router, error) {
|
|||||||
r.Group(func(mux chi.Router) {
|
r.Group(func(mux chi.Router) {
|
||||||
mux.Use(auth.Middleware)
|
mux.Use(auth.Middleware)
|
||||||
mux.Post("/users/me/avatar", taskcafeHandler.ProfileImageUpload)
|
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"}
|
frontend := FrontendHandler{staticPath: "build", indexPath: "index.html"}
|
||||||
|
94
internal/utils/mail.go
Normal file
94
internal/utils/mail.go
Normal file
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user